Skip to main content

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 a status 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_voicemail event immediately, so campaigns and AI voice agents can abandon, retry, or branch without waiting for the call to end. The call itself still ends at completed (or a failure status) — the event is a branch signal, not a status.
Inbound calls take the same arc in the other direction: an inbound ring moves to 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 the call.* events — webhooks emitted as the record advances: Three rules follow:
  1. Statuses and events track the same arc, but statuses are more granular. Failure statuses (no-answer, busy, canceled on an outbound call you abandoned before answer) all surface as terminal statuses, and any of them also reports through call.failed webhooks. Branch on the webhook type for “something changed” and on the status for “which outcome.”
  2. 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 the call.secure_payment.* family while the call record stays at answered. Use them for audit trails and dashboards; never treat them as a lifecycle advance.
  3. Screening events are branch signals, not outcomes. call.hit_voicemail fires 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).
Every one of these emits a 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 a call.transferred event 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 (or call.recording.ready once the file lands), recording.failed on a capture failure, and call.recording.paused / call.recording.resumed when 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.
  • status on the record — read it from GET /calls responses 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_voicemail as “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

  1. Treating call.initiated as acceptance of a human conversation. It fires at ring time for outbound and at arrival for inbound — before any answer. A call that ends no-answer still fired call.initiated. Pair it with call.answered before you conclude anyone spoke.
  2. Waiting for a failure-specific event per cause. There is one call.failed event type for every failure outcome; the cause is the status on the payload. Branch on data.status, not the event type, when you separate no-answer (retry-worthy) from failed (a routing or policy problem).
  3. Assuming exactly one terminal event per call. A mid-call event can arrive after your system has ingested call.completed — a late call.recording.paused audit line, for example. Apply webhook updates idempotently keyed on call_id and let the terminal status close your record.
  4. 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.
  5. Missing the early inbound signal. For inbound calls, call.ringing fires before call.answered specifically so an incoming-call popup does not wait for pick-up. If your agent UI subscribes only to call.answered, agents see the call at the same moment they are supposed to answer it.
Once the state machine is clear, the per-event payloads are in the webhook events reference, and the endpoint-level call fields are in the Voice API reference.