API basics
Webhook events
Every event a webhook subscription can receive, when it fires, and the fields in its payload.
This page is the catalogue. Receive webhooks covers subscribing, verifying the X-Webhook-Signature header, retries and the delivery log.
The envelope
Every delivery is one JSON object. Call events and chat events share the same envelope; a call event names the call, a chat event names the conversation.
{
"schema_version": "1.0",
"event": "call.ended",
"event_id": "evt_5c0e8a1f2b3d4e6f8a9b0c1d2e3f4a5b",
"idempotency_key": "call_7f2e...:call.ended:evt_5c0e...",
"call_id": "call_7f2e9a0b1c2d4e5f8a6b7c8d9e0f1a2b",
"timestamp": "2026-10-28T09:42:11.204Z",
"identity": {
"org_id": "org_...",
"project_id": "proj_...",
"assistant_id": "asst_...",
"assistant_version": 4
},
"metadata": { "crm_id": "0061x00000AbCdE" },
"data": { "call_type": "outbound", "ended_reason": "call.ending.agent-ended-call", "duration_ms": 182340 }
}{
"schema_version": "1.0",
"event": "lead.captured",
"event_id": "evt_9d1f...",
"idempotency_key": "conv_0c8e...:lead.captured:evt_9d1f...",
"conversation_id": "conv_0c8e5f2a9b7d4c1e86f3a0d2b4c6e8f1",
"timestamp": "2026-10-28T09:31:04.517Z",
"identity": { "org_id": "org_...", "project_id": "proj_...", "assistant_id": "asst_..." },
"data": { "...": "per event, below" }
}| Field | Meaning |
|---|---|
event | The event type. |
event_id | Unique per event and the same on every redelivery of it. Store it and skip events you have already handled. |
idempotency_key | <call_id or conversation_id>:<event>:<event_id>, if you prefer one string to dedupe on. |
timestamp | When the event happened, ISO 8601 in UTC. Deliveries can arrive out of order; sort by this, not by arrival. |
identity | The organization, project and assistant it belongs to. Call events add assistant_version. |
metadata | Call events only: the metadata you passed when you started the call, echoed back. |
livemode | false on events from test chats and test calls, which reach only test subscriptions. Live events have no livemode field. |
data | The event's own fields. |
Call events can also carry room_name and server_region; treat both as diagnostics.
Call events
| Event | Fires when |
|---|---|
call.started | The assistant has joined the call and loaded its configuration. |
call.connected | The other person is on the line. For an outbound call, after they answer. |
call.ended | The call is over, with its transcript, usage and recordings. |
call.started and call.connected carry caller_number and called_number when known.
call.ended carries:
| Field | Meaning |
|---|---|
call_type | inbound, outbound or web. |
ended_reason | Why it ended, as a phase-prefixed string such as call.ending.agent-ended-call, call.in-progress.customer-ended-call or call.start.customer-did-not-answer. New values appear over time. |
caller_number, called_number | The two ends of the call, when known. |
started_at, ended_at, duration_ms | Timing. duration_ms runs from connection to the end. |
messages | The transcript: role (user, assistant, tool), content, and for tool turns tool_calls and tool_call_id. Spoken turns carry seconds_from_start and duration_ms. |
usage | Billable usage rows: type plus token counts (input_tokens, output_tokens) or audio_duration seconds. |
recordings, recording_url | Recording references, when the call was recorded. Fetch playback links through GET/api/calls/{id}/recordings. |
performance | Latency figures. Treat the fields as diagnostics. |
For the full call, including the analysis that runs after it ends, read GET/api/calls/{id}.
Chat events
Chat events cover web chat and WhatsApp conversations.
| Event | Fires when | data |
|---|---|---|
conversation.started | A conversation is created. The website widget creates one with the visitor's first message; a WhatsApp conversation starts with the first inbound message. | visitor_id, parent_origin (the site it started on), widget_key_id. Each can be null. |
conversation.message.created | A message appears in the transcript: from the visitor, the assistant or a person on your team. | message (id, role of user, assistant or agent, content, seq, created_at, and agent and attachments when present; a turn spoken in a voice session adds channel voice and voice_session_id) and conversation (status, handoff_requested_at, taken_over). |
lead.captured | The assistant saved a lead, a later analysis created or refreshed it, the visitor sent files after it existed, or reading the chat after a voice session created or changed it. | lead (id, name, email, phone, fields, and attachments when the visitor sent files), conversation_id, source (tool, analysis, attachments or voice), summary, and attribution when known: where the visitor came from (landing page, referrer, UTM tags), or only chat_page once they declined analytics. Every delivery is the whole lead as it is now: update in place. |
conversation.handoff_requested | The assistant asked for a person to take over. | reason, the last 20 transcript entries, visitor_id, lead_id. |
conversation.callback_requested | The visitor asked to be called back. | phone, and when given slot_start (the time they prefer), note, visitor_id, lead_id. |
conversation.takeover.started | A person took the conversation over. | agent (id, and email and name when known), source (console or api). |
conversation.takeover.released | The person handed it back, someone released their seat, the conversation ended, or their seat lapsed. | agent, reason (released, forced, conversation_ended or abandoned), and previous_holder when forced. |
conversation.analyzed | A background analysis of the conversation finished. | summary, structured, lead_id, through_seq, conversation_status, human_review, needs_review. |
conversation.ended | The conversation closed. | See below. |
conversation.analyzed can arrive more than once for one conversation, and out of order. Keep the one with the highest through_seq (the last message it read).
conversation.ended carries the outcome in one place:
| Field | Meaning |
|---|---|
status, ended_reason, message_count | How and why it closed, and its length. |
lead_id | The lead, if one was captured. |
handoff_status | none, pending, answered, missed (it ended before anyone took over) or abandoned (someone took over and left the visitor waiting). |
handoff_requested_at, handoff_answered_at, first_taken_over_at | Handoff timing. |
human_outcome, human_metrics | When a person took part: handled or abandoned, and their reply times. |
transcript | The whole conversation. Replies from a person read as assistant turns prefixed with [human agent] . |
usage | Model usage for the conversation. |
analysis | The latest analysis when it ended (summary, structured, through_seq, human_review), or null. A newer one can follow as conversation.analyzed. |
Attachments in any chat event are { id, name, content_type, size, url }. The url works without a key for up to seven days (less when the chat came from a test key that is revoked or a preview link that is turned off), so copy the file if you need it longer.
Choosing events
A subscription with an empty or missing events list receives every event type in its mode, including ones added later. A subscription's mode is set when you create it: live (the default) receives live traffic, test only test chats and calls. Preview chats send no webhooks. List names to receive only those. A name that is not an event above is refused with 400 invalid_request, so a typo cannot leave a subscription silent.