Skip to content
RelationsAI
Guides

Tracking call progress

Available

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 need webhooks:write to 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:

FieldQuestionValues
statusWhere is the call in its lifecycle?queued, ringing (not reported today), in_progress, ended, unknown
outcomeHow did it end? null until status is endedcompleted (a conversation took place), no_response (answered, nobody spoke: most voicemail greetings), no_answer, busy, failed (could not connect), unknown
settledIs 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:

  1. Accepted (202): status queued, outcome null, artifacts all pending, settled false.
  2. Answered: status in_progress, outcome null, artifacts all pending, settled false.
  3. Hung up: status ended, outcome completed, artifacts all pending, settled false, event call.ended.
  4. Artifacts produced: status ended, outcome completed, artifacts all ready, settled false.
  5. Charge final: status ended, outcome completed, artifacts all ready, settled true, event call.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.

  1. Register an endpoint subscribed to call.ended and call.settled.
  2. 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.
  3. On call.settled, read the final values from the event's data.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:

curl -X GET https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7 \ -H "Authorization: Bearer <token>"

A few seconds after it was placed, the call is still queued (200):

JSONJSON
{ "id": "call_6Vb3Nm8Qw2Er5Ty9Ui4Op7", "object": "call", "livemode": false, "status": "queued", "outcome": null, "end_reason": null, "direction": "outbound", "source": "api", "agent_id": "agent_1ZltiWzxrXmMprGD6q63", "campaign_id": null, "phone_number_id": "pn_6Vhm3sFrB5UpmXAnsvmDu", "from": "+12025550150", "to": "+12025550100", "metadata": { "crm_record_id": "L-1042" }, "created_at": "2026-10-21T09:30:00.000Z", "ended_at": null, "duration_seconds": null, "billable_minutes": null, "cost": null, "artifacts": { "transcript": { "state": "pending" }, "recording": { "state": "pending" }, "analysis": { "state": "pending" } }, "settled": false, "settled_at": null }

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):

JSONJSON
{ "id": "call_6Vb3Nm8Qw2Er5Ty9Ui4Op7", "object": "call", "livemode": false, "status": "ended", "outcome": "completed", "end_reason": "hangup", "direction": "outbound", "source": "api", "agent_id": "agent_1ZltiWzxrXmMprGD6q63", "campaign_id": null, "phone_number_id": "pn_6Vhm3sFrB5UpmXAnsvmDu", "from": "+12025550150", "to": "+12025550100", "metadata": { "crm_record_id": "L-1042" }, "created_at": "2026-10-21T09:30:00.000Z", "ended_at": "2026-10-21T09:32:14.000Z", "duration_seconds": 42, "billable_minutes": 0, "cost": { "amount": 0, "currency": "INR" }, "artifacts": { "transcript": { "state": "ready" }, "recording": { "state": "ready" }, "analysis": { "state": "ready" } }, "settled": true, "settled_at": "2026-10-21T09:41:02.000Z" }
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.

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 status counts as "not finished",
  • an unknown outcome is 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_limited on reads: Polling too fast or too many calls. Fix: Wait Retry-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 queued for a while: The start is being confirmed. Fix: Keep following it: it either proceeds, or ends with end_reason: not_confirmed (never confirmed, not charged) within about two minutes.
  • ended but artifacts pending: Normal: they are produced after the call. Fix: Wait for call.settled.
  • settled never 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.

Next steps

Retrieving recordings and transcripts.