Quickstart
Five requests from an API key to a completed phone call — create an agent, deploy it, dial, and read back the transcript-bearing execution.
By the end of this page you will have made a real phone call and read back what happened on it. Every request below is complete and copy-pasteable; every response is the shape the API actually returns.
Set your key once so the rest of the page works verbatim:
Check your key
API keys start with gat_ and travel in an Authorization: Bearer header —
the only form /v2 accepts. GET /v2/user/me is the cheapest way to confirm a
key works, and it tells you the two numbers you will care about later: how many
calls you may run at once, and how much credit is left.
A 401 with error 1101 means you sent a browser session token rather than
an API key. /v2 rejects those locally, before any network call, so a leaked
dashboard session can never drive the API.
POST /v2/api-keys mints further keys, but it requires the
keys:write scope, which is denied by default — no key already
in circulation can mint another one until an account admin grants it. Your first
key therefore comes from your account admin, not from this API.
Create an agent
An agent is a name, a system prompt, a welcome line, and a tasks array — the
raw pipeline configuration that says which transcriber, model and voice to run.
name and a non-empty tasks list are the only required fields.
Anything in {braces} in the prompt or the welcome message becomes a variable
you fill per call in step 4.
Two things in that response are worth pointing at now, because they surprise people later:
variablescomes back as names, not objects. They are extracted from the prompt and the welcome message. The create body takes objects ([{"name": "callee_name"}]); reads hand back["callee_name"].conversation_configis empty on create. It is an update-only field — send it on aPATCHonce the agent exists.
tasks is echoed from the agent service's stored copy, which fills in its own
defaults inside each task. Read the agent back with
GET /v2/agents/{agent_id} for the exact configuration that will run.
If a task carries extraction_details without an extraction_json, the agent
service generates the schema with a language-model call during create, and this
request is allowed up to 45 seconds instead of the usual 15. Set your client
timeout accordingly.
Deploy it
A DRAFT agent cannot place calls. Move it to ACTIVE with a PATCH — only
the fields you send are changed, and every successful update increments
version.
The lifecycle is DRAFT → ACTIVE → INACTIVE → ARCHIVED, and it is a one-way
gate in one direction: an ACTIVE agent cannot be edited or archived. Set it
back to INACTIVE first, or the request returns 409 with error 1201.
Status values are uppercase on the wire.
Place the call
POST /v2/calls dials immediately. to_number is E.164; omit from_number to
let the telephony service pick a caller ID registered to your organization.
variables fills the {tokens} in the prompt and welcome message. metadata
is merged over variables, so a key present in both takes the metadata
value.
Three outcomes are worth handling explicitly:
| Status | Body | What to do |
|---|---|---|
201 | "status": "queued" | Normal. Poll the execution. |
202 | "status": "scheduled" | Stored and armed for scheduled_at. |
202 | "status": "queued", "dispatch": "pending_confirmation" | The dial reached the telephony service but the acknowledgement did not come back in time. The call may be live. Poll — never redial. |
A 429 here with the message "Account concurrency limit reached" is not the
request rate limit — it means every line your organization is allowed to run
at once is busy. The dial waits a few seconds for a free line before giving
up, and the response carries Retry-After: 5.
Read the execution
The same execution_id answers from "scheduled" through "ringing" and
"in-progress" to a terminal status. Read status and is_terminal; you never
have to branch on which phase the call is in.
Polling
Poll the execution until is_terminal is true. The terminal statuses are:
| Status | Meaning |
|---|---|
completed | The conversation ran to its end. |
failed | The call could not be completed. |
busy | The line was busy. |
no-answer | Nobody picked up. Also the stored status for a voicemail pickup, with answered_by_voicemail set. |
canceled | Cancelled before it dialled. Note the single l. |
stopped | Hung up on request via POST /v2/calls/{id}/stop. |
error | The pipeline errored. |
call-disconnected | The leg dropped. |
balance-low | Stopped because the wallet could not fund it. |
Everything else — queued, scheduled, rescheduled, initiated, ringing,
in-progress — is in flight and is_terminal is false.
Unlike the older dashboard API, GET /v2/calls shows in-flight calls by
default. Omitting status returns everything; pass it only to narrow the
result. It takes a comma-separated list, tolerates both spellings of the
hyphenated values (no_answer / no-answer), and also accepts voicemail,
which is not a status but a flag on no-answer rows.
Where to go next
- Fetch the audio —
GET /v2/calls/{execution_id}/recordingreturns the URL, or a302straight to the file with?redirect=true. The bytes are never proxied through the API. - Dial a list —
POST /v2/batchestakes contacts as JSON, andPOST /v2/batches/uploadtakes a CSV. Batches can be scheduled, paused, resumed and cancelled. - Filter by agent —
GET /v2/agents/{agent_id}/executionsreturns the same execution objects, narrowed to one agent, including calls that agent ran as a resolved A/B variant. - Watch your spend —
GET /v2/usageaggregates calls, connected minutes and cost over a date range.

