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 carriesflow: 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.Create a call
Request
- cURL
- Python
- Node.js
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
instructioncan 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’svariables, as they are in the agent’s prompt, and checked by the same rule. If the agent declares aninput_schema, every variable the prompt reads must be declared in it. An undeclared one returns400 invalid_request.- It works on
phoneandwebcalls. Arealtimesession takes its instructions from the client’ssession.update, sosystem_promptonchannel: "realtime"returns400 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 is400 invalid_request and nothing is dialled. The
message names the key and, where there is a fixed list, the values allowed:
400 Bad Request
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 onGET /v2/calls/{id}, on
each call in GET /v2/calls and in data.call of every
webhook.
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.callof every webhook for the call. - The agent never sees it. Anything the agent should use goes in
variables. - A call without metadata has no
metadatafield at all. - Put IDs in it, not secrets or personal data: it travels on every webhook.
400 invalid_request.
Idempotency
SendIdempotency-Key with a value derived from your own domain object:
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
- cURL
- Python
- Node.js
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.
Get the transcript
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
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
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
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
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
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.