Skip to main content

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. 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.
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.
Call initiation errors

The call object

Returned by POST /v1/calls/phone, GET /v1/calls/{call_id} and POST /v1/calls/{call_id}/end. Status lifecycle: queued (dialing initiated, no ring yet) → ringingongoing (callee answered) → ended (the conversation took place, including voicemail drops and transfers) or failed.
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.
Web calls skip ringing and failed: they go queuedongoing (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.
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.
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: 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

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.
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.