Events, 2026-10-01 format
This is the 2026-10-01 event format, frozen. Endpoints created before
release v1.84.0.0 keep receiving it, as does any endpoint created or changed
with "api_version": "2026-10-01", and so does the
event stream. A new endpoint gets
v2, whose fields are in
Events. How to receive either:
Receive webhooks.
Event types
Section titled “Event types”| type | when | data |
|---|---|---|
message.received | someone texts or sends a WhatsApp message to one of your numbers (after STOP/START handling, on both channels) | message_id, conversation_id, contact_id, channel_id, from, to, body, media[], media_failed, received_at, campaign_id, keyword, channel, quick_reply. channel is sms or whatsapp, absent on events written before WhatsApp messages were announced (absent means sms); quick_reply is { payload } for a WhatsApp button tap, else null, absent on those older events |
message.sent | a text left for the carrier: your AI agent’s reply, a teammate’s reply or an API send | message_id, conversation_id, author (ai_agent / human / api), agent_id, to, from, channel_id, body, send_ids[], sent_at (from is the number it went out from, channel_id its channel) |
message.status_updated | a text’s or a WhatsApp message’s status changed. Campaign messages’ events reach only endpoints that name this type; "*" leaves them out | SMS: channel ("sms"), status, send_id, campaign_id, message_id, to, from, state, display_status, segments, billed_credits, error, sent_at, delivered_at, updated_at. WhatsApp: channel ("whatsapp"), status, message_id, conversation_id, campaign_id, to, from, error, template, updated_at |
campaign.started | a campaign was started; always the first event of its run (it may still wait for its sending hours) | campaign_id, campaign_short_id, name, campaign_type, status, started_at |
campaign.paused | a campaign stopped sending, for any reason | the same five fields, plus reason, message, paused_at, resumes_at |
campaign.resumed | a paused campaign is sending again | the same five, plus resumed_at, previous_reason |
campaign.completed | a campaign finished | the same five, plus completed_at, totals, billed_credits |
campaign.canceled | a campaign was canceled | the same five, plus canceled_at, in_flight, not_yet_sent |
campaign.waiting | an SMS campaign reached the end of its sending hours, or started outside them, and waits for them (not a pause) | the same five, plus reason, waiting_at, resumes_at, tz |
campaign.window_opened | an SMS campaign’s sending hours started again and it goes on sending | the same five, plus opened_at, tz |
call.ringing | an outbound call started ringing. A campaign’s call events reach only endpoints that name the type; "*" leaves them out | call_id, conversation_id, direction, from, to, agent_id, campaign_id, status, occurred_at |
call.answered | a call was answered (by a person or a machine) | the same nine, plus answered_at |
call.completed | a call ended; status is completed, voicemail, no_answer or busy | the same nine, plus answered_at, outcome, duration_sec, ended_at, disconnect_reason |
call.failed | a call failed (a system or carrier failure; status is failed) | the same as call.completed |
Envelope
Section titled “Envelope”Every delivery’s body is an envelope with the payload under data:
{ "id": "evt_9c1f…", "type": "message.status_updated", "workspace_id": "00000000-0000-4000-8000-00000000000f", "created_at": "2026-10-15T17:02:19.000Z", "seq": 48213, "data": { "channel": "whatsapp", "status": "read", "message_id": "00000000-0000-4000-8000-000000000013", "conversation_id": "00000000-0000-4000-8000-000000000014", "campaign_id": null, "to": "+6591234567", "from": "+16285550100", "error": null, "template": { "id": "wt-0000000001", "name": "order_ready", "language": "en_US" }, "updated_at": "2026-10-15T17:02:19.000Z" } }seq is the event’s position in your workspace’s log, the same number as
the id: of its /v1/events frame.
What changes in v2
Section titled “What changes in v2”| event | 2026-10-01 | v2 |
|---|---|---|
| every event | { id, type, workspace_id, created_at, seq, data } | adds object, api_version, livemode |
message.received | channel_id (UUID); channel and quick_reply missing on old events | channel always (sms or whatsapp), quick_reply always (null unless a tap), channel_id (ch-…), channel_uuid, sender_id, campaign_short_id. message_id is still the thread message’s id: GET /v1/messages/{id} does not read inbound texts |
message.sent | message_id (thread message), send_ids[], agent_id (UUID), channel_id (UUID) | message_id = the first part’s /v1 message id, message_ids[] = every part in order, channel: "sms", agent_id (ag-…) + agent_uuid, channel_id (ch-…) + channel_uuid, sender_id, metadata. send_ids and the thread id are gone |
message.status_updated | SMS: send_id, thread message_id, state, display_status, error.meaning; channel/status missing on old events. WhatsApp: no segments, times | one shape for both channels: message_id (an SMS part’s id, or the WhatsApp message’s), channel always, status, from, to, conversation_id, campaign_id + campaign_short_id, sender_id, segments, billed_credits, error { code, category, message, action }, template, sent_at, delivered_at, read_at, updated_at, metadata |
campaign.* | campaign_type, status: "cancelled" | channel (sms, whatsapp, voice), status: "canceled", metadata |
campaign.completed | totals { recipients, sent, delivered, not_delivered, failed, unconfirmed, skipped, opted_out, replied } | counts { recipients, pending, sent, delivered, failed, skipped, replied, opted_out }, the same buckets and nulls as GET /v1/campaigns/{id} (replied and opted_out are null for voice, opted_out for WhatsApp; counted when the campaign completed: a text still being handed to the carrier then counts as failed here, and GET shows its later state) |
call.* | call_id, agent_id (ag-…) | adds call_short_id (cv-…), channel: "voice", agent_uuid, campaign_short_id, metadata |
conversation.created, turn.completed, confirmation.*, spend.recorded, run.completed, meta_*, webhook.test | data unchanged; only the envelope |
Event words and /v1 words
Section titled “Event words and /v1 words”The 2026-10-01 payloads keep the words they were first published with, so
code that reads them keeps working. Where they differ from the /v1
objects, map them like this:
| in an event | on /v1 | how |
|---|---|---|
status: "cancelled" (campaign events) | status: "canceled" | the same state, spelled two ways |
campaign_type: "sms" | channel: "sms" | |
campaign_type: "whatsapp_template" | channel: "whatsapp" | |
campaign_type: "voice_ai" (or voicemail_drop) | channel: "voice" | |
campaign_id (UUID) | a campaign’s id | the same UUID; campaign_short_id is its short_id (ca-…) |
agent_id (UUID, on message.sent; call events already carry the ag-…) | an agent’s id (ag-…) | GET /v1/agents lists each agent’s uuid |
channel_id (UUID) | a sender | GET /v1/senders lists each number’s channel_uuids |
send_id (SMS), message_id (WhatsApp) | a message’s id | the same id; read it with GET /v1/messages/{id} |
conversation_id | conversation_id | the same UUID |
display_status (SMS status events) | a message’s or a campaign recipient’s status | not_yet_sent → queued, sending → sending, sent_awaiting → sent, delivered → delivered, not_delivered → undelivered, failed → failed, unconfirmed → unconfirmed, skipped → skipped. Events also carry status, already in /v1 words |
A campaign recipient’s status is the word its message or call has on
/v1 (see Recipients), so an event’s
status and a recipient row agree.
What only this format carries
Section titled “What only this format carries”message.sent’ssend_idslists every send that carried the message, parts that were skipped or unconfirmed included: read each send’s status for the truth.- SMS status events carry the engine’s
stateanddisplay_status, anderroris{ code, category, meaning, action }. Events written beforechannelandstatuswere added do not have them: treat a missingchannelassms. - A cancel is spelled
cancelledinstatus, andcampaign.completedcarriestotalsrather thancounts.