This is the payload reference. Creating a subscription, reading the delivery log, rotating a secret and test-firing all live on webhooks — come here for what actually arrives. Every event is a POST of application/json to your URL, with the same envelope whatever the type.

The envelope

Subscriptions are workspace-wide: every active webhook receives events for every agent, and a subscription with an empty event list receives every type. Filter on type and on the envelope’s agent_id in your handler. GET /v1/webhooks/event-types answers {"all": [...], "live": [...]}all is every string a subscription will accept, live the subset it reports as emitted today.
That endpoint currently lists session.queued and session.in_progress under all but not under live, which disagrees with what a subscriber actually receives: both are emitted, on phone calls. live gates only that listing, not delivery, so a webhook with an empty event list — the default, meaning all — gets them. They are documented below. If you see an event our own endpoint says does not exist, this is why.

Verifying a delivery

Three headers ride on every request, alongside Content-Type: application/json and User-Agent: talqing-webhooks/1. The secret is generated at create time as whsec_ followed by 32 random bytes, and shown in full exactly once — at create, and again when you rotate it. Reads return only secret_hint, the last four characters (whsec_…ab12). Supply your own at create if you would rather.
Sign the raw bytes, before any JSON parsing. Re-serializing a parsed body will not reproduce them, and every signature will fail. In Express that means express.raw({ type: "application/json" }) on this route, not express.json().
Compare in constant time — crypto.timingSafeEqual and hmac.compare_digest above. A plain === on a signature is a timing oracle.

Delivery is one attempt

There is no retry and no dead-letter queue. One POST per event per matching webhook, with a 5-second timeout. Anything that is not a 2xx, and every timeout, DNS failure and connection error, is recorded as failed in the delivery log and never sent again.
What that means for your handler:
  • Answer inside five seconds. Verify, enqueue, return. Do the real work after you have responded. A handler that calls your CRM inline will intermittently blow the budget and lose events silently.
  • Any 2xx is success. A 3xx is not: redirects are deliberately not followed, so a redirect counts as a failure.
  • Reconcile from the API, not from a replay. There is nothing to replay. GET /v1/calls takes start and end, so a nightly sweep over the window you were down closes the gap; GET /v1/webhooks/{webhook_id}/deliveries tells you which events you missed and when.
  • Your URL must be public. A private, internal or cloud-metadata address is refused when you save the webhook, and blocked again at delivery time.
Events for one call are dispatched in the order they happen: session.queued when an outbound dial is accepted, session.started (and, on a phone call, session.in_progress) when the call connects, then recording.ready once the audio is in storage, then session.completed after analysis and billing have run. They arrive over separate HTTP requests, so treat the order as the common case rather than a guarantee.

Every event type

batch.completed fires only for a batch that ran to the end. A cancelled or failed one has already said what happened, and a second event claiming completion would contradict the first.

session.queued

Sent when POST /v1/calls/outbound has written the call row and handed the dial to the carrier — before the agent has accepted it and before the callee has answered. It is the earliest notice that a session_id exists. One-at-a-time outbound phone calls only. A batch places its calls through a different path and sends no session.queued; nothing fires for a web call or a text conversation either.

session.started

Sent when a session begins — a web call, an inbound or outbound phone call, or a text window. The three telephony fields are absent from the text worker’s payload entirely, rather than present and null.

session.in_progress

Sent on inbound and outbound phone calls only, from the same code and at the same instant as session.started, carrying a byte-identical payload. It exists because a telephony integration expects the vocabulary; it carries no information session.started does not, and there is no window between them in which a call is one and not the other. Subscribe to one or the other. If your subscription lists no events — the default, meaning all — you will receive both for every phone call, and treating them as two stages will double-count.

recording.ready

Sent once the call’s audio has been written to object storage — before session.completed, so a player can light up without waiting on the analysis LLM. It does not fire for a call with no stored audio.
url is a credential with an hour’s life, and this payload lands in your logs. Take a copy of the file or drop the field before you store the event. When you need another link, GET /v1/calls/{session_id}/recording mints a fresh one.

session.completed in full

The end-of-session report: transcript, analysis, cost and recording state in one payload, dispatched once everything it describes is true. It is the event to subscribe to for a CRM, and the only one carrying the outcome. It closes the pair with session.started, and it fires for a text window as well as a call. All three producers — the voice worker, the text worker, and the billing sweep that picks up a session whose worker died — build it by reading the database back, so it cannot disagree with what GET /v1/calls/{session_id} shows a minute later.

Identity

The call itself

What the agent knew, and what a reader inferred

analysis is deliberately never merged into userdata. One is what a tool confirmed; the other is what a model guessed afterwards, and a merge would let the guess overwrite the fact with nothing downstream able to tell them apart. See call analysis.

The transcript

transcript is an array, oldest first. Each row:

Money

cost is the stored pricing snapshot, or null when the session was not priced — a call whose usage the catalog could not price, or one that has not been billed. When present: See costs for how to read a breakdown.

Recording and transfer

recording and screen_recording have the same four fields, and neither carries a link — this payload is rebuilt by the reconcile sweep hours after the call, where a URL that dies in an hour would be a broken promise. transfer is null on every call that never attempted one. There is no separate transfer event: a transfer ends our session, so this fires immediately afterwards and carries everything such an event would have.

Testing your endpoint

The test-fire button sends webhook.test with {"message": "talqing test event", "webhook_id": "…"}. It ignores the subscription filter — it is a probe of your endpoint, not a type anyone subscribes to — and refuses a disabled webhook with 400 webhook is disabled; enable it before testing. The result comes back inline as {"webhook_id", "status", "status_code", "error"}, so you see what your server answered without going to the delivery log.