Skip to main content
A call is one outbound dial attempt driven by an agent. Creating a call is asynchronous: you get 202 Accepted immediately and the outcome arrives by webhook (or by polling GET /v2/calls/{id}). To show the call while it runs, open its live event stream. A call can also run in your user’s browser instead of on a phone: create it with channel: "web". See Browser calls for the whole flow. The examples on this page use the sandbox address. On the production address, a key from a workspace that has not gone live is refused with 403 production_workspace_required.

The call object

Flow outcome

A call run by a node-based agent also carries flow: where the flow finished, the steps it went through and the values it extracted. It appears here, on each call in GET /v2/calls, and at data.call.flow in call.processed. A call run by a single-prompt agent has no flow field.
flow is absent until the outcome is recorded, usually within seconds of the call ending, so it can be missing from the terminal event. call.processed waits up to 2 minutes for it.

Statuses

Statuses only move forward. A terminal status never changes.

Ended reasons

ended_reason explains why a terminal status was reached. Match on status for control flow; use ended_reason for analytics and support.
Treat ended_reason as an open vocabulary. We add new values without a version bump, so always have a default branch. The last two rows above are in the vocabulary, but no call emits them today. They arrive when warm transfer and later releases ship. See the roadmap.
no_answer is under-reported todaySome calls that genuinely rang out are reported as failed with ended_reason: "no-media" rather than as no_answer.Whether we can tell the two apart depends on what the carrier tells us. When the carrier returns a clear verdict (SIP 480 or 408 for rang out, 486 or 600 for busy), you get no_answer or busy correctly. When it black-holes the call instead, accepting the invite and returning nothing, we have no signal to tell “rang, nobody picked up” apart from “answered into silence”. The call then ends as failed with no-media after the media gate expires.If you count unanswered calls for retry logic, treat no_answer, busy, and failed with no-media together as calls that did not connect. None of the three is billed, so your costs are unaffected either way. We are working on this, and the reported status will only get more specific over time.

Create a call

Request

202 Accepted
202 means the call is admitted to the queue. The phone has not rung yet. The full call object is available from GET /v2/calls/{id}. A phone call’s 202 has no ws_url and no transport. A browser call’s 202 adds them, because the page also needs an audio link:
202 Accepted (channel web)

Errors

Variables

variables is a flat map of string keys to string values. Each key replaces the matching {{key}} placeholder in the agent’s system_prompt and first_message, or in the call’s own system_prompt and first_message when you send them.
  • Plain {{key}} placeholders match loosely: letters and digits only, case-insensitive, so {{Customer Name}}, {{customer_name}} and {{CustomerName}} all read the same variable. Only a Liquid expression with a filter ({{ key | upcase }}) needs the exact key.
  • A placeholder with no matching variable is replaced with an empty string. There is no error, and no {{braces}} are left for the agent to read aloud. Validate on your side.
  • Keep values short. They are part of the prompt, and the prompt is on the latency path.
  • Variables are static per call. There is no way to change them mid-call. On a browser call, an instruction can direct the agent mid-call instead.
  • On a node-based agent, variables fill the node prompts, the start node’s greeting and extraction prompts. Transition conditions are not filled in.

Per-call prompt

system_prompt on the call replaces the agent’s prompt for that call only. The agent is not changed, and the rest of its setup still applies to the call: its voice, language, opening line and tools. One agent with variables is still the simplest way to personalise calls, because the instructions stay the same and only the details change. Send a per-call prompt when each call needs different instructions, for example when your own system writes the script for each customer.
  • It holds 1 to 16,000 characters, the same ceiling as the agent’s prompt. A blank or longer prompt returns 400 invalid_request.
  • {{variable}} placeholders are filled from the call’s variables, as they are in the agent’s prompt, and checked by the same rule. If the agent declares an input_schema, every variable the prompt reads must be declared in it. An undeclared one returns 400 invalid_request.
  • It works on phone and web calls. A realtime session takes its instructions from the client’s session.update, so system_prompt on channel: "realtime" returns 400 invalid_request.
  • A node-based agent runs its nodes’ prompts, so it refuses a per-call prompt with 409 flow_system_prompt_unsupported.
  • We never return it: it is not on the call object and not in any webhook. To know later which prompt a call ran, store it on your side against the call id.
  • Keep it short. The prompt is on the latency path, and a long one adds delay well before it reaches the limit. See the prompting guide.

Call settings

call_settings sets the voice and the languages of one call. The agent and its saved revisions stay as they are.
Every key is optional. A key you leave out keeps the agent’s value. The settings apply from the agent’s first word, greeting included, on every channel. On a realtime session, the client’s session.update can still change them. language sets what speech recognition listens for, and our standby voice pronounces in it if it serves the call. It does not change your prompt. If the agent should reply in that language, say so in the prompt, for example with a variable. The older "voice": { "voice_id": "…" } on a request still works and means call_settings.voice_id. Send one or the other: both with different values is a 400.

Where the settings come from

call_settings is accepted in three places, with the same keys and rules. A call starts from the agent’s own values, and each later source wins, key by key: A request can set the voice, for example, and a pre-call tool that answers with only language then changes the language and keeps the request’s voice.

Bad values

On a request, a bad value is 400 invalid_request and nothing is dialled. The message names the key and, where there is a fixed list, the values allowed:
400 Bad Request
The same 400 answers a value of the wrong type, a blank value (leave the key out instead), an unknown key, a language longer than 35 characters, a secondary language outside the list, and more than 4 secondary languages. A campaign contact with a bad value is rejected on its own row, and the rest of the list goes in. A pre-call tool’s bad value never fails the call: the call keeps the value it had, and the tool’s receipt says which key was skipped and why.

On the call object

Every call shows the settings it runs with, the agent’s own values included, and who chose each one. They are on GET /v2/calls/{id}, on each call in GET /v2/calls and in data.call of every webhook.
Each value in sources is agent, request, campaign or pre_call:<tool name>. call.queued shows the settings before pre-call tools run, and every event from call.started on shows the ones the call used. Calls from before per-call settings existed have no call_settings, and neither does an incoming call turned away before an agent was found.

Metadata

metadata holds your own identifiers, so every webhook can be matched to your records without a lookup table:
  • Up to 16 keys. A key is 1 to 40 characters: ASCII letters, digits and _, ., :, -, starting with a letter, digit or _.
  • Values are strings of up to 256 characters. The whole object, serialized, is at most 4 KB.
  • It is fixed when the call is created and returned unchanged on the call object and in data.call of every webhook for the call.
  • The agent never sees it. Anything the agent should use goes in variables.
  • A call without metadata has no metadata field at all.
  • Put IDs in it, not secrets or personal data: it travels on every webhook.
Invalid or oversized metadata returns 400 invalid_request.

Idempotency

Send Idempotency-Key with a value derived from your own domain object:
If we have seen that key in the last 24 hours, we replay the original response byte for byte and place no second call. This is the safe way to retry a request that timed out. A network timeout does not tell you whether the call was placed, and without the key a retry makes the customer’s phone ring twice. The replay repeats events_url while its token is valid, and leaves it out after that. Your API key still opens the live event stream. The key is scoped to your workspace. Reusing a key with a different body still replays the original response, so pick keys that are unique per intended call. There is one window where a reused key does not replay: while the first request with that key is still being processed, there is no stored response to replay yet, so the second request gets 409 duplicate_call. It means the first request is already in flight and no second call was placed. Retry a moment later to get the original response, or wait for the webhook.

Get a call

200 OK returns the call object. You get 404 not_found both for an unknown ID and for an ID that belongs to another workspace. The two responses are identical on purpose.
Poll no faster than every 5 seconds, and only when you cannot receive webhooks. Polling counts against your rate limit; webhooks do not.

Get the transcript

Returns what was said, as turns, once the call has ended.
200 OK
404 not_found while the call is still running, and for a call that produced no speech at all. Check transcript_status on the call object to tell them apart: pending, empty or failed. For browser calls, final results deliver the same turns by webhook as soon as they are ready.
404 Not Found

Get the recording

Returns 302 to a signed, time-limited URL for the call’s audio (WAV). Follow the redirect. Most HTTP clients do by default; curl needs -L.
302 Found
The signed link expires in 15 minutes and it is a bearer token.Use it to fetch the recording right away. To get the audio later, store the call_id and ask again; do not store the link. Anyone holding it can fetch the audio until it expires, so treat it exactly as you would treat the recording.
404 not_found while the call is running, for a call that produced no audio, and for one whose audio was never stored, which includes every call created with recording_enabled: false. recording_available on the call object tells you which requests are worth making. Transcripts and recordings are kept for 90 days, the same as the call record. Pull anything you need to keep longer into your own system.

Abort a call

Cancels a call that has not ended yet. Abort is accepted while the call is queued, dialing or in_progress. If the call is live, the SIP server hangs it up mid-conversation. On a browser call, abort also closes the browser’s audio socket. Only a call that has already reached a terminal status is refused. Abort is immediate and silent. To end a browser call with a spoken goodbye, send a close control instead.
202 Accepted
202 means the cancel was signalled. The call reaches aborted shortly after with ended_reason: "aborted-by-api", and a call.aborted event fires. An aborted call is billed for the time it was connected, at the same rate and in the same blocks as any other call. An abort while the call is still queued or ringing costs nothing.

Live control and events

Three more endpoints work while a call runs. Every call has the live event stream. Live control is for browser calls; Browser calls covers it in full.

List calls

200 OK
Results are newest first by created_at, inbound and outbound calls together. Each call in the list gives its total as cost_inr. For the full cost block with its rates, read the call with GET /v2/calls/{id}. next_cursor is an opaque string; send it back as cursor unchanged. See pagination.
Use the list endpoint for reconciliation and reporting. To drive your application state, use webhooks.

Count calls

How many calls your workspace has made and taken, without paging through them. These are the same figures the console’s Calls page shows. Leave both out to count every call. The window works the same way as on GET /v2/calls, so a count and a list over the same window cover the same calls.
200 OK
Every count is always present, even when it’s zero.