true when post-call processing has finished. Always true on the webhook payload itself. Use it when polling GET /calls/{id} to tell final results from in-progress processing.
pipeline_completed_at
string (ISO 8601) or null
Timestamp when post-call processing finished. Non-null on the webhook. null when polling GET /calls/{id} and processing has not completed yet, or on older sessions that pre-date this field.
transcript
array
Ordered list of conversation turns (see below)
recording_url
string or null
Presigned URL for the call recording. null if recording was not enabled. URL is valid for 7 days. If it expires, mint a fresh one with GET /calls/{call_id}/recording-url.
variables
object
Pre-call variable values that were substituted into the agent’s prompt and greeting. Empty object {} when no variables were supplied. Echoed for partner-side audit.
extraction
object or absent
Post-call extraction result, present only when the agent has a Custom Analysis config defined. Status is success, failed, or skipped (no transcript). Values are typed per the agent’s config; missing keys are emitted as null. See Post-call Extraction below.
call
object or absent
Call metadata. Present only for telephony sessions started via POST /calls/start. Omitted entirely for browser WebRTC sessions started via POST /sessions/webrtc (no associated phone call).
call.call_id
string (UUID)
Unique call identifier
call.from_number
string
Caller phone number (E.164)
call.to_number
string
Destination phone number (E.164)
call.direction
string
"outbound"
call.status
string
Call lifecycle/carrier status: completed, failed, no_answer, busy, canceled. Voicemail is exposed through call.disposition, not call.status.
call.disposition
string or null
Final call outcome: completed, voicemail, user_hangup_no_speech, no_answer, busy, canceled, blocked, or failed.
call.answered_by
string or null
Best-known answer classification: human, machine, unknown, or not_answered. machine indicates an answering machine or voicemail.
call.transferred
boolean
true if the bot handed the call off to a human via the cold-transfer flow. See Transferred calls below.
call.transferred_at
string (ISO 8601) or null
When the bot left the room and the carrier began bridging the caller to the human. null for non-transferred calls.
call.transfer_target
string or null
E.164 destination the caller was bridged to. null for non-transferred calls.
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.
Both the webhook and GET /api/v2/calls/{id} return the same data shape, so a partner 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:
status processing_complete meaning─────────────────────────────────────────────────────────────────────────────active false call still in progresscompleted false call ended; post-call processing still runningcompleted true final — transcript / recording / extraction are stablefailed true terminal failure (no_answer, busy, reject); no transcript
async function fetchUntilReady(callId) { while (true) { const { data } = await vocobase.get(`/calls/${callId}`); if (data.session?.processing_complete === true) return data; if (data.status === 'failed' || data.status === 'no_answer' || data.status === 'busy') return data; await sleep(30_000); }}
In practice the session.completed webhook will reach you before polling kicks in — polling is the fallback for partners 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.
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.
Extracted fields, typed per the agent’s extraction_config.keys. Missing fields are emitted as null. Absent entirely when status != 'success'.
error
string or absent
Failure reason — present only when status === 'failed'
model
string
Opaque extraction model identifier. Do not branch business logic on this value.
extractedAt
string (ISO 8601)
When extraction ran
latencyMs
integer
LLM latency in milliseconds
attempts
integer
Number of attempts (1 = success on first try; 2-3 = retried)
Extraction failures never block webhook delivery. The unified payload always ships with extraction.status indicating outcome — partners that need extraction values can branch on status === 'success', partners that just need transcript + recording can ignore the field.
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).
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.