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.
- Sign in to Nevox and check, in the company switcher, that the right company is open.
- Go to Developers → Webhooks → Add endpoint.
- Paste the URL in your system that will receive the events (it must start with
https://). - Pick the environment (Live for real data) and check the events.
- 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
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"
}'{
"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
POST /webhooks/nevox HTTP/1.1
Content-Type: application/json
Nevox-Signature: t=1762183200,v1=9ab3...
Nevox-Event-Id: evt_1z8x
Nevox-Environment: live{
"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
| Event | When |
|---|---|
conversation.created | New conversation, on any channel |
conversation.handoff | The agent handed off to a human (reason in reason) |
conversation.human_assigned | Someone on the team replied for the first time |
conversation.closed | Conversation closed |
message.created | New message (high volume; use it only if you need to mirror the chat) |
contact.created / contact.updated | Contact 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.identified | An anonymous visitor was linked to a contact |
analytics.export_ready | An export finished |
form.submitted | Someone 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.
{
"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"
}
}
}phonecomes as the person typed it;phone_e164only when the country is certain (the number starts with+or has the length of a number from the site's country). Otherwisenull.fieldshas every other field of the form, with the site's label, plus page, language, origin andutm_*(these keys are in Portuguese:Página,Idioma,Veio de).page,referrer,languageandutmrepeat those values so you don't need to readfields.liveenvironment only. The event is sent once per submission; use the formid(frm_…) to avoid saving it twice.
Step by step to receive the forms
- Register an endpoint in Developers → Webhooks with the
form.submittedevent, in the Live environment (how to register). Keep thewhsec_…secret in your system. - In your system, check the signature of each event (how to check), save the form by its
id(frm_…) and answer2xx. - 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.
- To bring the forms sent before that, use Generate key in Developers → API keys for a Live key with only the
forms:readscope 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
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"{
"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
2xxwithin 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_atand fetch the current state from the API if you need to. - The same event may arrive more than once. Use the event
idto 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
// 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");
}