> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hubtalk.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Individual calls

> `POST /v1/calls/phone`, the call object, reading, ending and listing calls.

# Individual calls

## `POST /v1/calls/phone`

One outbound call: the platform registers the call, answers immediately, and dials the destination from a durable dial queue through the specified trunk, connecting the published agent when the callee answers.

| Field               | Type           | Required | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------- | -------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`          | string         | yes      | —       | Agent name. Must be **published** and listed in the `agents` of the outbound subgroup being used.                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `trunk_id`          | string         | yes      | —       | The trunk to dial through.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `to_number`         | string         | yes      | —       | Destination number; normalized before dialing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `route_id`          | string         | no       | `""`    | Outbound subgroup of the trunk. Empty → resolved from (`trunk_id`, `agent_id`): the agent attached to exactly one subgroup of the trunk selects it; several → `409 agent_subgroup_ambiguous`; none → `403 agent_not_in_subgroup`. Unknown `route_id` → `404 subgroup_not_found`.                                                                                                                                                                                                                                                                        |
| `from_number`       | string         | no       | `""`    | Caller ID; must be one of the **subgroup's** `caller_numbers`. An empty string leaves the choice to the subgroup, following its caller-ID policy: `random` (the default) picks one at random, `round_robin` walks the pool in turn. Candidates are the subgroup pool intersected with the organization grant pool; when a single number remains, that is the one used. A subgroup with an empty pool refuses every value, including empty (`caller_number_not_allowed`); an empty intersection with the grant gives `caller_number_outside_grant_pool`. |
| `dynamic_variables` | object         | no       | `{}`    | Variables handed to the agent's flow (customer name, account data).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `metadata`          | object         | no       | `{}`    | Your opaque correlation payload. Echoed back unchanged in the call object and in **every** webhook — put your lead ID here.                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `ring_duration`     | number \| null | no       | `null`  | Per-call ring limit in seconds; must be > 0. `null` uses the agent's default.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

**Number normalization.** All non-digit characters are stripped from `to_number` (`"+1 (555) 123-4567"` → `"15551234567"`). A number with no digits is rejected with `400`. If the **subgroup** defines a dial prefix, it is prepended to the normalized number; allowed destinations are checked against the normalized number **before** the prefix.

**Order of checks.** The dial-queue ceiling (`429 dial_queue_full`) is checked first, before any validation. Then, synchronously and before the call is registered: trunk and grant (404), subgroup and agent attachment (404/409/403), caller ID (400), destination policy (403), subgroup limits (429), `ring_duration` (400), and finally the published version of the agent (409). A refusal at any of these steps leaves no call behind. Because the limits are checked before the publication check, a 429 does not tell you whether the agent is published.

The `Idempotency-Key` header is recommended here (and effectively required after a `429`): a repeat with the same key returns the **already-created call** instead of dialing a second time. Keys are remembered for 24 hours and are scoped to your API key and endpoint. The replay returns the call object **as it was at creation** (`status: "queued"`), not its current state — poll `GET /v1/calls/{call_id}` for that.

<Note>
  **Success is `202 Accepted`, not `201`.** The call is registered and queued for dialing; the dial itself happens asynchronously after the response. Store `call_id`: it is the correlation key for polling and for every webhook.
</Note>

**Call initiation errors**

| Status | `error.code`                             | Condition                                                                                                                                                                                                                                               |
| ------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `caller_number_not_allowed`              | `from_number` is not one of the subgroup's numbers                                                                                                                                                                                                      |
| 400    | `caller_number_outside_grant_pool`       | `from_number` is outside the pool granted to your organization (trunks used under a grant only)                                                                                                                                                         |
| 400    | `destination_number_invalid`             | `to_number` contains no digits                                                                                                                                                                                                                          |
| 400    | `ring_duration_invalid`                  | `ring_duration` ≤ 0                                                                                                                                                                                                                                     |
| 403    | `agent_not_in_subgroup`                  | the agent is not attached to the subgroup (this is also the answer for an agent name that does not exist)                                                                                                                                               |
| 403    | `destination_not_allowed`                | the destination is blocked by the subgroup's policy                                                                                                                                                                                                     |
| 404    | `trunk_not_found` / `subgroup_not_found` | unknown `trunk_id` (or no grant for it) / unknown `route_id`                                                                                                                                                                                            |
| 409    | `agent_subgroup_ambiguous`               | the agent is in several subgroups of the trunk — pass `route_id`                                                                                                                                                                                        |
| 409    | `agent_not_published`                    | the agent has no published version                                                                                                                                                                                                                      |
| 422    | `invalid_request`                        | the body fails schema validation; field-level details are in `message`                                                                                                                                                                                  |
| 502    | `dial_failed`                            | only when the operator has disabled the dial queue (synchronous fallback). In the default asynchronous mode a dial failure is **not** an HTTP error — the call is created (`202`) and later reads as `status: "failed"`, `status_reason: "dial_failed"` |

## The call object

Returned by `POST /v1/calls/phone`, `GET /v1/calls/{call_id}` and `POST /v1/calls/{call_id}/end`.

| Field                                              | Type                     | Description                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| -------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `call_id`                                          | string                   | Call identifier (`call-out-*` outbound, `call-in-*` inbound, `web-*` web).                                                                                                                                                                                                                                                                                                                                                                             |
| `agent_id`                                         | string                   | Agent name.                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `flow_version`                                     | string \| number \| null | Version marker of the published flow the call ran on. **Its type is not stable**: while the call is live it is an opaque fingerprint string; once the call is stored it is the integer version number. Treat it as opaque and use `flow_version_id` for identity.                                                                                                                                                                                      |
| `flow_version_id`                                  | number \| null           | Numeric ID of the published flow version.                                                                                                                                                                                                                                                                                                                                                                                                              |
| `channel`                                          | string                   | `phone` \| `web`.                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `direction`                                        | string                   | `outbound` \| `inbound` \| `web`.                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `status`                                           | string                   | Live state: `queued` → `ringing` → `ongoing` → `ended` \| `failed`.                                                                                                                                                                                                                                                                                                                                                                                    |
| `call_status`                                      | string                   | Final bucket: `completed` \| `not_connected` \| `error`; `""` until finalized.                                                                                                                                                                                                                                                                                                                                                                         |
| `status_reason`                                    | string                   | Fine-grained reason within the bucket; `""` until finalized.                                                                                                                                                                                                                                                                                                                                                                                           |
| `error_code`                                       | string \| null           | Carrier / telephony error code when the platform received one; `null` otherwise. Diagnostic.                                                                                                                                                                                                                                                                                                                                                           |
| `message_left`                                     | boolean                  | Answering-machine detection triggered and the agent left a message.                                                                                                                                                                                                                                                                                                                                                                                    |
| `started_at`                                       | number \| null           | Start of the attempt (for phone calls — before dialing).                                                                                                                                                                                                                                                                                                                                                                                               |
| `answered_at`                                      | number \| null           | The moment the callee answered; `null` for unanswered and web calls.                                                                                                                                                                                                                                                                                                                                                                                   |
| `media_started_at`                                 | number \| null           | When audio media started flowing. Diagnostic.                                                                                                                                                                                                                                                                                                                                                                                                          |
| `talk_from`, `talk_to`                             | number \| null           | Bounds of the talk-time interval used for `duration`. Diagnostic.                                                                                                                                                                                                                                                                                                                                                                                      |
| `ended_at`                                         | number \| null           | End of the call; `null` while live.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `duration`                                         | number \| null           | Talk time in seconds from `answered_at`. `null` for unanswered calls, for `error/not_finalized`, and while ringing; for a live call, the elapsed talk time.                                                                                                                                                                                                                                                                                            |
| `transcript`                                       | array                    | Ordered turns: `{"role": "user"\|"assistant", "text": string, "interrupted"?: true, "pending"?: true, "unspoken"?: true, "ts"?: number, "spoke_at"?: number}`. `unspoken: true` means the reply never played at all — it was interrupted before its first sound; the caller did not hear it, and it goes into neither the model's context nor post-call analytics. The key was added without a version bump: parsers that do not know it keep working. |
| `dynamic_variables`                                | object                   | Current conversation variables: your initial values plus what was collected during the call. On an **unanswered** call it holds the values you supplied for the contact — an empty object is not a no-answer marker, read `call_status` for that.                                                                                                                                                                                                      |
| `metadata`                                         | object                   | Your payload, unchanged.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `analysis_status`                                  | string                   | `""` (not started) \| `pending` \| `processing` \| `done` \| `skipped` \| `failed` \| `interrupted`. Terminal: `done`, `skipped`, `failed`, `interrupted`.                                                                                                                                                                                                                                                                                             |
| `call_analysis`                                    | object \| null           | Only when `analysis_status` is `done`: `summary`, `sentiment`, `custom_fields`, `call_successful`.                                                                                                                                                                                                                                                                                                                                                     |
| `health`                                           | string \| null           | Technical call health: `ok` \| `warning` \| `error`; `null` until finalized. It reflects provider and infrastructure issues, **not** conversation quality.                                                                                                                                                                                                                                                                                             |
| `health_reasons`                                   | array                    | Reason codes behind the verdict, see the glossary below.                                                                                                                                                                                                                                                                                                                                                                                               |
| `events`                                           | array                    | Call events without details: `{"type", "turn", "ts", "title"?, "status"?}` (observer incidents, transfers and similar moments). Additive, diagnostic.                                                                                                                                                                                                                                                                                                  |
| `cost`                                             | object \| null           | `{"total_microusd", "analytics_microusd"}` — `null` in the responses your key receives.                                                                                                                                                                                                                                                                                                                                                                |
| `recording_url`                                    | string                   | Stable download URL once the recording is ready; `""` before that.                                                                                                                                                                                                                                                                                                                                                                                     |
| `recording`                                        | object \| null           | Recording metadata: `{"id", "status", "channels", "format", "duration", "size", "expires_at"}`. `status` — `recording` \| `processing` \| `ready` \| `failed` \| `deleted`; `channels` — `stereo` \| `mono`.                                                                                                                                                                                                                                           |
| `from_number`, `to_number`, `trunk_id`, `route_id` | string                   | Phone calls only.                                                                                                                                                                                                                                                                                                                                                                                                                                      |

**Status lifecycle:** `queued` (dialing initiated, no ring yet) → `ringing` → `ongoing` (callee answered) → `ended` (the conversation took place, including voicemail drops and transfers) or `failed`.

<Warning>
  **`failed` does not by itself mean the callee never answered.** Usually it comes with `call_status: "not_connected"` (`no_answer`, `busy`, `dial_failed`, …), but a platform failure also reads as `failed` with `call_status: "error"` (`internal_error`, `not_finalized`, `agent_unavailable`). Always read `call_status` / `status_reason`.
</Warning>

Web calls skip `ringing` and `failed`: they go `queued` → `ongoing` (first turn of history) → `ended`.

Asynchronous fields fill in after the call ends: `call_analysis` when post-call analysis completes, `recording_url` when the recording is finalized. The `call_analyzed` webhook notifies you when both are done — polling is a fallback, not the primary mechanism.

## Reading and ending a call

`GET /v1/calls/{call_id}` returns the full call object; `404 call_not_found` for unknown IDs.

`POST /v1/calls/{call_id}/end` hangs up a live call — a phone call still waiting in the dial queue is cancelled before dialing — and returns the call object.

<Warning>
  `end` is idempotent only **within the live-registry window**: calling it on a call that ended less than 5 minutes ago returns the object without an error; after that the registry entry is gone and the endpoint answers `404 call_not_found`, even though `GET /v1/calls/{call_id}` still returns the stored object. Do not use `end` as a way to read state.
</Warning>

`GET /v1/calls/{call_id}/turns` returns per-turn latency metrics of a finished phone or web call: an array of `{"turn", "metrics": {"vad_ms", "asr_ms", "eou_wait_ms", "classifier_ms", "llm_ms", "tts_ms", "turn_ms", …, "estimated"?: true}, "interrupted"?: true, "merged_from"?: number}`. A diagnostic surface for load testing; `404` for unknown IDs and for non-call sessions.

`merged_from` appears on a caller turn whose sentence was split by a pause and [glued back together](/v4/platform/campaign-history): it is the number of fragments, and the turn's phases are those of the first one. The key is additive: a client that ignores it parses the response the ordinary way. There is no `llm_first_sentence_ms` metric in the response.

## `GET /v1/calls` — list calls

| Parameter                  | Notes                                                                                                 |
| -------------------------- | ----------------------------------------------------------------------------------------------------- |
| `agent_id`                 | filter by agent                                                                                       |
| `direction`                | `outbound` \| `inbound` \| `web`. **Not validated**: an unknown value yields an empty list, not `400` |
| `status`                   | only `ongoing` or `ended` (anything else → `400 invalid_filter_value`); `ended` includes failed calls |
| `call_status`              | `completed` \| `not_connected` \| `error` (anything else → `400 invalid_filter_value`)                |
| `status_reason`            | exact match, e.g. `agent_hangup` (unknown value → `400 invalid_filter_value`)                         |
| `health`                   | `ok` \| `warning` \| `error` — one-click "problem calls only": `health=error`                         |
| `health_reason`            | exact code match. Not validated: an unknown code yields an empty list                                 |
| `from_number`, `to_number` | ⚠️ applied **after** pagination, to the current page only                                             |
| `since`, `until`           | bounds on call start, epoch seconds, inclusive                                                        |
| `limit`                    | 1–200, default 50; out of range → `422 invalid_request`                                               |
| `cursor`                   | cursor from a previous response; malformed → `400 invalid_cursor`                                     |

Response: `{calls: [...], has_more, next_cursor}`. List items are summaries without a transcript; fetch `GET /v1/calls/{call_id}` for the full object. Iterate by passing `next_cursor` back as `cursor` until `has_more` is `false`. Only phone and web calls are listed; editor test sessions and chats are not.

<Warning>
  `from_number` / `to_number` are page-local filters: a page may contain fewer than `limit` items (possibly zero) while `has_more` is still `true`. Do not treat an empty page as the end of results. For reconciliation by number, prefer filtering on your own `metadata` client-side.
</Warning>
