# Events

> The event stream (GET /v1/events) and, when the OpenAPI file lists them, every event type's fields.

<!-- Generated by apps/docs/scripts/gen-reference.mjs from apps/docs/public/openapi/v1.json. Do not edit: change the API's schemas (then `pnpm --filter @seasalt/api openapi:emit`) or the notes in apps/docs/src/reference-notes/, then run `pnpm --filter @seasalt/docs gen:reference`. -->

What arrives at a webhook endpoint or on the event stream. How-to: [Receive webhooks](/platform-api/events/).

Generated from [`/openapi/v1.json`](/openapi/v1.json); interactive version: [API reference](/openapi/v1/).

## Endpoints

| method | path | what it does | success |
|---|---|---|---|
| [`GET`](#get-v1events) | `/v1/events` | Stream or poll workspace events | `200` |

<!-- notes: src/reference-notes/events.md -->

## Rules

What each event promises, in the v2 format. Receiving, verifying and
replaying events: [Receive webhooks](/platform-api/events/). The legacy
format: [Events, 2026-10-01 format](/platform-api/reference/events-2026-10-01/).

### Every event

- **Written once, delivered at least once.** Each change is logged as one
  event, with one id. A delivery can repeat (a retry after a timeout, or our
  side retrying after a lost answer), so drop repeats by the envelope `id`,
  which is also the `webhook-id` header.
- Deliveries run concurrently and retries come later, so events can arrive
  out of order. Order by `seq` (higher is newer): an event whose `seq` is
  lower than one you already applied for the same object is older news. The
  `/v1/events` stream is always in `seq` order.
- `metadata` on `message.sent`, `message.status_updated`, `call.*` and
  `campaign.*` is what you passed when you sent the message, placed the call
  or created the campaign (`null` when there was none). It, the ids beside
  each `<thing>_id`, and `conversation_id` on an SMS status are looked up
  when the delivery is first sent; one is `null` when its row is gone.
- New event types and new fields may be added: answer 2xx to a `type` you
  do not handle, and ignore fields you do not know.
- `POST /v1/webhooks/{id}/test` sends its `webhook.test` sample in the
  endpoint's format (a v2 sample has `"seq": 0` and an id starting
  `evt_test_`). It is never stored, retried or counted as a failure.
- The `/v1/events` stream never skips a frame: during a long database job on
  our side it pauses rather than jumping ahead, and goes on when the job
  ends. Use webhooks when you need real-time delivery.
- `campaign.resumed`'s `previous_reason` is the reason of the pause it
  ended, or `null` when that is not known.

### Messages

- `message.received` covers SMS and WhatsApp: branch on `channel` (`sms` or
  `whatsapp`). `message.sent` covers SMS only: AI-agent, human and API
  sends, and `body` is the text actually sent (after formatting), not the
  draft. Automated rating requests and campaign texts
  are not announced with `message.sent`; an API send with
  `conversation: "none"` has `conversation_id` null.
- **A split reply** that went out as two texts is one `message.sent`:
  `message_id` is the first part, `message_ids` every part in order, and each
  part gets its own `message.status_updated`, keyed by its own `message_id`.
  `message_id: null` with `message_ids: []` means no text reached the carrier
  queue. A multi-part reply is announced once, when every part is handed
  over.
- A reply the carrier never confirmed has no `message.sent`; its status
  events say so. One confirmed only later gets its `message.sent` within
  about 10 minutes of that.
- For an AI agent's or a teammate's reply, the status events can come
  **before** its `message.sent` (even on the ordered stream). For an API
  send, `message.sent` always comes first.
- `message.received`'s `keyword` (`opt_out`, `opt_in`, `help`) says the
  platform already applied it; `campaign_id` names the campaign a reply was
  attributed to. Its `message_id` is the conversation message's id:
  `GET /v1/messages/{id}` does not read inbound texts.
- A WhatsApp `message.received` is a message the person sent: text, a
  button tap, a list or button reply, a location, or a file with or without
  a caption. `body` is their own words (a tap's button label; empty for a
  file with no caption), and a tap's `quick_reply.payload` is the button's
  payload. A WhatsApp STOP or START the platform applied has `keyword`
  `opt_out` or `opt_in`, as on SMS (WhatsApp has no HELP keyword). A
  reaction, a message only the WhatsApp Business App can show, your own
  replies from the Business App and chat history imported from it raise no
  `message.received`.
- `message.status_updated` carries the same `status` word
  `GET /v1/messages/{id}` answers. Branch on `channel` first.
- An SMS status event is silent while a text is `sending`: the first one is
  usually `sent`, then `delivered`, `undelivered`, `failed` or
  `unconfirmed`, and a send can go straight to `delivered`, `failed` or
  `skipped`. Early events can show `segments: null`.
- WhatsApp status events come as Meta reports each step: `sent`,
  `delivered`, `read`, `failed`; a repeated status makes no second event. A
  `read` event also has the `sent_at` and `delivered_at` Meta reported.
  SMS `read_at` is always `null`.
- Campaign messages' status events reach only endpoints and streams that
  name `message.status_updated`: `"*"` leaves them out.
- For SMS, an event lost to a crash between storing a text (or its status)
  and logging the event is logged later by a sweep, within about 10 minutes,
  with the same content. A queued inbound text can have its events up to 30
  seconds late. WhatsApp events have no such repair: `GET /v1/messages/{id}`
  always has a status, and `GET /v1/conversations/{id}/messages` every
  message the person sent.

### Inbound pictures (MMS)

A picture or video someone texts you is downloaded and stored in your
workspace; the provider's own URL is never passed on. Each file is in
`message.received`'s `media` as `{ "url", "content_type", "size" }`, where
`url` is `https://api.omniocto.com/v1/media/{id}`: call it with your API key
for a signed download link valid for 1 hour. A file we could not fetch is
not listed but counted in `media_failed`, so a picture-only MMS that failed
arrives as `media: []` with `media_failed: 1`. Vonage numbers do not pass
media on: the message arrives with a placeholder line in `body`.

### Calls

- A call gets **exactly one** terminal event, `call.completed` (answered,
  voicemail, no answer or busy) or `call.failed` (a system or carrier
  failure). After it, the call's `status` and `outcome` never change, and
  `outcome` is never `null`.
- `call.ringing` and `call.answered` come before it, once each, when the
  call was seen to ring and to be answered. An inbound call has no
  `call.ringing`; a call nobody answered has no `call.answered`.
  `call.ringing` is sent at most once and not written again if lost.
- A call that stops reporting is closed as `call.failed` with outcome
  `unknown` and `disconnect_reason` `settle_sweep_idle`, normally within 20
  minutes, up to 2 hours when the liveness signal is unavailable. An inbound
  call is not classified: its outcome is `unknown`.
- In `seq`, a call's events come in that order (an event written again
  after a loss can come later); deliveries can still arrive out of order.
- `answered_at`, `disconnect_reason` and `occurred_at` are in events only.
  A campaign's calls reach only endpoints and streams that name the call
  types. Calls placed from the dashboard's Try Voice have no events.

### Campaigns

- Every channel announces `campaign.started`, `campaign.paused`,
  `campaign.resumed`, `campaign.completed` and `campaign.canceled`;
  `campaign.waiting` and `campaign.window_opened` are SMS only. An event
  fires whoever made the change: your API call, a teammate, Octo, or the
  platform (a pause when credits run out).
- `status` is the campaign's status right after the change.
  `campaign.started` comes first, `queued` or `running`; the move to
  `running` has no event. A run whose audience turns out empty sends
  `campaign.started`, then `campaign.completed`. Treat `campaign.completed`
  and `campaign.canceled` as final.
- **Treat the events as a prompt, not a ledger.** In rare races a change is
  announced twice, and very rarely one is missing. Act on the `status` in
  each payload, and read `GET /v1/campaigns/{id}`, which is always the truth.
- For an SMS campaign, an event that could not be written when the campaign
  changed is written later, once, usually 10 to 15 minutes after. Its `*_at`
  is the time it was written, and a late `campaign.resumed` has
  `previous_reason: null`.
- For a WhatsApp or voice campaign, `campaign.completed` and
  `campaign.canceled` are written exactly once: one event id per campaign,
  never a second one. One that could not be written when the campaign ended
  is written later, usually 5 to 10 minutes after, with its `*_at` the time
  it was written. Its other events (`started`, `paused`, `resumed`) have no
  such repair. To wait for a campaign to finish, poll
  `GET /v1/events?wait=25&campaign=<id>&types=campaign.completed,campaign.canceled,campaign.paused`.
- `campaign.paused`'s `reason` is `operator` (someone paused it, your own
  `POST /v1/campaigns/{id}/pause` included), `spend_cap`, `out_of_credits`,
  `daily_cap` (the carrier's daily limit; `resumes_at` says when it goes on,
  after midnight Pacific time), `circuit_breaker` (too many recent texts
  failed), `campaign_blocking_error` (the carrier is refusing the number),
  `trial_check_unavailable`, `trial_campaign_needs_card`, `needs_review` or
  `engine_error`. `message` says the same in one sentence, ready to show.
  `resumes_at` is `null` for every other reason.
- `campaign.completed`'s `counts` are counted when it completed: a text still
  being handed to the carrier then counts as `failed`, and
  `GET /v1/campaigns/{id}` shows its later state. `billed_credits` is the SMS
  texts' cost, `null` for WhatsApp and voice.
- `campaign.canceled`: `in_flight` counts messages already handed over (they
  may still arrive), `not_yet_sent` the recipients nothing was sent to.
  Discarding a draft that never started sends no event.
- `campaign.waiting` is not a pause: `status` stays `queued` or `running` and
  the campaign goes on by itself. `resumes_at` is when the hours next start
  (`null` when none fall in the next 8 days). `campaign.window_opened`
  follows once per wait, unless a pause or cancel ended it. Sending hours are
  checked once a minute, so both can be up to a minute late.

### Poll and Monitor

`GET /v1/events?wait=<0..25>` (poll mode) answers one `EventPage`. A loop:
call once without `after` to get a `next_cursor` from now (do it before you
start the campaign you will wait for: an event still being written at that
instant counts as before now), then call with
`after=<next_cursor>` each time, whether or not events came; `has_more: true`
means call again at once. At most 5 polls per key (20 per workspace) may wait
at once (`429 too_many_waits`); `wait=0` never counts, and a `wait=25` loop
makes about 2 requests a minute of the route's 60. From an MCP client the
`wait_for_events` tool is this call ([API tools](/mcp/api-tools/)).

In Claude Code, Monitor can watch the stream in the background and report
each event line; take `$CURSOR` from one poll first, or the stream replays
7 days:

```bash
curl -sN "https://api.omniocto.com/v1/events?api_version=v2&after=$CURSOR&campaign=ca-0000000002&types=campaign.completed,campaign.paused" \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" | grep --line-buffered '^event: '
```

<!-- /notes -->

## `GET /v1/events`

Stream or poll workspace events.

**Poll mode** (`?wait=0..25`): one JSON page (`EventPage`, the `/v1` list shape) of the events after `after`, answered as soon as there is one or after `wait` seconds with `data: []`. Without `after` (or `Last-Event-ID`) it starts from now: the first call returns `next_cursor` and only events written after it. Pass `next_cursor` back as `after`, whether or not events came; `has_more: true` means call again at once. At most 5 waiting polls per key and 20 per workspace (`429 too_many_waits`); `wait=0` never counts. **Stream mode** (no `wait`): a long-lived `text/event-stream` of workspace events. First **replays** durable events after the resume cursor (`?after=<seq>` or the `Last-Event-ID` header), then **tails** live events. Each SSE frame's `id:` is a monotonic cursor and `event:` is the event `type`; the `data:` payload is the event envelope. About every 15 s an `event: ping` frame arrives with empty `data` and no `id:`: skip it. Two other frames are not envelopes and have no `id:` either: `replay_truncated` (a cursor older than the log) and `error` (the key's owner lost the owner or admin role; `data` is the `/v1` error body with `code: insufficient_role`, and the stream then ends). With `?api_version=v2`, an event that could not be rendered as v2 is an `event: render_error` frame with the event's seq as `id:` and `data` `{ event_id, type, seq }` plus the event's `message_id`, `send_id`, `call_id`, `campaign_id` and `conversation_id` when it has them: read that resource, or replay the event without `api_version` (`?after=<seq-1>&types=<type>`). `?after=` wins over `Last-Event-ID` when both are sent. `?campaign=` (both modes) keeps only the events whose `data.campaign_id` is that campaign; without `types`, a campaign's per-recipient `message.status_updated` and `call.*` events are still left out, as with `"*"`. Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role` (the feed carries every event of the workspace, message text included).

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `after` | query | integer<br>≥ 0 | Resume cursor: stream events with a `seq` greater than this (a frame's `id:`). A whole number of 0 or more; anything else is `400 invalid_cursor`. |
| `types` | query | string | Comma-separated event types to filter to (e.g. `turn.completed,run.completed`). An unknown type returns 400. |
| `campaign` | query | string | A campaign's `ca-…` id or UUID: only events whose payload names it (`data.campaign_id`). Without `types`, its texts' status events and its calls' events are left out: name `message.status_updated`, `call.completed` or `call.failed` in `types` to get them. No such campaign in this workspace: `404 not_found`. |
| `wait` | query | integer<br>0–25 | Poll mode: answer one JSON page instead of a stream, waiting up to this many seconds (0 to 25) for the first event. `0` answers at once. |
| `limit` | query | integer<br>1–200; default `100` | Poll mode only: the most events one page carries (1 to 200, default 100). Without `wait` it is `400 invalid_request`. |
| `api_version` | query | string<br>`2026-10-01`, `v2`; default `2026-10-01` | The payload version of the frames: `2026-10-01` (`WorkspaceEvent`, the default) or `v2` (`WorkspaceEventV2`, the body a v2 webhook endpoint receives). Anything else returns 400. |
| `Last-Event-ID` | header | string | Standard SSE resume header — an alternative to `?after`. |

</div>

**Answers** `200` with [EventPage](#eventpage); `200` (`text/event-stream`) with [WorkspaceEvent](/platform-api/reference/events-2026-10-01/) or [WorkspaceEventV2](#workspaceeventv2). Errors use the [error envelope](/platform-api/reference/#v1error).

## Objects

### EventPage

One page of the event log (poll mode). `data` holds the events after the cursor, oldest first, each the same envelope a stream frame carries.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of map of string to any, required | Events in `seq` order, each the envelope a stream frame carries: `WorkspaceEvent` (2026-10-01, the default) or, with `?api_version=v2`, `WorkspaceEventV2`. The `Octo-Event-Version` header names which. |
| `has_more` | boolean, required | `true` when the page stopped at `limit` with more events ready: call again at once with `next_cursor`. |
| `next_cursor` | string, required | A `seq` (as a string). Pass it as `after` on the next call, whether or not this page had events: it also moves past events your filters skipped. Always a safe resume point, for a poll or a stream. |
| `replay_truncated` | object or null, required | `null`, or `{ after, oldest_retained_seq, retention_days }` when `after` is older than the 7-day log: the events between them are gone. |
| `replay_truncated.after` | integer |   |
| `replay_truncated.oldest_retained_seq` | integer |   |
| `replay_truncated.retention_days` | integer |   |
| `render_errors` | array of map of string to any, required | `api_version=v2` only: events that could not be rendered as v2, each `{ event_id, type, seq }` plus the ids it had (`message_id`, `send_id`, `call_id`, `campaign_id`, `conversation_id`). Read the resource, or poll `?after=<seq-1>&types=<type>` without `api_version` for the 2026-10-01 body. Empty otherwise. |

</div>

Example: see [Receive webhooks](/platform-api/events/).

<a id="workspaceeventv2"></a>

### Envelope (every event)

These are the **v2** fields, what an endpoint on `api_version: "v2"` receives. The [event stream](/platform-api/events/#the-event-stream) sends them with `?api_version=v2`; by default it, and endpoints on `2026-10-01`, send the [2026-10-01 format](/platform-api/reference/events-2026-10-01/). Every event carries these fields; `type` names the event and `data` differs per type, below.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required |   |
| `object` | string, required<br>always `event` |   |
| `api_version` | string, required<br>always `v2` |   |
| `livemode` | boolean, required |   |
| `workspace_id` | string, required |   |
| `created_at` | string, required |   |
| `seq` | integer, required | Its exact limits differ by event type (a test event's is `0`). |
| `type` | string, required | The event type: one of the sections below. |

</div>

### conversation.created

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.conversation_id` | string, required |   |
| `data.channel` | string, required |   |
| `data.contact_id` | string |   |

</div>

### turn.completed

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.conversation_id` | string, required |   |
| `data.surface` | string, required |   |
| `data.reply_preview` | string, required |   |
| `data.finish_reason` | string, required |   |
| `data.model` | string, required |   |

</div>

### confirmation.requested

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.action_id` | string, required |   |
| `data.action_code` | string, required |   |
| `data.summary` | string, required |   |
| `data.conversation_id` | string |   |

</div>

### confirmation.resolved

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.action_id` | string, required |   |
| `data.action_code` | string, required |   |
| `data.resolution` | string, required<br>`approved`, `rejected`, `expired`, `superseded` |   |

</div>

### spend.recorded

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.amount_credits` | number, required |   |
| `data.category` | string, required |   |
| `data.conversation_id` | string |   |
| `data.balance_credits` | number |   |

</div>

### run.completed

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.run_id` | string, required |   |
| `data.workflow_id` | string, required |   |
| `data.status` | string, required |   |

</div>

### message.received

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.message_id` | string, required | The conversation message's id. `GET /v1/messages/{id}` does not read inbound texts. |
| `data.channel` | string, required<br>`sms`, `whatsapp` | The message's channel. |
| `data.conversation_id` | string, required | The conversation the message landed in. |
| `data.contact_id` | string or null, required | The sender's contact; null when none matched. |
| `data.from` | string, required | Who sent it: a phone number in E.164, on SMS or on WhatsApp. |
| `data.to` | string or null, required | Your number that received it, in E.164; null when it is not known. |
| `data.body` | string, required | The person's own words: the SMS text, or a WhatsApp message's text, caption or tapped button's label. Empty when they sent only a file. |
| `data.media` | array of objects, required |   |
| `data.media[].url` | string, required |   |
| `data.media[].content_type` | string, required |   |
| `data.media[].size` | integer or null, required |   |
| `data.media_failed` | integer, required<br>≥ 0 | How many attachments could not be stored. |
| `data.received_at` | string, required | When it arrived (ISO 8601). |
| `data.campaign_id` | string or null, required | The campaign a reply was attributed to: its UUID. |
| `data.campaign_short_id` | string or null, required | The same campaign's `ca-…` id. |
| `data.channel_id` | string or null, required | The channel's `ch-…` id. |
| `data.channel_uuid` | string, required | The same channel's UUID (`channel_uuids` in `GET /v1/senders`). |
| `data.sender_id` | string or null, required | The number's `GET /v1/senders` id: `pn-…`, or a WhatsApp-only number's `ch-…`. |
| `data.keyword` | string or null, required<br>`opt_out`, `opt_in`, `help` | A STOP, START or HELP keyword, already handled; null for any other text. On WhatsApp only `opt_out` or `opt_in` (there is no HELP keyword). |
| `data.quick_reply` | object or null, required | A WhatsApp button tap; null for anything typed, and always null on SMS. |
| `data.quick_reply.payload` | string, required | The tapped button's payload: a template quick-reply button's payload, or an interactive reply's id. |

</div>

### message.sent

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.message_id` | string or null, required | The `GET /v1/messages/{id}` id of the first (or only) part; null when no part reached the carrier queue. |
| `data.message_ids` | array of string, required | Every part of a split reply, in order; each gets its own `message.status_updated`. |
| `data.channel` | string, required<br>always `sms` | Always `sms`. |
| `data.conversation_id` | string or null, required | The conversation it was added to, if any. |
| `data.author` | string, required<br>`ai_agent`, `human`, `api` | Who wrote it: an AI agent, a person, or an API send. |
| `data.agent_id` | string or null, required | The agent's `ag-…` id; null unless an AI agent wrote it. |
| `data.agent_uuid` | string or null, required | The same agent's UUID. |
| `data.from` | string or null, required | The sending number, in E.164. |
| `data.to` | string, required | The recipient, in E.164. |
| `data.channel_id` | string or null, required | The sending channel's `ch-…` id. |
| `data.channel_uuid` | string or null, required | The same channel's UUID. |
| `data.sender_id` | string or null, required | The number's `GET /v1/senders` id. |
| `data.body` | string, required | The whole text, every part together. |
| `data.sent_at` | string, required | When it was handed to the carrier (ISO 8601). |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### message.status_updated

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.message_id` | string, required | The `GET /v1/messages/{id}` id: an SMS part's, or the WhatsApp message's. |
| `data.channel` | string, required<br>`sms`, `whatsapp` | The message's channel. |
| `data.status` | string, required<br>`queued`, `sending`, `sent`, `delivered`, `read`, `undelivered`, `failed`, `skipped`, `unconfirmed` | The new status, the `status` of `GET /v1/messages/{id}`. |
| `data.from` | string or null, required | The sending number, in E.164. |
| `data.to` | string, required | The recipient, in E.164. |
| `data.conversation_id` | string or null, required | The conversation the message is in, if any. |
| `data.campaign_id` | string or null, required | The sending campaign's UUID; null for a single send. |
| `data.campaign_short_id` | string or null, required | The same campaign's `ca-…` id. |
| `data.sender_id` | string or null, required | The number's `GET /v1/senders` id. |
| `data.segments` | integer or null, required | SMS only; null on WhatsApp, and on early SMS events. |
| `data.billed_credits` | number or null, required | SMS only; null on WhatsApp. |
| `data.error` | object or null, required | Why it failed, the `error` of `GET /v1/messages/{id}`; null otherwise. |
| `data.error.code` | string or null, required |   |
| `data.error.category` | string, required |   |
| `data.error.message` | string, required |   |
| `data.error.action` | string or null, required |   |
| `data.template` | object or null, required | WhatsApp only: the template sent; null on SMS. |
| `data.template.id` | string, required |   |
| `data.template.name` | string, required |   |
| `data.template.language` | string, required |   |
| `data.sent_at` | string or null, required | When it was sent, if it was. |
| `data.delivered_at` | string or null, required | When it was delivered, if it was. |
| `data.read_at` | string or null, required | WhatsApp: when it was read, if it was. |
| `data.updated_at` | string or null, required | When the status last changed. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### campaign.started

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.campaign_id` | string, required | The campaign's UUID, its `id` on `GET /v1/campaigns/{id}`. |
| `data.campaign_short_id` | string or null, required | The campaign's `ca-…` id. |
| `data.name` | string, required | The campaign's name. |
| `data.channel` | string, required<br>`sms`, `whatsapp`, `voice` | The campaign's channel. |
| `data.status` | string, required<br>`draft`, `queued`, `running`, `paused`, `completed`, `failed`, `canceled` | Its status after this change. |
| `data.started_at` | string, required | When it started. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### campaign.paused

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.campaign_id` | string, required | The campaign's UUID, its `id` on `GET /v1/campaigns/{id}`. |
| `data.campaign_short_id` | string or null, required | The campaign's `ca-…` id. |
| `data.name` | string, required | The campaign's name. |
| `data.channel` | string, required<br>`sms`, `whatsapp`, `voice` | The campaign's channel. |
| `data.status` | string, required<br>`draft`, `queued`, `running`, `paused`, `completed`, `failed`, `canceled` | Its status after this change. |
| `data.reason` | string, required<br>`operator`, `spend_cap`, `out_of_credits`, `daily_cap`, `circuit_breaker`, `campaign_blocking_error`, `trial_check_unavailable`, `trial_campaign_needs_card`, `needs_review`, `engine_error` | Why it paused; `operator` when a person paused it. |
| `data.message` | string, required | The reason in plain words, to show as it is. |
| `data.paused_at` | string, required | When it paused. |
| `data.resumes_at` | string or null, required | When it resumes by itself, if it will; null otherwise. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### campaign.resumed

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.campaign_id` | string, required | The campaign's UUID, its `id` on `GET /v1/campaigns/{id}`. |
| `data.campaign_short_id` | string or null, required | The campaign's `ca-…` id. |
| `data.name` | string, required | The campaign's name. |
| `data.channel` | string, required<br>`sms`, `whatsapp`, `voice` | The campaign's channel. |
| `data.status` | string, required<br>`draft`, `queued`, `running`, `paused`, `completed`, `failed`, `canceled` | Its status after this change. |
| `data.resumed_at` | string, required | When it resumed. |
| `data.previous_reason` | string or null, required<br>`operator`, `spend_cap`, `out_of_credits`, `daily_cap`, `circuit_breaker`, `campaign_blocking_error`, `trial_check_unavailable`, `trial_campaign_needs_card`, `needs_review`, `engine_error` | Why it had paused. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### campaign.completed

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.campaign_id` | string, required | The campaign's UUID, its `id` on `GET /v1/campaigns/{id}`. |
| `data.campaign_short_id` | string or null, required | The campaign's `ca-…` id. |
| `data.name` | string, required | The campaign's name. |
| `data.channel` | string, required<br>`sms`, `whatsapp`, `voice` | The campaign's channel. |
| `data.status` | string, required<br>`draft`, `queued`, `running`, `paused`, `completed`, `failed`, `canceled` | Its status after this change. |
| `data.completed_at` | string, required | When it completed. |
| `data.counts` | object, required | The final `counts`, as `GET /v1/campaigns/{id}` answers them. |
| `data.counts.recipients` | integer, required<br>≥ 0 |   |
| `data.counts.pending` | integer, required<br>≥ 0 |   |
| `data.counts.sent` | integer, required<br>≥ 0 |   |
| `data.counts.delivered` | integer, required<br>≥ 0 |   |
| `data.counts.failed` | integer, required<br>≥ 0 |   |
| `data.counts.skipped` | integer, required<br>≥ 0 |   |
| `data.counts.replied` | integer or null, required<br>≥ 0 |   |
| `data.counts.opted_out` | integer or null, required<br>≥ 0 |   |
| `data.billed_credits` | number or null, required | Credits the campaign cost in all; null when not known. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### campaign.canceled

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.campaign_id` | string, required | The campaign's UUID, its `id` on `GET /v1/campaigns/{id}`. |
| `data.campaign_short_id` | string or null, required | The campaign's `ca-…` id. |
| `data.name` | string, required | The campaign's name. |
| `data.channel` | string, required<br>`sms`, `whatsapp`, `voice` | The campaign's channel. |
| `data.status` | string, required<br>`draft`, `queued`, `running`, `paused`, `completed`, `failed`, `canceled` | Its status after this change. |
| `data.canceled_at` | string, required | When it was canceled. |
| `data.in_flight` | integer or null, required<br>≥ 0 | Sends or calls already under way when it was canceled. |
| `data.not_yet_sent` | integer or null, required<br>≥ 0 | Recipients that will not be reached. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### campaign.waiting

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.campaign_id` | string, required | The campaign's UUID, its `id` on `GET /v1/campaigns/{id}`. |
| `data.campaign_short_id` | string or null, required | The campaign's `ca-…` id. |
| `data.name` | string, required | The campaign's name. |
| `data.channel` | string, required<br>`sms`, `whatsapp`, `voice` | The campaign's channel. |
| `data.status` | string, required<br>`draft`, `queued`, `running`, `paused`, `completed`, `failed`, `canceled` | Its status after this change. |
| `data.reason` | string, required<br>always `send_window` | Outside its sending hours or days. |
| `data.waiting_at` | string, required | When it started waiting. |
| `data.resumes_at` | string or null, required | When its next window opens. |
| `data.tz` | string, required | The schedule's IANA time zone. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### campaign.window_opened

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.campaign_id` | string, required | The campaign's UUID, its `id` on `GET /v1/campaigns/{id}`. |
| `data.campaign_short_id` | string or null, required | The campaign's `ca-…` id. |
| `data.name` | string, required | The campaign's name. |
| `data.channel` | string, required<br>`sms`, `whatsapp`, `voice` | The campaign's channel. |
| `data.status` | string, required<br>`draft`, `queued`, `running`, `paused`, `completed`, `failed`, `canceled` | Its status after this change. |
| `data.opened_at` | string, required | When the window opened. |
| `data.tz` | string, required | The schedule's IANA time zone. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### meta_response.completed

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.response_id` | string, required |   |
| `data.conversation_id` | string, required |   |
| `data.status` | string, required<br>`completed`, `failed` |   |
| `data.error` | object or null, required |   |
| `data.error.type` | string, required |   |
| `data.error.code` | string or null, required |   |
| `data.error.message` | string, required |   |

</div>

### meta_chat.updated

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.conversation_id` | string, required |   |
| `data.surface` | string, required |   |
| `data.title` | string, required |   |
| `data.preview` | string, required |   |
| `data.message_count` | integer, required<br>≥ 0 |   |

</div>

### call.ringing

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.call_id` | string, required | The call's UUID, its `id` on `GET /v1/calls/{id}`. |
| `data.call_short_id` | string or null, required | The call's `cv-…` id. |
| `data.channel` | string, required<br>always `voice` | Always `voice`. |
| `data.conversation_id` | string, required | The call's conversation (its transcript). |
| `data.direction` | string or null, required<br>`inbound`, `outbound` | Who called whom. |
| `data.from` | string or null, required | The calling number, in E.164. |
| `data.to` | string or null, required | The called number, in E.164. |
| `data.agent_id` | string or null, required | The agent's `ag-…` id. |
| `data.agent_uuid` | string or null, required | The same agent's UUID. |
| `data.campaign_id` | string or null, required | The placing campaign's UUID; null for a single call. |
| `data.campaign_short_id` | string or null, required | The same campaign's `ca-…` id. |
| `data.status` | string, required<br>`queued`, `ringing`, `in_progress`, `completed`, `voicemail`, `no_answer`, `busy`, `failed` | The call's `status` after this event. |
| `data.occurred_at` | string, required | When this happened (ISO 8601). |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### call.answered

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.call_id` | string, required | The call's UUID, its `id` on `GET /v1/calls/{id}`. |
| `data.call_short_id` | string or null, required | The call's `cv-…` id. |
| `data.channel` | string, required<br>always `voice` | Always `voice`. |
| `data.conversation_id` | string, required | The call's conversation (its transcript). |
| `data.direction` | string or null, required<br>`inbound`, `outbound` | Who called whom. |
| `data.from` | string or null, required | The calling number, in E.164. |
| `data.to` | string or null, required | The called number, in E.164. |
| `data.agent_id` | string or null, required | The agent's `ag-…` id. |
| `data.agent_uuid` | string or null, required | The same agent's UUID. |
| `data.campaign_id` | string or null, required | The placing campaign's UUID; null for a single call. |
| `data.campaign_short_id` | string or null, required | The same campaign's `ca-…` id. |
| `data.status` | string, required<br>`queued`, `ringing`, `in_progress`, `completed`, `voicemail`, `no_answer`, `busy`, `failed` | The call's `status` after this event. |
| `data.occurred_at` | string, required | When this happened (ISO 8601). |
| `data.answered_at` | string, required | When it was answered. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### call.completed

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.call_id` | string, required | The call's UUID, its `id` on `GET /v1/calls/{id}`. |
| `data.call_short_id` | string or null, required | The call's `cv-…` id. |
| `data.channel` | string, required<br>always `voice` | Always `voice`. |
| `data.conversation_id` | string, required | The call's conversation (its transcript). |
| `data.direction` | string or null, required<br>`inbound`, `outbound` | Who called whom. |
| `data.from` | string or null, required | The calling number, in E.164. |
| `data.to` | string or null, required | The called number, in E.164. |
| `data.agent_id` | string or null, required | The agent's `ag-…` id. |
| `data.agent_uuid` | string or null, required | The same agent's UUID. |
| `data.campaign_id` | string or null, required | The placing campaign's UUID; null for a single call. |
| `data.campaign_short_id` | string or null, required | The same campaign's `ca-…` id. |
| `data.status` | string, required<br>`completed`, `voicemail`, `no_answer`, `busy` |   |
| `data.occurred_at` | string, required | When this happened (ISO 8601). |
| `data.answered_at` | string or null, required | When it was answered; null if never. |
| `data.outcome` | string, required<br>`valid_pickup`, `answered_human`, `invalid_pickup`, `callback_requested`, `do_not_call`, `wrong_person`, `hung_up_early`, `voicemail`, `ivr_system`, `no_answer`, `busy`, `disconnected`, `unreachable`, `failed`, `unknown`, `completed`, `delivered`, `pending` | How it ended (never null; `unknown` when unclassified). |
| `data.duration_sec` | integer, required<br>≥ 0 | Talk time in whole seconds. |
| `data.ended_at` | string, required | When it ended. |
| `data.disconnect_reason` | string or null, required | Who or what hung up, when known. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### call.failed

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.call_id` | string, required | The call's UUID, its `id` on `GET /v1/calls/{id}`. |
| `data.call_short_id` | string or null, required | The call's `cv-…` id. |
| `data.channel` | string, required<br>always `voice` | Always `voice`. |
| `data.conversation_id` | string, required | The call's conversation (its transcript). |
| `data.direction` | string or null, required<br>`inbound`, `outbound` | Who called whom. |
| `data.from` | string or null, required | The calling number, in E.164. |
| `data.to` | string or null, required | The called number, in E.164. |
| `data.agent_id` | string or null, required | The agent's `ag-…` id. |
| `data.agent_uuid` | string or null, required | The same agent's UUID. |
| `data.campaign_id` | string or null, required | The placing campaign's UUID; null for a single call. |
| `data.campaign_short_id` | string or null, required | The same campaign's `ca-…` id. |
| `data.status` | string, required<br>always `failed` |   |
| `data.occurred_at` | string, required | When this happened (ISO 8601). |
| `data.answered_at` | string or null, required | When it was answered; null if never. |
| `data.outcome` | string, required<br>`valid_pickup`, `answered_human`, `invalid_pickup`, `callback_requested`, `do_not_call`, `wrong_person`, `hung_up_early`, `voicemail`, `ivr_system`, `no_answer`, `busy`, `disconnected`, `unreachable`, `failed`, `unknown`, `completed`, `delivered`, `pending` | How it ended (never null; `unknown` when unclassified). |
| `data.duration_sec` | integer, required<br>≥ 0 | Talk time in whole seconds. |
| `data.ended_at` | string, required | When it ended. |
| `data.disconnect_reason` | string or null, required | Who or what hung up, when known. |
| `data.metadata` | map of string to string or null, required | The `metadata` you passed on the send, call or campaign create; null when none. |

</div>

### webhook.test

<div class="ref-table">

| field | type | description |
|---|---|---|
| `data` | object, required |   |
| `data.webhook_id` | string, required |   |
| `data.message` | string, required |   |

</div>