> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Voice Skill

> Skill file for agents managing StateSet Voice — voice agents, number routing, calls, transcripts, latency and the MCP server — over the tenant REST API.

## 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.

```bash theme={null}
cd mcp-server && npm install && npm run build   # -> dist/index.js
```

Env (both required):

| Var                       | Value                                                         |
| ------------------------- | ------------------------------------------------------------- |
| `STATESET_VOICE_BASE_URL` | `https://api.voice.stateset.com` (or `http://localhost:5050`) |
| `STATESET_VOICE_API_KEY`  | tenant API key                                                |

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:

```json theme={null}
{ "mcpServers": { "stateset-voice": {
    "command": "node",
    "args": ["/abs/path/rust-phone-server/mcp-server/dist/index.js"],
    "env": { "STATESET_VOICE_BASE_URL": "https://api.voice.stateset.com",
             "STATESET_VOICE_API_KEY": "YOUR_TENANT_API_KEY" } } } }
```

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

```bash theme={null}
curl -s https://api.voice.stateset.com/api/v1/voice/agents \
  -H "Authorization: Bearer $STATESET_VOICE_API_KEY"
```

## Tool ↔ endpoint map

| MCP tool                                                                         | REST                                                                                                                                                          |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_account`                                                                    | `GET /voice/account` (`PATCH` to update profile/greeting/handoff number)                                                                                      |
| `list_agents` / `get_agent`                                                      | `GET /voice/agents`, `GET /voice/agents/{uuid}`                                                                                                               |
| `create_agent`                                                                   | `POST /voice/agents` — `agent_key`, `name`, `config`, `publish`                                                                                               |
| `update_agent`                                                                   | `PATCH /voice/agents/{uuid}` — mints a NEW version from `config`                                                                                              |
| `list_phone_numbers`                                                             | `GET /voice/phone-numbers`                                                                                                                                    |
| `upsert_phone_number`                                                            | `POST /voice/phone-numbers` — `phone_number`, `direction` (`inbound`/`outbound`/`sip_inbound`/`sip_outbound`), `agent_id`                                     |
| `delete_phone_number`                                                            | `DELETE /voice/phone-numbers/{uuid}`                                                                                                                          |
| `search_knowledge`                                                               | `POST /voice/knowledge/search` — `query`, `limit`                                                                                                             |
| `list_knowledge_sources` / `upsert_knowledge_source` / `delete_knowledge_source` | `GET`/`POST /voice/knowledge/sources`, `DELETE /voice/knowledge/sources/{id}` — fast-answer sources (`id`, `answer`, `match_phrases`, `deterministic`)        |
| `list_webhook_endpoints` / `upsert_webhook_endpoint`                             | `GET`/`POST /webhooks/endpoints` — `url`, `secret`, `events[]` (empty list = all events); `POST /webhooks/endpoints/{id}/test`                                |
| `list_api_keys` / `create_api_key` / `revoke_api_key`                            | `GET`/`POST /api-keys`, `DELETE /api-keys/{uuid}`                                                                                                             |
| `create_realtime_token`                                                          | `POST /voice/realtime/token` — short-lived observer WS token                                                                                                  |
| `create_web_call`                                                                | `POST /voice/web-calls` — signed WS URL + start message, no phone number                                                                                      |
| `make_call`                                                                      | `POST /make-call` — REAL outbound PSTN call; `POST /voice/calls` is the same hardened flow with extras                                                        |
| `list_call_logs` / `get_call_log`                                                | `GET /call-logs` (filter by `call_sid`, `status`, `outcome`, `direction`, number, date, transcript text), `GET /call-logs/{uuid}` — full transcript + summary |
| `get_call_stats` / `get_call_latency`                                            | `GET /call-logs/stats`, `GET /call-logs/latency` — p50/p95 voice latency                                                                                      |

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.**

```json theme={null}
{ "agent_key": "front-desk", "name": "Front Desk", "publish": true,
  "config": {
    "agent_name": "Front Desk",
    "greeting": "Thanks for calling Acme Dental. How can I help?",
    "instructions": "Answer questions, book appointments, escalate urgent concerns.",
    "realtime_model": "gpt-4o-realtime-preview",
    "chat_model": "gpt-4o-mini",
    "voice": "alloy",
    "tools": [ { "name": "get_order_status",
      "description": "Look up an order.",
      "parameters": { "type": "object", "required": ["order_id"],
        "properties": { "order_id": { "type": "string" } } },
      "endpoint": { "method": "POST",
        "url": "https://api.acme.example/voice/get-order-status",
        "auth_header": "Bearer $TOKEN", "timeout_ms": 3000 } } ] } }
```

`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_source` → `search_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

* [Voice API reference](/api-reference/voice/overview) — the 65 tenant endpoints
* [Voice MCP server](/stateset-voice/mcp-server) — the 24 tools
* [Voice webhooks](/stateset-voice/webhooks)
