Webhook Payloads
This page documents the structure of webhook payloads sent by Vocobase when events occur.Headers
Every webhook request includes these headers:Event types
Branch on the top-levelevent field. These are the values accepted in an
endpoint’s events allowlist — see
Scoping & Event Filtering.
Six event types, and that is the complete list.
GET /api/v2/config/webhooks
returns the same set as data.available_events, so you can read it from the API
rather than hard-coding it.
webhook.test is fired only by the test endpoint and is not a valid
allowlist value — it always reaches the endpoint being tested. Handle or ignore
it explicitly.
How a failed session surfaces
A session that ends in a terminal failure — carrier no-answer, busy, rejected, or a bot that never started — is delivered assession.completed, not as a
separate failure event. Every session-terminal path builds its payload through
the same builder, which always emits event: "session.completed", including the
explicit failure path.
Branch on the payload, not on the event name:
data.call.status is the reliable discriminator for telephony sessions. For
WebRTC sessions there is no call object at all, so use duration_seconds and
transcript.
inbound.route (below) is a pre-answer routing callback with its own headers
and a required response body. It is not part of this event stream and cannot be
subscribed to.
Event: webhook.test
Sent by POST /api/v2/config/webhooks/{id}/test. Signed and retried exactly
like a real event, so receiving it confirms your URL, TLS, and signature
verification all work end to end.
agent_id is the endpoint’s scope: null for an account-wide endpoint, the
agent’s id for an agent-scoped one.
Event: inbound.route
This event is sent when an account-configured pre-answer policy is available for a Vobiz or Plivo inbound call.
Payload structure
Headers
Inbound route webhooks for this feature use feature-specific headers:Response contract
Accounts return JSON with anaction value:
ANSWER_WITH_AISELECT_AGENTTRANSFERREJECT
TRANSFER, to_number must be E.164.
When action is SELECT_AGENT, agent_id must be set.
Vocobase records each resolved route decision for your own observability. Query GET /api/v2/inbound-route-decisions to inspect action, source, webhook status, webhook latency, failure reason, transfer target, and created session or phone-call IDs.
Event: call.status.updated
Sent whenever Vocobase learns that a telephony call moved to a new meaningful call status. This event is for real-time orchestration: live dashboards, your own dialer queues, and concurrency release.
call.status.updated is sent for phone calls only. Browser WebRTC sessions do not have a carrier call leg and therefore do not emit this event.
Payload structure
Field reference
Call status values
Vocobase normalizes provider-specific call states into one vocabulary:Provider normalization
Accounts should branch ondata.status, not on provider-specific values. Vocobase stores the raw provider value for debugging.
External queue orchestration
If you run your own dialer, usecall.status.updated to manage slots:
session.completed to release a dialer slot. session.completed is the final rich result after transcript, recording, billing, and extraction processing.
Event: session.completed
Sent when a Vocobase session has reached final post-call processing. For telephony calls, this happens after the call leg ends and the bot/session pipeline has persisted transcript, recording metadata, billing, variables, and extraction results.
Payload structure
Field reference
Data object fields
Transcript entry format
Each entry in thetranscript array has:
recording_url is a presigned URL valid for 7 days from the moment the webhook fires. Download and persist the recording on your side if you need long-term retention. If the URL expires before you can download it, call GET /calls/{call_id}/recording-url to mint a fresh one with another 7-day window.End call reason
end_call_reason answers one question — why did this call end? — with one
vocabulary that reads the same on every carrier, including MCube and Exotel,
which report no carrier hangup cause at all.
It is never null on session.completed. UNKNOWN and NOT_CONNECTED are
values, not absence: they say what we honestly know, rather than implying a
conversation that nothing witnessed.
How it relates to disposition
They answer different questions and are both correct at once.
dispositionis the coarse outcome bucket that drives campaign retry logic.end_call_reasonis why the call ended.
Use
disposition for retry logic. Use end_call_reason for reporting and for
explaining an outcome to a human.
Caveats worth reading once
- On
call.status.updated,end_call_reasonisnullwhile the call is live. A reason for ending does not exist until the call ends. - On the terminal
call.status.updated, it usually reflects the carrier half only. The carrier’s completion callback reaches us before our bot’s own end-of-call report is persisted, so an answered call typically readsCOMPLETEDon that event and, say,CUSTOMER_HUNG_UPonsession.completed.session.completedis the authoritative one. TRANSFERREDmeans the transfer was initiated, not that the human picked up: a busy transfer destination still readsTRANSFERRED.TRANSFERREDis not reported on VoiceLink or MCube. Those carriers bridge the transfer in-band, so we never see it happen; a transferred call on them readsCUSTOMER_HUNG_UP. This is a known gap.GET /callsandGET /calls/{call_id}can disagree on a call whose session failed but whose carrier leg completed. The single-call route reports the session’s status (failed, and so typicallySYSTEM_ERROR); the list reports the carrier’s (completed, and so whatever the bot reported, orUNKNOWN). This is a pre-existing difference in how the two routes reportstatus, whichend_call_reasonnow follows.GET /calls/{call_id}is the more complete of the two.GET /calls/{call_id}can report a transientfailed/SYSTEM_ERRORin the short window after the carrier has ended a call but before our bot’s end-of-call report has been persisted (typically seconds; longer if the report is being retried). Once the report lands, the same call reads the bot’s reason. If you poll immediately aftercall.status.updated, prefer to wait forsession.completedinstead.hangup_causeandhangup_sourceremain on every payload this release and are deprecated; they are removed in the next release.end_call_reasonreplaces both.
Call holds
Vocobase notices when a call is put on hold and reports every hold inhold.
English, Hindi and Hinglish are recognised, plus common Punjabi and Marathi hold messages.
null is not the same as “no hold”. null means hold detection did not run
for this call (it was off for the agent, or the call predates the feature).
{"detected": false, ...} means it ran and found no hold.
Long holds end the call. If a single hold lasts more than a minute, the agent
says a short goodbye and hangs up, so a call parked on hold does not keep
running. The call then reports end_call_reason: "COMPLETED" with
hold.ended_on_hold: true. For a person asking to wait, only their silence
counts toward the minute, not the agent’s own speech.
Short waits are not holds. A person asking to wait is reported only if they
stay quiet for at least 15 seconds; “one second” followed by more talking is
ordinary conversation. A question (“one minute, who is this?”) is never a hold,
and talking about a hold (“my loan is on hold”) is not one either.
Holds inform extraction. When a hold is found, your post-call
extraction is told about it, so an outcome such as
“customer asked us to wait” is not missed just because the call carried on
afterwards.
A hold message played by the phone network appears in
transcript as a
user turn, because it is heard on the customer’s side of the line. Use
hold rather than the transcript to tell the two apart.Hold detection is switched on per agent. Contact support to turn it off for
an agent.
Final call status on session.completed
call.status in session.completed reflects the final known carrier state. For real-time intermediate statuses, use call.status.updated.
Call disposition values
Transferred calls
When an agent transfers a live call to a human:- The bot speaks the configured announcement (
transfer_message). - Vocobase transfers the caller to
transfer_target. - The
session.completedwebhook includes transfer metadata.
session.completed webhook fires once the bot leg ends, not when the caller eventually hangs up with the human. This means:
duration_secondsreflects only the bot leg (caller-on-with-bot time).recording_url, when present, is the bot-leg recording. The human side is not recorded.call.transferredistrue;call.transferred_atis the redirect moment;call.transfer_targetis the destination E.164.
call.transferred as the cheap discriminator if your downstream automation needs to distinguish a transferred call from a normal hangup.
IVR Flows calls
Calls placed withPOST /ivr-flows/{flowId}/calls (and test calls from the IVR editor) carry an extra ivr block on session.completed. Every other field is unchanged. The lead you pushed is echoed as variables. agent_id and agent_name identify an internal agent that backs the flow; key your integration on ivr.flow_id.
ivr.fetches[].error is one of timeout, connect_error, url_blocked, too_large, bad_json, render_error, http_<status> (your API answered with a non-2xx status), missing_<field> (a required field was not in the response) or type_<field> (a field could not be read as its type).
The ivr block never carries Fetch URLs, headers, request bodies or the values your API returned.
Pre-dial failures. When a pre-dial Fetch fails, the number is never dialed. The call ends the way any failed session does: call.status is failed, call.end_call_reason is NOT_CONNECTED, duration_seconds and credits_used are 0, and transcript is null. ivr.failure is prefetch_failed, ivr.path ends at the Fetch that failed, and ivr.fetches shows why.
Event: call.transferred
Fired the moment a live call is handed off to a human, from the transfer route
on a successful transfer. The call is still in progress when this fires, so
there is no duration, disposition, transcript, or recording yet —
transferred_at and transfer_target are the payload.
Emitted for phone calls only. Browser WebRTC sessions cannot be transferred.
Payload structure
Field reference
call.transferred does not replace session.completed. A transferred call
still emits session.completed when the bot leg ends, with
call.transferred: true. Use call.transferred when you need to react at
hand-off time; use session.completed for the final record.Events: whatsapp.message.*
Three events cover outbound WhatsApp messaging. They share one payload shape and
differ only in the event value and the data.status that triggered them.
Payload structure
Field reference
data.status is uppercase here, unlike call.status.updated and
session.completed, whose call statuses are lowercased. Compare
case-insensitively if you normalize statuses across event types.Polling GET /calls/{id} vs webhook
Both the webhook and GET /api/v2/calls/{id} return the same data shape, so an account that can’t accept inbound webhooks (or wants to reconcile dropped deliveries) can poll the GET endpoint instead.
A call can appear completed before all transcript, recording, and extraction fields are ready. The processing_complete boolean tells you when the response is final:
Recommended polling pattern
session.completed webhook will reach you before polling kicks in — polling is the fallback for accounts that can’t accept inbound webhooks, or for reconciliation after a delivery failure.
For sessions created before this field shipped,
pipeline_completed_at will be null permanently. Treat null + status: completed on an older row as “final” — those rows pre-date the signal.Post-call extraction
When the agent has a Custom Analysis config defined, the platform runs an LLM extraction over the transcript after every completed call and ships the result inline in the samesession.completed webhook — no second webhook to handle. Configure extraction via the dashboard’s “Variables & Analytics” tab on the agent editor or via PUT /api/v2/agent/:id with extraction_config.
Extraction object structure
Extraction failures never block webhook delivery. The unified payload always ships with
extraction.status indicating outcome — accounts that need extraction values can branch on status === 'success', accounts that just need transcript + recording can ignore the field.Replay extraction against a past session
If you editextraction_config and want to backfill an existing session, call POST /api/v2/calls/{call_id}/extract with { "dry_run": false }. Optional dry_run: true returns the result without persisting. The replay uses the agent’s current config (not the config at original-call time).
Pre-call variables
Thevariables field on every webhook echoes the per-call values that were substituted into the agent’s prompt and greeting. Configure variable names via the dashboard “Variables & Analytics” tab or PUT /api/v2/agent/:id with variables: ["callee_name", "mobile_number"]. Supply per-call values via POST /api/v2/calls/start body field variables: { callee_name: "Sajal" }. Missing values render as empty strings — the platform never errors on missing variables.
Next steps
Webhook Setup
Configure webhook endpoints and verify signatures.
Error Handling
Understand retry behavior and failure handling.