Skip to content

Webhooks send your agents' events to your own server as they happen: a conversation started, a conversation ended (with its transcript, summary and outcome), a WhatsApp message received, a tool called. Use them to push leads into your CRM, confirmed orders into your store, or a summary into your team's chat.

​Add an endpoint

On Webhooks, add your endpoint's URL, choose its events, and choose whether it listens to all your agents or one. You can add up to 10 endpoints per account.

When you create the endpoint, its signing secret (whsec_…) is shown once. Store it on your server, for example as VOICE213_WEBHOOK_SECRET. Lost it? New secret replaces it with a new one, also shown once.

Send test delivers a signed sample conversation.ended event to your endpoint right away. Its data.test is true.

​Events

EventWhen
conversation.startedA call connects, or a customer starts a new WhatsApp thread
conversation.endedA call ends, or a WhatsApp thread closes (when the customer writes again after 24 hours), once its summary is written
message.receivedA WhatsApp message or voice note arrives
tool.calledOne of your tools ran during a call

​The delivery

Every delivery is a POST with a JSON body:

{
  "id": "4b1f6c1e-8e43-4d43-9d55-6e8c3b2a9f10",
  "event": "conversation.ended",
  "createdAt": "2026-10-04T12:00:00.000Z",
  "data": { "...": "..." }
}

and these headers:

HeaderValue
X-Voice213-EventThe event name
X-Voice213-DeliveryThe delivery's id, the same as id. Dedupe on it
X-Voice213-Signaturesha256= and the hex HMAC-SHA256 of the raw body, keyed with your signing secret
User-AgentVoice213-Webhooks/1.0

Answer with any 2xx status within 10 seconds. A delivery that times out, can't connect or gets a 5xx is retried once, right away. A 4xx isn't retried. Deliveries never hold up a conversation, and they may arrive in a different order from the events. Sort by createdAt, and handle conversation.ended even if you never saw the conversation.started.

Your endpoint must be a public https:// address. Private and local addresses are refused, also when a host name only resolves to one at delivery time. A redirect is followed only to another path on the same origin, so a delivery never lands anywhere else.

​Verify the signature

Check every delivery before you trust it: compute the HMAC-SHA256 of the raw body, exactly the bytes you received and not re-serialised JSON, with your secret, and compare it with the header in constant time.

// npm install @voice213/agent-client
import { constructWebhookEvent } from "@voice213/agent-client/server";

// Next.js App Router: app/api/voice213/route.js
export async function POST(request) {
  let event;
  try {
    event = await constructWebhookEvent(await request.text(), request.headers.get("x-voice213-signature"), process.env.VOICE213_WEBHOOK_SECRET);
  } catch {
    return new Response("Bad signature", { status: 400 });
  }
  if (event.event === "conversation.ended") await saveConversation(event.data);
  return new Response(null, { status: 204 });
}

See Webhook Verification for the SDK's helpers and types.

​Payloads

Every event's data carries the agentId of the agent concerned and the conversationId, the id shown in Conversations and passed to your tools as {{conversation_id}}. channel is one of web (the public page, or a client without a browser Origin such as a server or a native app), widget (a website: the widget or the SDK), test (a dashboard test call), whatsapp_chat or whatsapp_call.

​conversation.started

{
  "id": "0c2b8f0e-7a1d-4a8e-9d5f-2f1e3c4b5a69",
  "event": "conversation.started",
  "createdAt": "2026-10-04T11:58:12.000Z",
  "data": {
    "agentId": "6ac2aab4f3fdade650f3b013",
    "conversationId": "6ac2aab5f3fdade650f3b120",
    "channel": "whatsapp_call",
    "contact": { "name": "Karim", "phone": "+213555123456", "waId": "213555123456" },
    "startedAt": "2026-10-04T11:58:12.000Z"
  }
}

contact is empty ("" everywhere) for web calls, where the caller is anonymous.

​conversation.ended

{
  "id": "4b1f6c1e-8e43-4d43-9d55-6e8c3b2a9f10",
  "event": "conversation.ended",
  "createdAt": "2026-10-04T12:00:00.000Z",
  "data": {
    "agentId": "6ac2aab4f3fdade650f3b013",
    "conversationId": "6ac2aab5f3fdade650f3b120",
    "channel": "widget",
    "contact": { "name": "", "phone": "", "waId": "" },
    "startedAt": "2026-10-04T11:58:12.000Z",
    "endedAt": "2026-10-04T11:59:54.000Z",
    "durationSeconds": 102,
    "endReason": "agent_ended",
    "summary": "سأل الزبون عن سعر كريم الصبار والتوصيل لوهران، وأكّد طلبية بحبتين.",
    "sentiment": "positive",
    "outcome": "lead",
    "transcript": [
      { "role": "agent", "text": "السلام عليكم، معاك متجر لمسة!", "at": "2026-10-04T11:58:13.000Z" },
      { "role": "user", "text": "شحال سعر كريم الصبار؟", "at": "2026-10-04T11:58:18.000Z" },
      { "role": "tool", "tool": "confirm_order", "text": "{\"orderId\":1042,\"status\":\"confirmed\"}", "at": "2026-10-04T11:59:40.000Z" },
      { "role": "agent", "text": "تم تأكيد الطلبية، شكرا بزاف!", "at": "2026-10-04T11:59:45.000Z" }
    ]
  }
}
FieldMeaning
summaryOne to three sentences on what the customer wanted and what happened: in Arabic for Arabic agents, otherwise in the agent's language
sentimentThe customer's mood: positive, neutral or negative
outcomeresolved (they got what they needed), lead (an order, booking or contact details captured), follow_up (your team must get back to them), unresolved, or no_answer (no real conversation)
endReasonCalls: agent_ended, caller_ended, caller_hangup, max_duration, silence, connection_lost, and for WhatsApp calls that couldn't connect accept_failed or media_failed. WhatsApp threads: idle (closed when the customer wrote again after 24 hours)
transcriptEvery turn: user, agent, and tool entries with the tool's name and a short form of its answer

If writing the summary fails, summary, sentiment and outcome are empty strings, and the event is still sent.

​message.received

A WhatsApp message or voice note, before the agent answers it:

{
  "id": "a5e0d7c2-31f4-4f0b-8f1c-7d9e2b6a4c10",
  "event": "message.received",
  "createdAt": "2026-10-04T12:05:31.000Z",
  "data": {
    "agentId": "6ac2aab4f3fdade650f3b013",
    "conversationId": "6ac2aab5f3fdade650f3b131",
    "channel": "whatsapp_chat",
    "contact": { "name": "Karim", "phone": "+213555123456", "waId": "213555123456" },
    "text": "شحال التوصيل لوهران؟",
    "voice": true
  }
}

voice is true for a voice note, and text is then its transcript.

​tool.called

One of your tools ran during a call. The built-in knowledge search and current time aren't reported:

{
  "id": "e8d1b2a3-9c4f-4e5d-8a7b-6c5d4e3f2a1b",
  "event": "tool.called",
  "createdAt": "2026-10-04T11:59:40.000Z",
  "data": {
    "agentId": "6ac2aab4f3fdade650f3b013",
    "conversationId": "6ac2aab5f3fdade650f3b120",
    "tool": "confirm_order",
    "args": { "wilaya": "Oran", "quantity": 2 },
    "ok": true,
    "status": 200
  }
}

ok is true when your endpoint answered 2xx, and status is its HTTP status (0 if it couldn't be reached).

Was this page helpful?