BrightshoalDocsOpen console
MenuWebhook events

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.

Call event
{
  "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 }
}
Chat event
{
  "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" }
}
FieldMeaning
eventThe event type.
event_idUnique 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.
timestampWhen the event happened, ISO 8601 in UTC. Deliveries can arrive out of order; sort by this, not by arrival.
identityThe organization, project and assistant it belongs to. Call events add assistant_version.
metadataCall events only: the metadata you passed when you started the call, echoed back.
livemodefalse on events from test chats and test calls, which reach only test subscriptions. Live events have no livemode field.
dataThe event's own fields.

Call events can also carry room_name and server_region; treat both as diagnostics.

Call events

EventFires when
call.startedThe assistant has joined the call and loaded its configuration.
call.connectedThe other person is on the line. For an outbound call, after they answer.
call.endedThe 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:

FieldMeaning
call_typeinbound, outbound or web.
ended_reasonWhy 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_numberThe two ends of the call, when known.
started_at, ended_at, duration_msTiming. duration_ms runs from connection to the end.
messagesThe 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.
usageBillable usage rows: type plus token counts (input_tokens, output_tokens) or audio_duration seconds.
recordings, recording_urlRecording references, when the call was recorded. Fetch playback links through GET/api/calls/{id}/recordings.
performanceLatency 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.

EventFires whendata
conversation.startedA 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.createdA 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.capturedThe 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_requestedThe assistant asked for a person to take over.reason, the last 20 transcript entries, visitor_id, lead_id.
conversation.callback_requestedThe visitor asked to be called back.phone, and when given slot_start (the time they prefer), note, visitor_id, lead_id.
conversation.takeover.startedA person took the conversation over.agent (id, and email and name when known), source (console or api).
conversation.takeover.releasedThe 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.analyzedA background analysis of the conversation finished.summary, structured, lead_id, through_seq, conversation_status, human_review, needs_review.
conversation.endedThe 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:

FieldMeaning
status, ended_reason, message_countHow and why it closed, and its length.
lead_idThe lead, if one was captured.
handoff_statusnone, 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_atHandoff timing.
human_outcome, human_metricsWhen a person took part: handled or abandoned, and their reply times.
transcriptThe whole conversation. Replies from a person read as assistant turns prefixed with [human agent] .
usageModel usage for the conversation.
analysisThe 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.

Stuck on webhook events?