# Tracking call progress

**Availability: 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](/guides/receiving-webhooks)).
- A call id (`call_...`) from [`POST /calls`](/api/calls#place-a-call). 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:

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

> **Values that can still change:** Between `ended` and settled, `outcome`, `duration_seconds`, `billable_minutes` and `cost` can still change. Write them to your system as final only when `settled` is `true` (or on `call.settled`).

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](/guides/receiving-webhooks) 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](/guides/retrieving-recordings-and-transcripts), [analysis](/guides/working-with-analysis-results).

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}`](/api/calls#retrieve-a-call).

## Track by polling

When you cannot receive webhooks (a script, a batch job, early development), poll [`GET /calls/{call_id}`](/api/calls#retrieve-a-call) 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:

```bash
curl -X GET https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7 \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const call = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/calls/call_6Vb3Nm8Qw2Er5Ty9Ui4Op7",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
call = response.json()
```

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

```json
{
  "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`):

```json
{
  "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: [cURL script](/examples/guides/tracking-call-progress/poll-call.sh) · [JavaScript](/examples/guides/tracking-call-progress/poll-call.mjs) · [Python](/examples/guides/tracking-call-progress/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`](/api/calls#list-calls) filtered by `status`, `campaign_id` or `created_after` (see [Common integration patterns](/guides/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`](/errors/rate_limited) on reads**: Polling too fast or too many calls. Fix: Wait `Retry-After`, slow down, or switch to webhooks.
- **[`resource_not_found`](/errors/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](/guides/receiving-webhooks)) and reconcile with the list endpoint.

## Next steps

[Retrieving recordings and transcripts](/guides/retrieving-recordings-and-transcripts).
