Documentation menu

Webhooks

Get real-time notifications when a conversation is handed off to a human, a lead shows up or an appointment is requested.

With webhooks, Nevox notifies your system when something happens. That's how a conversation that needs a human shows up right in your admin panel, without anyone opening Nevox.

Register an endpoint

In the dashboard

No API key needed.

  1. Sign in to Nevox and check, in the company switcher, that the right company is open.
  2. Go to Developers → Webhooks → Add endpoint.
  3. Paste the URL in your system that will receive the events (it must start with https://).
  4. Pick the environment (Live for real data) and check the events.
  5. Save and copy the whsec_… secret: it is shown only once. Lost it? Delete the endpoint and register it again, or create a new one through the API (rotate-secret, below).

The same screen shows the recent deliveries of each endpoint, with Resend, and the on/off switch. To send a test event, use POST /v1/webhooks/{id}/test (below).

Through the API

POST /v1/webhooks · scope webhooks:write

bash
curl https://api.nevox.ai/v1/webhooks \
  -H "Authorization: Bearer $NEVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourcompany.com/webhooks/nevox",
    "events": ["conversation.handoff", "conversation.closed", "lead.created"],
    "description": "Admin support queue"
  }'
json
{
  "id": "whk_6tq2",
  "url": "https://api.yourcompany.com/webhooks/nevox",
  "events": ["conversation.handoff", "conversation.closed", "lead.created"],
  "status": "enabled",
  "secret": "whsec_...",
  "created_at": "2026-11-03T17:20:00Z"
}

The secret is shown only once. Other endpoints: GET /v1/webhooks, PATCH /v1/webhooks/{id}, DELETE /v1/webhooks/{id}, POST /v1/webhooks/{id}/rotate-secret, POST /v1/webhooks/{id}/test.

Use "events": ["*"] to receive all of them. The endpoint belongs to the environment of the key that created it: an endpoint created with a test key only receives events from test conversations. The URL must use https and cannot point to an internal network address.

POST /v1/webhooks/{id}/test immediately sends a webhook.test event to that endpoint only and returns the result.

Event format

http
POST /webhooks/nevox HTTP/1.1
Content-Type: application/json
Nevox-Signature: t=1762183200,v1=9ab3...
Nevox-Event-Id: evt_1z8x
Nevox-Environment: live
json
{
  "id": "evt_1z8x",
  "type": "conversation.handoff",
  "created_at": "2026-11-03T17:20:00Z",
  "data": {
    "conversation": {
      "id": "conv_7mk2p9",
      "agent_id": "agt_8f2k1x",
      "channel": "whatsapp",
      "status": "handoff",
      "summary": "Patient is asking for a refund for the 10/28 appointment; says they couldn't join the call.",
      "reason": "refund_request"
    },
    "contact": {
      "id": "ctc_4hq81",
      "external_id": "patient_1042",
      "name": "Mary Smith"
    }
  }
}

The signature uses the same format as actions: HMAC-SHA256 of "{t}.{raw body}" with the endpoint's secret.

Events

EventWhen
conversation.createdNew conversation, on any channel
conversation.handoffThe agent handed off to a human (reason in reason)
conversation.human_assignedSomeone on the team replied for the first time
conversation.closedConversation closed
message.createdNew message (high volume; use it only if you need to mirror the chat)
contact.created / contact.updatedContact created or its data changed
lead.created (coming soon)A visitor or contact was qualified as a lead
appointment.requested (coming soon)The agent collected an appointment request
knowledge.ready / knowledge.failed (coming soon)A file or URL finished processing
learning.suggestion_created (beta)New learning suggestion
visitor.identifiedAn anonymous visitor was linked to a contact
analytics.export_readyAn export finished
form.submittedSomeone sent a form on one of the company's sites built by Nevox

reason values in conversation.handoff: complaint, refund_request, special_request, forbidden_topic, user_asked_human, low_confidence, action_failed, manual.

Site forms

When Nevox builds the company's website, each form sent on it (contact, quote, sign-up) becomes the form.submitted event and is listed in GET /v1/form_submissions, in the same format. Subscribe to the event to receive it right away; use the list to fetch the history or check you missed nothing.

json
{
  "id": "evt_9c1e0b7a4f2d4e8a9b3c5d6e7f8a9b0c",
  "type": "form.submitted",
  "created_at": "2026-10-28T14:05:12Z",
  "data": {
    "form_submission": {
      "id": "frm_3f9c2a7e0b1d4c5e8f6a9b0c1d2e3f40",
      "site": { "id": "6a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "name": "Rivermark site", "domain": "rivermark.com" },
      "form": "Get a quote",
      "name": "John Miller",
      "email": "john@example.com",
      "phone": "(512) 555-0142",
      "phone_e164": "+15125550142",
      "message": "Weekly cleaning for a 3,000 sq ft office.",
      "fields": {
        "City": "Austin",
        "Página": "/get-a-quote",
        "Idioma": "en",
        "Veio de": "https://www.google.com/",
        "utm_source": "google",
        "utm_medium": "cpc",
        "utm_campaign": "office-cleaning"
      },
      "page": "/get-a-quote",
      "referrer": "https://www.google.com/",
      "language": "en",
      "utm": { "source": "google", "medium": "cpc", "campaign": "office-cleaning", "term": null, "content": null },
      "created_at": "2026-10-28T14:05:12Z"
    }
  }
}
  • phone comes as the person typed it; phone_e164 only when the country is certain (the number starts with + or has the length of a number from the site's country). Otherwise null.
  • fields has every other field of the form, with the site's label, plus page, language, origin and utm_* (these keys are in Portuguese: Página, Idioma, Veio de). page, referrer, language and utm repeat those values so you don't need to read fields.
  • live environment only. The event is sent once per submission; use the form id (frm_…) to avoid saving it twice.

Step by step to receive the forms

  1. Register an endpoint in Developers → Webhooks with the form.submitted event, in the Live environment (how to register). Keep the whsec_… secret in your system.
  2. In your system, check the signature of each event (how to check), save the form by its id (frm_…) and answer 2xx.
  3. Send a real form on the site: it should show up in your system within seconds, and the delivery shows in Recent deliveries in the dashboard.
  4. To bring the forms sent before that, use Generate key in Developers → API keys for a Live key with only the forms:read scope and use the list below.

The secret and the key give access to the name, phone and email of whoever filled the form: keep them on the server, never in the browser or in an open message. Leaked? Replace the secret (delete and register the endpoint again, or rotate-secret) and revoke the key.

Fetch the history

bash
curl "https://api.nevox.ai/v1/form_submissions?form=Get%20a%20quote&created_after=2026-10-01T00:00:00Z&limit=50"   -H "Authorization: Bearer $NEVOX_SECRET_KEY"
json
{
  "data": [ { "id": "frm_3f9c2a7e0b1d4c5e8f6a9b0c1d2e3f40", "form": "Get a quote", "name": "John Miller", "...": "..." } ],
  "has_more": true,
  "next_cursor": "MTIz"
}

Scope forms:read. Newest first, up to 100 per page; for the next one, send cursor with next_cursor. Filters: site_id (the site id), form (the exact form name) and created_after (ISO 8601 date).

Delivery and retries

  • Respond with any 2xx within 10 seconds. Process the event afterwards, in a queue.
  • Without a 2xx, Nevox retries after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h.
  • After 3 days of failures, the endpoint is disabled and the account owner gets an email.
  • Delivery order is not guaranteed. Use created_at and fetch the current state from the API if you need to.
  • The same event may arrive more than once. Use the event id to ignore duplicates.

The latest deliveries show up in Dashboard → Developers → Webhooks, with a button to resend. The history is kept for 30 days.

The conversation summary may be null in the event: the summary is generated right after and is available at GET /v1/conversations/{id}.

Example: support queue in your admin

js
// app/api/webhooks/nevox/route.ts (Next.js)
import { isValidSignature } from "@/lib/nevox";

export async function POST(req: Request) {
  const raw = await req.text();
  const ok = isValidSignature(
    raw,
    req.headers.get("nevox-signature") ?? "",
    process.env.NEVOX_WEBHOOK_SECRET!
  );
  if (!ok) return new Response("invalid signature", { status: 400 });

  const event = JSON.parse(raw);

  if (event.type === "conversation.handoff") {
    await db.ticket.upsert({
      where: { nevoxConversationId: event.data.conversation.id },
      create: {
        nevoxConversationId: event.data.conversation.id,
        patientId: event.data.contact.external_id,
        summary: event.data.conversation.summary,
        reason: event.data.conversation.reason,
      },
      update: {},
    });
  }

  return new Response("ok");
}