Tracking call progress
A call moves through several states after you create it. This guide explains the difference between a call's status, its outcome and settlement, and shows how to track progress with webhooks (recommended) or polling.
Before you begin
- A key with
calls:read. To receive events you also needwebhooks:writeto register an endpoint once (see Receiving webhooks). - A call id (
call_...) from POST/calls. The requests below use the documented example id.
Status, outcome and settlement
Three fields answer three different questions:
| Field | Question | Values |
|---|---|---|
status | Where is the call in its lifecycle? | queued, ringing (not reported today), in_progress, ended, unknown |
outcome | How did it end? null until status is ended | completed (a conversation took place), no_response (answered, nobody spoke: most voicemail greetings), no_answer, busy, failed (could not connect), unknown |
settled | Is everything final? | true once outcome, duration, transcript, recording, analysis and the charge are all final |
end_reason adds the detail behind the outcome: hangup (a normal end of a connected call), no_answer, busy, invalid_number, network_error, insufficient_balance (the balance ran out during the call), not_confirmed (the start was never confirmed; the call is not charged), refused (the network refused to start it) or unknown.
artifacts shows each artifact's state: pending (not ready yet), ready (read it from its endpoint), absent (there is none for this call) or failed.
A typical answered call, step by step:
- Accepted (
202): statusqueued, outcomenull, artifacts allpending, settledfalse. - Answered: status
in_progress, outcomenull, artifacts allpending, settledfalse. - Hung up: status
ended, outcomecompleted, artifacts allpending, settledfalse, eventcall.ended. - Artifacts produced: status
ended, outcomecompleted, artifacts allready, settledfalse. - Charge final: status
ended, outcomecompleted, artifacts allready, settledtrue, eventcall.settled.
A call that is not answered goes from queued to ended with outcome: no_answer and its recording, transcript and analysis absent.
How long it takes: in test mode a call ends within about 20 seconds and settles within about 30. In live mode call.ended is sent usually within seconds and at most a few minutes after the call ends, and call.settled can be several minutes after that.
Track with webhooks
Webhooks are the recommended way: you get one HTTP request per change instead of polling every call.
- Register an endpoint subscribed to
call.endedandcall.settled. - On
call.ended, update the call's status and outcome in your system if you need them early (for example to show "No answer" in a dashboard). The transcript and analysis may still be pending. - On
call.settled, read the final values from the event'sdata.object(a snapshot of the call) and fetch the artifacts you need: transcript and recording, analysis.
Events can arrive more than once and in any order. Deduplicate by the event id, and never let an older snapshot overwrite a newer state: for example, ignore a call.ended that arrives after you already processed call.settled for the same call. When in doubt, read the call with GET/calls/{call_id}.
Track by polling
When you cannot receive webhooks (a script, a batch job, early development), poll GET/calls/{call_id} with backoff and stop when settled is true. Start at a couple of seconds, grow the interval, add jitter, cap it, and honour Retry-After on 429 and 503.
Read the call:
A few seconds after it was placed, the call is still queued (200):
JSON
settled is false, so wait and send the same request again: start at about 2 seconds and multiply the wait by 1.5 each time (with a little random jitter), up to 15 seconds between reads. On 429 or 503, wait the seconds in Retry-After instead. Stop when settled is true:
About 30 seconds later the test call is settled (200):
JSON
Complete runnable example
Follow a call until it settles, polling with backoff. Prefer webhooks in production.
The same steps as a complete script, with error handling, tested against test mode on every docs build. Each script reads your test key from an environment variable named in its first lines.
- cURL script (bash)
poll-call.sh - JavaScript (Node.js 20+)
poll-call.mjs - Python 3.9+ (requests)
poll-call.py
Reads count against the read rate limit (600 requests per minute per key). Polling a handful of calls is fine; polling hundreds in a loop is not. For many calls, use webhooks, or list them in one request with GET/calls filtered by status, campaign_id or created_after (see Common integration patterns).
Open enums
status, outcome, end_reason and the artifact states are open enums: new values can be added without a new API version. Write your code so that:
- an unknown
statuscounts as "not finished", - an unknown
outcomeis shown as "other" and is never treated as success, status: unknown(the state could not be confirmed for an unusually long time) is a reason to look again later or ask support, not a success,- an unknown artifact state is "not ready".
What can go wrong
rate_limitedon reads: Polling too fast or too many calls. Fix: WaitRetry-After, slow down, or switch to webhooks.resource_not_found: The id is from the other mode (a live id with a test key, or the reverse) or another workspace. Fix: Use the key that created the call.- The call stays
queuedfor a while: The start is being confirmed. Fix: Keep following it: it either proceeds, or ends withend_reason: not_confirmed(never confirmed, not charged) within about two minutes. endedbut artifactspending: Normal: they are produced after the call. Fix: Wait forcall.settled.settlednever arrives in your system: Your webhook endpoint failed or was not subscribed. Fix: Check the delivery log (Receiving webhooks) and reconcile with the list endpoint.