Skip to content

Quickstart

Ship your first Nexo integration in minutes, then expand to richer experiences.

What you need

  • A webhook endpoint in your backend: POST /webhook
  • One shared secret: WEBHOOK_SECRET
  • Your app configured in Nexo with webhook_url and WEBHOOK_SECRET

Everything else in this docs site is optional capability expansion.

1) Implement your webhook

Your webhook receives a request from Nexo and returns a response envelope.

Profile context: Webhook payloads may include approved profile attributes such as locale, language, location, age, date_of_birth, gender, and dietary_preferences. Nexo manages consent collection and scope enforcement before sending profile data to your webhook. Parse defensively and ignore unknown fields.

JSON response

{
  "schema_version": "2026-03",
  "status": "completed",
  "content_parts": [{ "type": "text", "text": "Your assistant response" }]
}

SSE response

Use Content-Type: text/event-stream and stream delta events followed by done.

Minimal Python webhook

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Payload(BaseModel):
    event: str | None = None
    app: dict | None = None
    thread: dict | None = None
    message: dict | None = None
    history_tail: list[dict] | None = None
    profile: dict | None = None
    metadata: dict | None = None
    timestamp: str | None = None

@app.post("/webhook")
def webhook(payload: Payload):
    content = (payload.message or {}).get("content", "")
    profile = payload.profile or {}
    name = profile.get("display_name") or profile.get("name")
    locale = profile.get("locale") or profile.get("language")
    dietary = profile.get("dietary_preferences")
    text = f"{name}, you said: {content}" if name else f"Echo: {content}"
    hints = [h for h in [f"locale={locale}" if locale else None, f"dietary={dietary}" if dietary else None] if h]
    return {
      "schema_version": "2026-03",
      "status": "completed",
      "content_parts": [{"type": "text", "text": f"{text} ({', '.join(hints)})" if hints else text}],
    }

Minimal TypeScript webhook

import express from "express";

const app = express();
app.use(express.json());

app.post("/webhook", (req, res) => {
  const content = req.body?.message?.content ?? "";
  const profile = req.body?.profile ?? {};
  const name = profile.display_name ?? profile.name ?? null;
  const locale = profile.locale ?? profile.language ?? null;
  const dietary = profile.dietary_preferences ?? null;
  let text = name ? `${name}, you said: ${content}` : `Echo: ${content}`;
  const hints = [];
  if (locale) hints.push(`locale=${locale}`);
  if (dietary) hints.push(`dietary=${dietary}`);
  if (hints.length) text = `${text} (${hints.join(", ")})`;
  res.json({
    schema_version: "2026-03",
    status: "completed",
    content_parts: [{ type: "text", text }],
  });
});

2) Configure Nexo

This step connects Nexo to your webhook in production. For local-only testing, skip to step 4.

  1. Go to nexo.luzia.com
  2. Create or open your app
  3. Set your webhook URL and WEBHOOK_SECRET
  4. Send a test message and verify logs on your backend

3) Validate request handling

Checklist:

  • Verify X-App-Id
  • Verify timestamp and signature (X-Timestamp, X-Signature)
  • Return valid JSON or SSE stream

Signature verification — Python (FastAPI)

import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

WEBHOOK_SECRET = "your_webhook_secret"

def verify_signature(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    # Reject requests with a timestamp older than 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        return False
    payload = f"{timestamp}.{body.decode()}"
    expected = "sha256=" + hmac.new(
        secret.encode(), payload.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.post("/webhook")
async def webhook(request: Request):
    body = await request.body()
    timestamp = request.headers.get("X-Timestamp", "")
    signature = request.headers.get("X-Signature", "")

    if not verify_signature(WEBHOOK_SECRET, timestamp, body, signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    payload = await request.json()
    # ... process payload

Signature verification — TypeScript (Express)

import crypto from "crypto";
import express, { Request, Response, NextFunction } from "express";

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET ?? "";

function verifySignature(
  secret: string,
  timestamp: string,
  body: string,
  signature: string
): boolean {
  // Reject requests with a timestamp older than 5 minutes
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;
  const payload = `${timestamp}.${body}`;
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(payload).digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Use express.raw to access the raw body for signature verification
app.post("/webhook", express.raw({ type: "application/json" }), (req: Request, res: Response) => {
  const timestamp = req.headers["x-timestamp"] as string ?? "";
  const signature = req.headers["x-signature"] as string ?? "";
  const body = req.body as Buffer;

  if (!verifySignature(WEBHOOK_SECRET, timestamp, body.toString(), signature)) {
    res.status(401).json({ error: "Invalid signature" });
    return;
  }

  const payload = JSON.parse(body.toString());
  // ... process payload
});

4) Test your webhook directly

curl -X POST "http://localhost:8080/webhook" \
  -H "Content-Type: application/json" \
  -d '{"event":"message_received","app":{"id":"app-uuid","name":"Demo"},"thread":{"id":"thread-uuid","customer_id":"user-123"},"message":{"id":"msg-uuid","seq":1,"role":"user","content":"hello","content_json":{}},"history_tail":[],"profile":{"display_name":"María","locale":"es-MX","dietary_preferences":"vegetarian"},"metadata":{},"timestamp":"2026-03-04T12:00:00Z"}'

Expected response:

{
  "schema_version": "2026-03",
  "status": "completed",
  "content_parts": [{ "type": "text", "text": "Your assistant response" }]
}

5) Submit your app for review

Once your webhook is working and configured in the partner portal, submit your app for review:

  1. Go to your app in nexo.luzia.com
  2. Click Submit for review
  3. The Nexo team will approve or provide feedback
  4. Once approved, your app appears in the public catalog

See API Reference - App lifecycle for the full workflow.

Next steps

What to build next

Once your webhook is working, consider these patterns:

Add rich cards and actions

Return cards and actions alongside content_parts to give users structured UI:

{
  "schema_version": "2026-03",
  "status": "completed",
  "content_parts": [{ "type": "text", "text": "Here are today's top stories." }],
  "cards": [
    {
      "type": "source",
      "title": "Article title",
      "subtitle": "Publisher — date",
      "description": "Excerpt...",
      "metadata": { "url": "https://example.com/article" }
    }
  ],
  "metadata": {
    "prompt_suggestions": [
      "Show me another angle",
      "Summarize this in 3 bullets"
    ]
  },
  "actions": [
    { "id": "read_1", "label": "Read full article", "url": "https://example.com/article", "style": "secondary" }
  ]
}

metadata.prompt_suggestions renders as contextual clickable chips in chat.

For starter chips before any message is sent, publish prompt suggestions from GET /.well-known/agent.json under capabilities.items[].metadata.prompt_suggestions.

Add RAG

If your integration has a knowledge base (news, product catalog, documentation), add retrieval-augmented generation. See the production examples:

Add vertical orchestration

For multi-step flows beyond Q&A, start from the flagship vertical examples:

  • Routines -- Morning briefings, schedule actions, follow-up reminders
  • Food Ordering -- Discovery, basket building, checkout approval, delivery tracking
  • Travel Planning -- Itinerary planning, flight comparison, booking handoff, budget guidance, disruption replanning

Add live event push

For time-sensitive data (scores, price changes, flight updates, breaking news), push events proactively to subscriber threads:

curl -X POST "https://nexo.luzia.com/api/apps/YOUR_APP_ID/events" \
  -H "X-App-Secret: YOUR_APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "price_drop",
    "significance": 0.75,
    "summary": "MacBook Pro 30% off",
    "detail": "The MacBook Pro M4 is now $1399, down from $1999. Deal expires in 4 hours.",
    "card": {
      "type": "product",
      "title": "MacBook Pro M4",
      "subtitle": "$1399 (was $1999)",
      "badges": ["30% off", "Limited time"],
      "metadata": { "url": "https://store.example.com/macbook" }
    },
    "priority": "high"
  }'

Full reference: API Reference - Push Events API