data.object is always
"call" and data.call is the call object below - the same shape as
GET /v1/calls/{id}. Fields fill in as the call progresses: cost,
custom_analysis and transfer are null until they apply.
Current API version: 2026-08-01
The three terminal events fire for every call - including one that was transferred to a human (see
Transfers).
The call object
Present asdata.call on every event - the same object as GET /v1/calls/{id},
so you learn one shape across REST and webhooks. Fields are null until the relevant stage finishes.
string
The call id — the same
id that POST /v1/calls returns.string | null
The agent that ran the call.
string
Where the call got to. Mid-call:
started, in_progress, transferring, transfer_answered. Finished: completed, voicemail, no_answer, busy, declined, failed, transfer_completed, transfer_failed. Open set — treat it as a string rather than switching exhaustively.string
Which way the call went:
inbound, outbound, or web for a call that happened in a browser rather than on a phone. A web call carries no numbers. For WHO started it, read origin.string | null
Caller number.
string | null
Destination number.
integer | null
Connected duration.
string
When the call was created (ISO 8601, UTC).
string | null
When it connected.
string | null
When it ended.
object | null
Your passthrough JSON from
POST /v1/calls.object | null
The call’s dynamic/custom variables - from
POST /v1/calls variables, or resolved for inbound (e.g. caller_no). Only your custom variables, not internal runtime state.object | null
{ currency: "USD", total, rate_per_min } - null until billed.object | null
Every value the call’s analysis groups extracted, keyed by group name then analysis name - including Vocily’s built-in Default Analysis group.
null until analyzed.object
The conversation.
object | null
Present only if a transfer happened:
{ state, target, reason }. state is transferring | answered on call.transfer.status, and completed | failed on call.ended. Persists onto later events.Recordings on webhooks. The webhook payload does not include the stable
recording_url. Once
a recording is ready, call.processing.completed carries a ready-to-use, expiring
recording_presigned_url (plus recording_url_expires_at), minted fresh for each delivery. Do not
cache or store the pre-signed URL - it expires. For durable access, call
GET /v1/calls/{id}/recording (Vocily’s stable endpoint) with your API key.Call started
call.started - the call was placed and is dialing/ringing. Identity, routing, and your
metadata/variables are present; timing, cost, transcript, analysis, and recording are not yet available.
Call transfer status
call.transfer.status - a human-transfer state changed, in-call:
transferring- dialling the human.answered- the human actually picked up (bridged).failed- the human didn’t answer (busy / no-answer).
completed, is reported on call.ended, not
here - this event only carries in-call progress.
Call ended
call.ended - the call reached a terminal status. The core outcome event, with transcript, timings,
and cost.
Call analysis completed
call.analysis.completed - post-call analysis finished. The call’s custom_analysis field is now
populated with every value its analysis groups extracted:
custom_analysis is keyed by group name, then analysis name - exactly the names you typed in
the library, so renaming one changes the key later calls publish. See
Custom Analysis.
There is no separate
analysis object. Vocily’s built-in summary, sentiment, outcome and
language are the Default Analysis group, attached to every new agent, and arrive inside
custom_analysis like every other extracted value - so there is one place to read analysis from,
not two. custom_analysis["Default Analysis"]["User Sentiment"] is positive, neutral, or
negative; Outcome and Language are free text. A caller who detaches the group, or switches
one of its values off, simply gets no key for it.Call processing completed
call.processing.completed - the recommended default subscription. A fan-in event that fires once
analysis and the recording are terminal - the complete record in a single payload. Beyond the call
object it adds a dashboard deep link and explicit status fields so a null reads as “not
produced” rather than “not ready”:
Transfers
A transfer runs as part of the same call. In-call progress arrives oncall.transfer.status; the
terminal outcome on call.ended:
- Progress:
call.transfer.status{transferring}(dialling) →call.transfer.status{answered}(the human actually picked up). - Failed (busy / no-answer):
call.transfer.status{failed}, then the call ends →call.endedwithtransfer.state = "failed". - Answered, then the bridged call ends:
call.endedfires when that bridged call actually ends, withtransfer.state = "completed", followed bycall.analysis.completedandcall.processing.completed- so the terminal events fire for transferred calls too.
transcript and the recording cover the call up to the handoff - the human↔caller
conversation after the bridge is not captured. duration_seconds and cost cover the whole
call, including the time after the handoff, because the phone network keeps billing the line
for all of it.