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, alongsideContent-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.
crypto.timingSafeEqual and hmac.compare_digest
above. A plain === on a signature is a timing oracle.
Delivery is one attempt
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
2xxis success. A3xxis 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/callstakesstartandend, so a nightly sweep over the window you were down closes the gap;GET /v1/webhooks/{webhook_id}/deliveriestells 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.
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.
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 sendswebhook.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.