# Events, 2026-10-01 format

> The legacy event format, kept for webhook endpoints that have not moved to v2 and for the event stream: every type's fields, the envelope, and how its words map to the /v1 API.

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](/platform-api/events/#the-event-stream). A new endpoint gets
[v2](/platform-api/events/#payload-versions), whose fields are in
[Events](/platform-api/reference/events/). How to receive either:
[Receive webhooks](/platform-api/events/).

## 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

Every delivery's body is an envelope with the payload under `data`:

```json
{ "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

| 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

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](/platform-api/campaigns/#recipients)), so an event's
`status` and a recipient row agree.

## What only this format carries

- `message.sent`'s `send_ids` lists 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 `state` and `display_status`, and
  `error` is `{ code, category, meaning, action }`. Events written before
  `channel` and `status` were added do not have them: treat a missing
  `channel` as `sms`.
- A cancel is spelled `cancelled` in `status`, and `campaign.completed`
  carries `totals` rather than `counts`.