Skip to main content

Overview

stateset-voice is StateSet’s AI phone platform: a Rust/axum server that answers and places Twilio calls with realtime-model voice agents, plus browser web calls over the identical media pipeline. Tenants drive it through a versioned REST API (/api/v1/...) with a single tenant API key, or through the bundled MCP server that wraps that API 1:1.
  • Prod (default target): https://api.voice.stateset.com (API docs also reference https://voice.stateset.com)
  • Local dev: http://localhost:5050
  • Auth: Authorization: Bearer stsk_<tenant>_<key-id>_<secret> (tenant API key)

Two ways in

1. MCP server (mcp-server/, package @stateset/voice-mcp-server)

Thin, stateless stdio server; every tool is one REST call with the configured key passed through as Bearer.
Env (both required): Wire into a client (claude mcp add, or MCP config JSON). The package is not yet on npm; build it from the repository’s mcp-server/ directory:
Smoke test: pipe initialize / notifications/initialized / tools/list JSON-RPC lines into node dist/index.js (exact lines in mcp-server/README.md). A tools/call with a bad key returns a clean HTTP 401: Invalid tenant credentials — path works, key doesn’t.

2. Raw REST

Tool ↔ endpoint map

Not wrapped by MCP but in the REST API: voice sessions (/voice/sessions...), live-session supervisor actions (monitor/whisper/barge/escalate/end on /voice/sessions/{stream_sid}/actions/*), and translation calls (POST /voice/translation-calls).

Agent config schema

Accepted keys: instructions, assistant_profile, realtime_model, chat_model, voice, greeting, agent_name, tools[]. Unknown keys are rejected with a 400 — never silently dropped.
tools[] are customer functions fulfilled by YOUR backend: mid-call the server POSTs {tool, arguments, call} to endpoint.url (HMAC X-Webhook-Signature); the JSON reply is the tool result. Validation is eager: POST-only, public-https URL, 250–10000 ms timeout, max 16 tools; names colliding with built-in platform functions are ignored in favor of the built-in.

Canonical flows

Agent → number → call → transcript
  1. create_agent with publish: true (response includes agent + version UUIDs).
  2. upsert_phone_number {phone_number, direction: "inbound"|"outbound", agent_id}.
  3. make_call (or POST /voice/calls) → returns call_sid.
  4. list_call_logs {call_sid}get_call_log {uuid} for full transcript + summary.
Outbound extras on POST /voice/calls:
  • Idempotency-Key header — same key within 24 h replays the stored outcome instead of dialing again; a concurrent duplicate gets 409 (retry shortly). On a deployment without a database the header itself returns 503 — the server never silently places a duplicate call.
  • config body — transient inline agent config for this call only (mutually exclusive with agent_version_id).
  • metadata (≤ 4 KB JSON) — persisted and inherited by retry children.
  • machine_detection: off | enable | detect_message_end; retries via max_attempts, retry_on: ["no_answer","busy"], retry_backoff_seconds.
Web call (browser test, no phone number): create_web_call → short-lived signed websocket_url + ready-to-send start_message (embedded stream_token is the whole auth story), audio ulaw_8000 20 ms base64 frames, expires_at ~5 min — mint one per call. Requires server STREAM_AUTH_SECRET. Full browser client + wire protocol: docs/sdk-web.md in the repo. Knowledge: upsert_knowledge_sourcesearch_knowledge {query}; sources are deterministic low-latency fast answers used before broader search.

Gotchas

  • make_call dials a real phone. DNC and TCPA quiet-hours gates apply — quiet hours evaluated in the called party’s timezone (NANP); blocked calls return 403 (policy) or 503.
  • create_api_key returns the secret token exactly once; store it immediately. revoke_api_key is irreversible.
  • Agents are versioned. PATCH /voice/agents/{id} creates a new version; it’s not live unless publish: true (or published later).
  • Unknown/invalid agent config → 400, not silently ignored.
  • Voice lag? Start at get_call_latency (p50/p95), then per-call get_call_log.
  • Errors are { "ok": false, "error": "..." }; common: 400 validation, 401 auth, 403 policy block (DNC/quiet hours), 409 duplicate, 503 missing backend (no DB, no STREAM_AUTH_SECRET, no Twilio creds).

Further reading