Events and webhooks for Octo integrations
Most API calls let an agent observe a turn it started. Events let an agent listen for workspace state changes it did not start — a turn completing, a confirmation resolving, a run finishing — without polling.
There are two complementary surfaces. Use either or both:
- Outbound webhooks — register an endpoint; we POST a signed, retried delivery when a subscribed event fires.
GET /v1/events— a long-lived Server-Sent Events stream of the same events, with a resume cursor, for agents that prefer to hold a connection open rather than host an endpoint.
Both carry the same events, and all payloads are snake_case. The stream sends the 2026-10-01 envelope, or v2 with ?api_version=v2; a webhook endpoint receives the format its api_version names (Payload versions).
Event catalog
Section titled “Event catalog”type | Fires when | data fields |
|---|---|---|
conversation.created | A new conversation is opened | conversation_id, channel, contact_id? |
turn.completed | An agent turn finishes | conversation_id, surface, reply_preview, finish_reason, model |
confirmation.requested | The agent parks a risky action for confirmation | action_id, action_code, summary, conversation_id? |
confirmation.resolved | A parked action is approved/rejected/expired | action_id, action_code, resolution |
spend.recorded | Billable spend is recorded (amounts in credits) | amount_credits, category, conversation_id?, balance_credits? |
run.completed | A workflow run finishes | run_id, workflow_id, status |
message.received | An SMS or WhatsApp message reaches one of your numbers (after STOP/START handling) | message_id, conversation_id, contact_id, channel_id, from, to, body, media[], media_failed, received_at, campaign_id, keyword, channel (sms or whatsapp; absent on older events, meaning sms), quick_reply (a WhatsApp button tap’s payload, else null) |
message.sent | A text left for the carrier: an AI agent’s or a teammate’s reply, or an API send | message_id, conversation_id, author, agent_id, to, from, channel_id, body, send_ids[], sent_at |
message.status_updated | A text’s or a WhatsApp message’s status changed | channel, status, send_id, campaign_id, message_id, to, from, state, display_status, segments, billed_credits, error, sent_at, delivered_at, updated_at (SMS). WhatsApp: channel, status, message_id, conversation_id, campaign_id, to, from, error, template, updated_at |
campaign.started | A campaign was started (SMS: its send is confirmed) | campaign_id, campaign_short_id, name, campaign_type, status, started_at |
campaign.paused | A campaign stopped sending: a teammate, out of credits, its credit limit, the carrier’s daily limit, too many failures, the carrier refusing the number, a needed review, a card for the trial number, or a problem on our side | the common fields, reason, message, paused_at, resumes_at |
campaign.resumed | A paused campaign is sending again | the common fields, resumed_at, previous_reason |
campaign.completed | A campaign finished | the common fields, completed_at, totals, billed_credits |
campaign.canceled | A campaign was canceled | the common fields, canceled_at, in_flight, not_yet_sent |
campaign.waiting | An SMS campaign is outside its sending hours and waits for them to open (not a pause) | the common fields, reason, waiting_at, resumes_at, tz |
campaign.window_opened | Its sending hours started again and it goes on sending | the common fields, opened_at, tz |
meta_response.completed | A background POST /v1/responses request finished or failed | response_id, conversation_id, status, error |
meta_chat.updated | A chat with Octo got a new question and reply, and was saved | conversation_id, surface, title, preview, message_count |
call.ringing | An outbound phone call started ringing | call_id, conversation_id, direction, from, to, agent_id, campaign_id, status, occurred_at |
call.answered | A phone call was answered (by a person or a machine) | the call fields, answered_at |
call.completed | A phone call ended (status completed, voicemail, no_answer or busy) | the call fields, answered_at, outcome, duration_sec, ended_at, disconnect_reason |
call.failed | A phone call failed (a system or carrier failure) | the same as call.completed |
The three message.* events, the seven campaign.* events, the four
call.* events, turn.completed, meta_response.completed and
meta_chat.updated are emitted today;
Receive webhooks says what each message.*,
campaign.* and call.* event promises. The other types are published on the same log, webhooks and
stream as their emitters roll out: you can subscribe now, but don’t build a
hard dependency on them until the changelog announces them.
message.status_updated for campaign texts, and the call.* events of a
campaign’s calls, reach only endpoints that name the type; "*" does not
include them. "*" includes every other type. The campaign.* events are a handful per
campaign, so "*" includes them. They are delivered at least once and can, very rarely,
be missing after a failure on our side or repeat after a race: act on the
status in the payload, not on the number of events, and read
GET /v1/campaigns/{id} when it matters (it is the source of truth). A
status event we could not write at the time is written later, once (usually
10 to 15 minutes after the change); it carries the time it was written, not
the time of the change, and a late campaign.resumed has
previous_reason: null.
Octo events
Section titled “Octo events”These two used to be sent by the older integration webhook
(/api/users/me/integrations), which is retired: those routes now answer
410 integration_webhook_retired. Subscribe to the types on a webhook
endpoint, or read them from /v1/events, like any other event. The payloads
are snake_case, unlike the old webhook’s.
meta_response.completed: a background response finished (seebackgroundmode).response_idis theresp_…idGET /v1/responses/{id}takes,statusiscompletedorfailed, anderrorisnullwhen it completed, otherwise{ "type", "code", "message" }, the same error the stored response carries (codeisnullwhen there is none). A response failed by our restart sweep sends it too.meta_chat.updated: one per saved Octo turn, wherever it came from (surface:web,api,mcp, or a channel such asemail).titleis the chat’s title,previewthe start of Octo’s reply (about 120 characters),message_countthe number of messages saved in the chat.
Envelope
Section titled “Envelope”Every event — over a webhook or the SSE feed — has this shape:
{ "id": "evt_9f8c…", "type": "turn.completed", "workspace_id": "5b1d2c7e-…", "created_at": "2026-07-15T22:31:04.001Z", "seq": 48213, "data": { "conversation_id": "cv-…", "surface": "api", "reply_preview": "On it —…", "finish_reason": "stop", "model": "octo" }}This is the 2026-10-01 envelope, which the SSE feed and endpoints that
existed before release v1.84.0.0 receive. A new endpoint receives the v2 envelope (object,
api_version, livemode, and data in the /v1 words): see
Payload versions.
workspace_id is the workspace’s UUID. seq is the event’s position in the
workspace’s log: the id: of its /v1/events frame, and what to order
webhooks by (see Handle events).
Outbound webhooks
Section titled “Outbound webhooks”Webhooks for these events work exactly as for the messaging events, and are documented once, in Receive webhooks:
| to | see |
|---|---|
register an endpoint (POST /v1/webhooks, the secret shown once) | Register an endpoint |
| list, change, pause, delete, rotate the secret, send a test event | Manage endpoints |
check the webhook-signature of a delivery | Verify the signature |
| the retry schedule, and when an endpoint is disabled | Retries |
repeats and ordering by seq | Handle events |
the v2 or 2026-10-01 format an endpoint receives | Payload versions |
Owners and admins can also manage endpoints in the dashboard, under Settings → API Keys & Webhooks, where the last 7 days of deliveries show what was sent and what your server answered.
GET /v1/events (SSE)
Section titled “GET /v1/events (SSE)”The same events as a Server-Sent Events stream, with a resume cursor, for an
agent that would rather hold a connection open than host an endpoint. Pass
?types=turn.completed,run.completed to receive only those. Connecting,
resuming, the 7-day log and the stream’s limits:
The event stream.