Skip to content

Events & webhooks

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:

  1. Outbound webhooks — register an endpoint; we POST a signed, retried delivery when a subscribed event fires.
  2. 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 identical event envelope, and all payloads are snake_case.

typeFires whendata fields
conversation.createdA new conversation is openedconversation_id, channel, contact_id?
turn.completedAn agent turn finishesconversation_id, surface, reply_preview, finish_reason, model
confirmation.requestedThe agent parks a risky action for confirmationaction_id, action_code, summary, conversation_id?
confirmation.resolvedA parked action is approved/rejected/expiredaction_id, action_code, resolution
spend.recordedBillable spend is recordedamount_usd, category, conversation_id?, balance_usd?
run.completedA workflow run finishesrun_id, workflow_id, status

Every event — over a webhook or the SSE feed — has this shape:

{
"id": "evt_9f8c…",
"type": "turn.completed",
"workspace_id": "ws-…",
"created_at": "2026-07-15T22:31:04.001Z",
"data": { "conversation_id": "cv-…", "surface": "api", "reply_preview": "On it —…", "finish_reason": "stop", "model": "octo" }
}
Terminal window
curl https://api.omniocto.com/v1/webhooks \
-H "Authorization: Bearer $OCTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/hooks/octo",
"event_types": ["turn.completed", "confirmation.requested"]
}'

event_types is optional and defaults to ["*"] (all events). Omit it to subscribe to everything. The response returns the endpoint plus a secret — this is shown once and is used to verify signatures:

{
"id": "whe-0123456789",
"object": "webhook_endpoint",
"url": "https://your-app.example.com/hooks/octo",
"event_types": ["turn.completed", "confirmation.requested"],
"enabled": true,
"secret": "whsec_…"
}

Manage endpoints with GET /v1/webhooks, GET /v1/webhooks/{id}, PATCH /v1/webhooks/{id} (toggle enabled, change url/event_types), and DELETE /v1/webhooks/{id}. Reads never return the secret again.

Deliveries are signed with the Standard Webhooks scheme (svix-compatible). Each POST carries:

webhook-id: evt_9f8c… (the event id)
webhook-timestamp: 1752619864 (unix seconds)
webhook-signature: v1,K5f2… (base64 HMAC-SHA256)

The signed content is ${webhook-id}.${webhook-timestamp}.${raw-body}, keyed by your whsec_ secret (base64-decoded). Verify with the svix / standardwebhooks libraries, or manually:

import { createHmac } from "node:crypto";
function verify(secret: string, id: string, ts: string, body: string, header: string) {
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = "v1," + createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
return header.split(" ").includes(expected);
}

A delivery that does not return a 2xx is retried with exponential backoff (5s, 10s, 20s, … capped at 1h) up to 6 attempts, then abandoned. An endpoint that keeps failing is automatically disabled; re-enable it with a PATCH.

Terminal window
curl -N https://api.omniocto.com/v1/events \
-H "Authorization: Bearer $OCTO_API_KEY"

The stream first replays any events after your cursor, then tails new events live. Each frame is a standard SSE event whose id: is a monotonic cursor and whose event: is the event type:

id: 1041
event: turn.completed
data: {"id":"evt_…","type":"turn.completed","workspace_id":"ws-…","created_at":"…","data":{…}}

Pass ?after=<cursor> to resume after a known event, or rely on the standard Last-Event-ID request header — reconnecting clients that echo the last id they saw pick up exactly where they left off with no gaps or duplicates.

Terminal window
curl -N "https://api.omniocto.com/v1/events?after=1041" -H "Authorization: Bearer $OCTO_API_KEY"

Pass ?types=turn.completed,run.completed to receive only those event types.

A :ping comment is sent every 15s to keep the connection alive through proxies.