Voice call lifecycle
Every call that crosses Orbit — placed by your app through the API, dialed by a campaign, or dialed by a customer into one of your numbers — carries astatus field on the call record that advances as the call moves toward completion or a failure outcome. This page explains that state machine at the concept level: what the states mean, what advances each transition, and where the edges are. Read it before you subscribe to call webhooks or build reporting against call outcomes.
This is the voice counterpart to Delivery lifecycle for messaging. The per-payload webhook contract for every call.* event lives in the webhook events reference; the request and response fields for placing and controlling calls live in the Voice API reference. This page ties the two together without restating them.
The happy path
An outbound call that succeeds end-to-end passes through:initiated → ringing → answered → completed
Each transition is advanced by a different actor — no single party sees the whole arc:
One actor sits beside this path and rewrites the tail of it:
- The AMD screening. On outbound calls with answering-machine detection enabled, an answer can resolve to a machine, a fax tone, or a carrier voicemail screen instead of a person. That fires a
call.hit_voicemailevent immediately, so campaigns and AI voice agents can abandon, retry, or branch without waiting for the call to end. The call itself still ends atcompleted(or a failure status) — the event is a branch signal, not a status.
ringing (with a call.ringing event so your dashboard can paint an incoming-call UI before anyone answers), then to answered when an agent, an AI voice agent, or a phone tree picks up, then to completed on hangup.
Status vs. event
The distinction that matters most for integrations is between the call’s status — a field on the call record — and thecall.* events — webhooks emitted as the record advances:
Three rules follow:
- Statuses and events track the same arc, but statuses are more granular. Failure statuses (
no-answer,busy,canceledon an outbound call you abandoned before answer) all surface as terminal statuses, and any of them also reports throughcall.failedwebhooks. Branch on the webhook type for “something changed” and on the status for “which outcome.” - Mid-call events carry no status change. A transfer inside a live call, a DTMF sequence injected mid-call, a recording paused and resumed, an agent-assisted secure-payment session — these emit
call.transferred,call.dtmf_sent,call.recording.paused,call.recording.resumed, and thecall.secure_payment.*family while the call record stays atanswered. Use them for audit trails and dashboards; never treat them as a lifecycle advance. - Screening events are branch signals, not outcomes.
call.hit_voicemailfires mid-arc; the status it foreshadows arrives when the call actually ends.
Failure outcomes
A call that never completes talking resolves to a terminal failure status:no-answer— the far end rang but nobody picked up inside the timeout.busy— the far end rejected the call as busy.failed— the network or the platform could not advance the call (routing failure, rejected destination, wallet out of credit at placement).canceled— your app or dialer abandoned the call before it was answered (an API cancel, a campaign stop, a queue caller hanging up).
call.failed webhook with the specific status on the payload, so a subscriber can keep failure-rate metrics per cause rather than per generic failure event.
Concurrency and transfer states
A live call does not have to stay a two-party line:- A transfer between agents or to an external number moves the call through
transferred(and acall.transferredevent per leg) while the call itself remains answered. - A consult-then-transfer parks the caller on hold while the agent speaks to the target, then either bridges all three parties or completes a cold transfer — the status arc runs
answered → on_hold → consulting → answered → completed. - A conference has its own event family —
conference.created,conference.participant_joined,conference.participant_left,conference.ended— because a conference room is not scoped to a single call. If your product builds “who is on this call right now” UI, consume the conference events rather than inferring membership from call statuses. - A recording follows its own lifecycle beside the call:
recording.started(orcall.recording.readyonce the file lands),recording.failedon a capture failure, andcall.recording.paused/call.recording.resumedwhen an operator pauses capture mid-call. A recording event never implies a call status — a paused recording is not a held call.
What to branch on
Integrations should switch on machine-readable fields only, never on display labels:call_id— the join key across every event in a call’s life. All lifecycle, mid-call, and recording events for one call carry it; reconcile by id, not by arrival order.- event
type— the primary branch point on webhooks. Handle the full lifecycle set (call.initiated,call.ringing,call.answered,call.completed,call.failed) plus the mid-call family (call.transferred,call.dtmf_sent,call.recording.paused,call.recording.resumed,call.hit_voicemail,call.disclosure.omitted) if you subscribe to them. statuson the record — read it fromGET /callsresponses or from the webhook payload when you need the specific terminal outcome. Keep failure-cause metrics per status, not per event type.- AMD / hit-voicemail — treat
call.hit_voicemailas “decide now,” and treat the terminal status as “the call closed,” so an abandoned machine call and a completed machine call are both countable.
Common pitfalls
- Treating
call.initiatedas acceptance of a human conversation. It fires at ring time for outbound and at arrival for inbound — before any answer. A call that endsno-answerstill firedcall.initiated. Pair it withcall.answeredbefore you conclude anyone spoke. - Waiting for a failure-specific event per cause. There is one
call.failedevent type for every failure outcome; the cause is the status on the payload. Branch ondata.status, not the event type, when you separateno-answer(retry-worthy) fromfailed(a routing or policy problem). - Assuming exactly one terminal event per call. A mid-call event can arrive after your system has ingested
call.completed— a latecall.recording.pausedaudit line, for example. Apply webhook updates idempotently keyed oncall_idand let the terminal status close your record. - Inferring conference membership from call statuses. A call leg entering a conference does not change the leg’s status in a way you can derive occupancy from; that is exactly what the
conference.*event family is for. Consume those events for membership, and call statuses for the leg’s own outcome. - Missing the early inbound signal. For inbound calls,
call.ringingfires beforecall.answeredspecifically so an incoming-call popup does not wait for pick-up. If your agent UI subscribes only tocall.answered, agents see the call at the same moment they are supposed to answer it.