Skip to main content
This guide takes one agent — a front desk for a store called acme-outdoors — from a JSON config to a real phone call. The order is deliberate: you talk to the agent in your browser first, with no phone number, no Twilio and no per-minute PSTN charges, and only then attach a number and dial out. The Build Your First Voice Agent quickstart covers the wider setup (tenant config, webhooks, live supervision); this guide is the shortest honest path from key to conversation.

Before you start

Keys are minted with POST /api-keys ({ "label": "front-desk-prod" }) and the response’s token is returned once — only a hash is stored, and GET /api-keys never shows it again. A lost key is deleted and re-minted, not recovered. The examples assume yours is exported as $STATESET_VOICE_API_KEY.
1

Create the front-desk agent

POST /voice/agents takes an agent_key (your stable handle), name, an optional description, a config, and publish — with publish: true version 1 goes live in the same call. Inside config: agent_name, greeting, instructions, realtime_model, chat_model, voice, and optional tools[] — JSON-schema functions fulfilled by your HTTPS backend, invoked mid-call.
Response
Keep agent.id — it is $AGENT_ID below. Validation is eager and strict: an unknown key anywhere in config is a 400, never silently dropped, and each tool must pass the same gate (name charset, object JSON schema, POST-only public-HTTPS endpoint, 250–10000 ms timeout, at most 16 tools). Mid-call the server POSTs {tool, arguments, call} to your endpoint with an X-Webhook-Signature HMAC — verify it exactly like a webhook; the JSON reply becomes the tool result the model sees.
Agents are versioned. PATCH /voice/agents/{agent_id} mints a new version, and that version is not live unless you send publish: true (or publish it later). If an edit seems to have no effect on calls, you shipped a draft — check which version is published before rewriting the prompt.
2

Talk to it in your browser — no phone number yet

This is the magic moment, and it needs nothing you don’t already have. Browsers speak the identical media-stream protocol Twilio does, so POST /voice/web-calls gives a web page the entire pipeline — this agent, its tools, transfers, evals, billing — with call_type: "web". All body fields are optional: agent_key, agent_version_id (pin an exact version), greeting.
Response (201)
The response is the whole contract: open websocket_url, send start_message verbatim (the signed stream_token inside it is the entire auth story — browsers cannot set WebSocket headers), then stream 20 ms base64 ulaw_8000 mic frames in the message_shape and play the ones you receive. Honor the clear event by flushing playback, or the agent talks over the caller after barge-in. Send stop_message to hang up. The Web SDK page has a complete dependency-free client — mic capture, µ-law codec, paced playback, barge-in — you can paste into a page and click Talk.
The token expires in ~5 minutes: mint per call, never cache a websocket_url. And your tenant key must never reach the browser — mint the web call from your backend and forward the response body to the page. A 503 here means the server has no STREAM_AUTH_SECRET to sign tokens with.
Talk to it. Ask for order A-1207 and watch your tool endpoint get hit. Iterate on the prompt with PATCH + publish until it sounds right — each browser call is the same pipeline the phone will use, so what you hear is what callers get.
3

Attach a phone number

A phone-number route maps a number to an agent, and inbound Twilio webhooks use it to resolve the owning tenant and agent deterministically. direction is one of inbound, outbound, sip_inbound, sip_outbound; agent_version_id optionally pins the route to an exact version, and greeting overrides the agent’s own for this number.
Call +1 555 555 0142 from your own phone: the front desk answers with that greeting. GET /voice/phone-numbers lists routes, PATCH/DELETE /voice/phone-numbers/{route_id} re-point or remove one — re-pointing a route is how you roll a new agent version out to a number, or back.
4

Place a real outbound call, behind the gates

POST /voice/calls dials a real phone through the hardened outbound flow. Only to is required; from defaults to the tenant’s number. agent_key resolves the currently published version, agent_name and greeting override presentation for this call, machine_detection is off (default), enable, or detect_message_end, and max_attempts (1–5) with retry_on (no_answer, busy, failed, voicemail) and retry_backoff_seconds (0–3600) make the platform redial on your terms. metadata (a JSON object ≤ 4 KB) is persisted on the attempt and inherited by retry children, so campaign attribution survives the whole chain; org_id tags multi-org tenants. Two fields are mutually exclusive with each other’s plane: agent_version_id pins a version, while config carries a transient inline agent (same shape and validation as step 1, custom tools included) for this call only.
Response
This rings a real phone, so two safety gates run before Twilio is touched and a blocked call answers 403 instead of dialling: the tenant’s do-not-call list, and TCPA quiet hours — evaluated in the called party’s timezone (derived from the NANP number), not yours, so a compliant-looking 2 pm call from your office can still be blocked at a 7 am destination. The Idempotency-Key is the third gate: the same key within 24 hours replays the stored outcome instead of dialling again, a concurrent duplicate gets 409 (retry shortly), and a deployment without a database answers 503 rather than risk a silent duplicate call. Derive the key from the thing you’re calling about, not from a timestamp.
ok: true, status: "queued" means the call was placed, not answered — the transcript in the next step is where the outcome lands, and answered_by records what machine detection concluded.
5

Read back the transcript and the latency

Call logs are the durable record. Find the call by its call_sid (other filters: status, outcome, direction, from_number/to_number, escalated, from_date/to_date, min_duration/max_duration, transcript_search, plus limit/offset or keyset cursor pagination):
then fetch it whole — GET /call-logs/{id} returns status, direction, duration, the full turn-by-turn transcript, the summary, the normalized outcome (answered, voicemail, no_answer, busy, failed), answered_by, every function_calls entry your tool endpoint served, escalation fields and the recording_url when recording is on.
Then ask the question every voice deployment eventually asks — how fast does it feel?
Response (stats excerpt)
Two numbers matter: LLM first token (model think time) and end-to-end first audio (what the caller actually waits through). window accepts 24h, 7d, 30d or all, and slowest hands you the exact calls to open when a percentile drifts — Operations covers what to do about each cause.

What you built

Troubleshooting

config is validated strictly: an unknown or misspelled key is rejected, never dropped, so a typo like instrutions is a 400 instead of an agent that ignores half its prompt. Tools fail the same way for a non-object JSON schema, a non-HTTPS or non-public endpoint URL, a method other than POST, a timeout_ms outside 250–10000, or a seventeenth tool. A tool whose name collides with a built-in platform function is accepted but ignored in favor of the built-in — rename yours.
That is the design: the signed token in the URL and start_message expires after about five minutes (expires_at says exactly when). Mint a fresh call per click. If the mint itself returns 503, the server has no STREAM_AUTH_SECRET. If audio connects but the agent constantly interrupts itself, enable browser echo cancellation — without it the agent’s own voice loops back in as caller speech — and remember browsers only start audio after a user gesture.
403 is a policy block doing its job — the destination is on your do-not-call list, or it is outside TCPA quiet hours in the called party’s timezone. Don’t retry a 403 on a timer with the same intent; queue it for permitted hours. 409 means the same Idempotency-Key is in flight concurrently: wait briefly and retry, and the replayed outcome comes back instead of a second dial. A 503 on placement means the deployment has no database to record idempotency in — it refuses rather than risk a duplicate call.
PATCH /voice/agents/{agent_id} created a draft version; calls resolve the published one (or whatever agent_version_id a route or call pinned). Publish the new version, and check your phone-number route isn’t pinned to the old id. Inline config on a single call is different again — it is recorded as a draft under the reserved transient-inline agent and touches nothing else.

Next steps

Web SDK

The complete dependency-free browser client for step 2 — µ-law codec, paced playback, barge-in.

Webhooks

Get voice.call.ended and voice.session.completed pushed to you instead of polling call logs.

Voice API reference

The 65 generated tenant endpoints with live playgrounds — calls, sessions, outcomes, automations.

Live supervision

Monitor, whisper, barge, escalate or end the call you just placed, while it is still running.
Last modified on August 31, 2026