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
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 The response is the whole contract: open
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)
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.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. Call
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.+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
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 then fetch it whole — Then ask the question every voice deployment eventually asks — how fast does it feel?Two numbers matter: LLM first token (model think time) and end-to-end first audio
(what the caller actually waits through).
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):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.Response (stats excerpt)
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
400 creating the agent, and the message names a field I didn't think I sent
400 creating the agent, and the message names a field I didn't think I sent
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.The web call worked, then the same websocket_url stopped connecting
The web call worked, then the same websocket_url stopped connecting
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 or 409 on POST /voice/calls
403 or 409 on POST /voice/calls
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.I patched the agent but calls still use the old prompt
I patched the agent but calls still use the old prompt
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.