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 refuses those outright, 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, the instructions it follows, the line it opens with, and how it
hears, speaks and thinks. display_name is the only required field — everything else
can be added later with a PATCH. The body uses the same shape
GET /v2/agents/{agent_id} returns, so what you send is what you will read back.
Anything in {braces} in the prompt or the greeting becomes a variable you fill per
call in step 4.
Three things in that response are worth pointing at now, because they surprise people later:
versionis2, not1. A new agent starts at version 2, and every successful update increments it. Your body is applied exactly as aPATCHwould apply it, so a field means the same thing on create as on update.variablesis read from the text. It lists the placeholders the prompt and the greeting actually contain, as plain names, every time the agent is read. To add a variable, write it into the text.- What you did not send is filled in. The policy objects under
configurationand the flatconversation_configcome back with defaults; change any of them later with aPATCHthat sends only that control.
A field this API does not know, at the top level of the body, is refused with 422
and error 1001 naming it. Inside configuration a provider's own options are
passed through, so a misspelled key at that depth is not caught — read the agent
back with GET /v2/agents/{agent_id} to confirm what was stored.
Creating an agent takes longer than most requests, and configuring extraction can need a language-model call, so allow this request up to two minutes before your client gives up — not the usual 15 seconds.
Deploy it
Placing a call does not check the agent's status: a DRAFT agent can already be dialled in step 4. What
ACTIVE does is mark the agent live and lock its configuration, so nothing edits a
running agent by accident. 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. An ACTIVE agent cannot be
edited or archived: switch it off with {"status": "INACTIVE"} on its own, make
your change, and set it back to ACTIVE. Archiving it with
DELETE /v2/agents/{agent_id} needs INACTIVE first too, or the request returns
409 with error 1201. Status values come back uppercase.
Place the call
POST /v2/calls dials immediately. to_number is E.164; omit from_number to
let Graine pick a caller ID registered to your organization.
variables fills the {tokens} in the prompt and the greeting. metadata
is merged over variables, so a key present in both takes the metadata
value. An app talking to the same agent sends the same names through its SDK —
Send your user's details to the agent.
Five 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": "scheduled", "deferred_by_calling_guard": true | The agent's calling window was shut; the call rings at scheduled_at, inside the window. Do not resend it. |
202 | "status": "scheduled", "reason": "waiting_for_a_free_line" | Every line was busy, so the call is queued and dials the moment one frees. Do not resend it. |
202 | "status": "queued", "dispatch": "pending_confirmation" | The dial was sent, but its confirmation 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 and the call could not be queued for one. It 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 usual stored status for a voicemail pickup, with answered_by_voicemail set. |
canceled | Cancelled before it dialled, or stopped with POST /v2/calls/{id}/stop. Note the single l. |
stopped | Kept for compatibility; no call placed today ends in it. |
error | An error ended the call. |
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. A call the person
answers but the agent never joins ends failed, with an error.message
that begins agent_unavailable.
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 — usually set on a no-answer row.
Where to go next
- Fetch the audio —
GET /v2/calls/{execution_id}/recordingreturns the URL, or a302straight to the file with?redirect=true. The audio itself never comes through the API. - Dial a list —
POST /v2/campaignstakes the contacts as JSON or a CSV, with the calling policy — caller IDs, calling hours, retries and follow-ups — in the same request. Campaigns can be paused, resumed and cancelled. - Filter by agent —
GET /v2/agents/{agent_id}/executionsreturns the same execution objects, narrowed to one agent, including calls it served as the variant in an A/B test. - Watch your spend —
GET /v2/usageaggregates calls, connected minutes and cost over a date range.

