Skip to main content

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-level event 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 as session.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.
session.failed was previously published in this catalog with no producer behind it. It has been removed, and it is no longer accepted in an endpoint’s events allowlist — sending it now returns 400 VALIDATION_ERROR. Existing allowlists that contained it were stripped of it automatically; an endpoint left with an empty allowlist receives every event type.
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 an action value:
  • ANSWER_WITH_AI
  • SELECT_AGENT
  • TRANSFER
  • REJECT
When action is 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

Do not build a call record from call.status.updated alone. This event is enqueued from the carrier’s callback, which can arrive several seconds before we have written the call’s final row. On a terminal update that means hangup_cause and hangup_source are frequently still null, and there is no end timestamp on this event at all — occurred_at marks the moment of the state transition, not the end of the call.Use call.status.updated for real-time dialer and concurrency orchestration. Use session.completed for the final record: it is built after the call row is settled and carries call.started_at, call.answered_at, call.ended_at and the final hangup attribution. If you need the record before session.completed arrives, poll GET /calls/{call_id}, which returns started_at and ended_at under those same names. answered_at is currently carried only on session.completed.

Call status values

Vocobase normalizes provider-specific call states into one vocabulary:

Provider normalization

Accounts should branch on data.status, not on provider-specific values. Vocobase stores the raw provider value for debugging.

External queue orchestration

If you run your own dialer, use call.status.updated to manage slots:
Do not wait for 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 the transcript 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.

Hangup attribution coverage

hangup_source and hangup_cause come from the carrier’s Call Detail Record, which we fetch after the hangup callback. Not every carrier exposes one. On carriers that do not, both fields are null on every call — including calls that ended perfectly normally. Treat null as “this carrier does not report it,” never as “nobody hung up” and never as an error. In particular, do not infer an abandoned or failed call from a null hangup_source; use call.disposition for outcome logic and hangup_source only as supporting detail when present. "unknown" is distinct from null: it means the carrier answered the question and did not know. Both are non-actionable, but only null means the signal was never available. If your account needs teardown attribution and you are seeing null across the board, contact support — the coverage gap is per-carrier, and switching the number’s provider is usually what changes it.

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:
  1. The bot speaks the configured announcement (transfer_message).
  2. Vocobase transfers the caller to transfer_target.
  3. The session.completed webhook includes transfer metadata.
The session.completed webhook fires once the bot leg ends, not when the caller eventually hangs up with the human. This means:
  • duration_seconds reflects 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.transferred is true; call.transferred_at is the redirect moment; call.transfer_target is the destination E.164.
Use call.transferred as the cheap discriminator if your downstream automation needs to distinguish a transferred call from a normal hangup.

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

These payloads have no top-level timestamp. Every other event in this reference carries one; the WhatsApp events carry data.ts instead. If your handler reads event.timestamp unconditionally it will get undefined here. Fall back to data.ts, or to the X-Webhook-Timestamp header, which is sent on every delivery including these.

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.
Scope resolution: the messaging log has no agent of its own, so the agent is resolved through the message’s session when there is one. A message with no session is agentless and therefore reaches account-wide endpoints only — an agent-scoped endpoint will never see it. See Scoping & Event Filtering.

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:
In practice the 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 same session.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 edit extraction_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

The variables 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.