{"openapi":"3.1.0","info":{"title":"PyAI API","version":"1.0.0","description":"Telephony-native Voice AI behind one bearer key:\n\n- **Hear** — speech-to-text · `POST /v1/audio/transcriptions` (streaming + batch)\n- **Speak** — text-to-speech, stock voices & voice cloning · `POST /v1/audio/speech`, `GET /v1/voices`, `/v1/voice/clones`\n- **Cue** — streaming turn detection + knowledge-base context for your own LLM/voice pipeline · `GET /v1/audio/transcriptions/stream` with grounding\n- **Omni** — full-duplex agentic voice (speech-to-speech, grounded in your knowledge bases + tools) · `/v1/omni` (and the OpenAI-compatible `/v1/realtime`)\n- **Agents** — managed Omni platform (orchestration, tools, memory). _Coming soon._\n\n## Authentication\n\nCreate a key in the [console](https://console.pyai.com) (it is shown once) and send it as a bearer token:\n\n```\nAuthorization: Bearer pyai_live_...\n```\n\nKeys are environment-scoped: `pyai_live_...` (production) and `pyai_test_...` (sandbox). New accounts include $10 of credits.\n\nKeys are self-validating signed tokens: they work on every PyAI surface the instant they are created — no activation or propagation delay. Treat them as opaque strings (up to 512 chars) and never parse their contents.\n\nWebSocket endpoints can't use request headers from a browser, so pass the key as a **subprotocol** instead:\n\n```\nSec-WebSocket-Protocol: pyai-key.pyai_live_...\n```\n\n(server-side clients may instead append `?api_key=...` to the URL). The gateway authenticates the key on the WebSocket upgrade and swaps it for the internal upstream credential — your key never reaches the model, and the model's key never reaches you.\n\n## Quickstart — Hear (speech-to-text)\n\n```\ncurl https://api.pyai.com/v1/audio/transcriptions \\\n  -H \"Authorization: Bearer $PYAI_API_KEY\" \\\n  -F file=@audio.wav -F model=pyai-hear\n# -> { \"text\": \"...\" }\n```\n\n## Quickstart — Speak (text-to-speech)\n\n```\ncurl https://api.pyai.com/v1/audio/speech \\\n  -H \"Authorization: Bearer $PYAI_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"pyai-voice\",\"input\":\"Hello from PyAI.\",\"voice\":\"voice_abc\"}' \\\n  --output speech.wav\n```\n\n`voice` is a stock voice id from `GET /v1/voices` (38 prebuilt voices with personas, avatars, and previews) or a cloned voice id from `/v1/voice/clones`. Omit it to use your account's default voice.\n\n## Quickstart — Omni (realtime voice agent)\n\nOmni drives an **agent** (persona + knowledge bases + tools) that you create in the console's Agent Builder. Open a WebSocket, pass your key as a subprotocol, and name the `agent_id` to drive:\n\n```\nwss://api.pyai.com/v1/omni?agent_id=agent_123&format=pcm16&rate=24000\n  Sec-WebSocket-Protocol: pyai-key.$PYAI_API_KEY\n```\n\n`agent_id` is an opaque label for the agent you're driving — the session is authorized by your key's organization and `agent_id` is echoed to your own knowledge endpoint, so any id in your org's namespace is accepted (PyAI stores no per-agent state). `format` and `rate` are sent on the connect URL (load-bearing; the SDK sets them). Send PCM16 audio as binary frames and receive the agent's speech the same way. The OpenAI-realtime-compatible surface (`/v1/realtime`) is served by the same Omni engine; new integrations should prefer `/v1/omni`. (_Flow, the legacy voice-duplex engine, is retired for new customers; its `/v1/realtime` alias now routes to Omni._)\n\nFor reproducible eval runs, determinism controls (`seed`/`temperature`) ride the Omni session's `configure` frame, which the gateway passes through unchanged — they are honored once the engine supports them (PLATFORM_ASK_EVALS_ENGINE); no platform change is required.\n\n## Scopes\n\n| Scope | Grants |\n| --- | --- |\n| `hear:transcribe` | `POST /v1/audio/transcriptions` |\n| `hear:stream` | `GET /v1/audio/transcriptions/stream` (WebSocket) |\n| `voice:synthesize` | `POST /v1/audio/speech` (Speak) |\n| `voice:clone` | `/v1/voice/clones` (Speak) |\n| `voice:design` | `/v1/voice/design` (Speak) |\n| `flow:session` | _legacy_ — `/v1/realtime` for existing Flow customers; new traffic on `/v1/realtime` uses Omni |\n| `omni:session` | `/v1/omni` (native) and `/v1/realtime` (Omni) |\n| `transcribe:jobs` | `/v1/transcription/jobs` |\n| `trace:configure` | `/v1/trace/config`, `/v1/trace/rule-packs` (Trace management) |\n| `trace:read` | `/v1/trace/interactions`, `/violations`, `/findings`, `/exposure` (Trace reads) |\n| `recap:configure` | `/v1/recap/config` (Recap management) |\n| `recap:configure` | `/v1/recap/crm-config` (Salesforce field mapping) |\n| `recap:read` | `/v1/recap/calls` (Recap reads) |\n| `telephony:manage` | `/v1/telephony/*` (managed numbers) |\n\n`GET /v1/models`, `GET /v1/voices`, and `GET /v1/me` need no specific scope — any active key may call them. Wildcards (`hear:*`, `voice:*`, …, and the global `*`) grant every scope in their family.\n\n## Canonical endpoints\n\nOne row per product surface — endpoint, auth, required scope, and lifecycle status. **live** = generally available; **deprecated** = works during a migration window (don't build new on it); **legacy** = supported for existing customers only; **planned** = not yet available.\n\n| Product | Endpoint | Auth | Scope | Status |\n| --- | --- | --- | --- | --- |\n| Identity | `GET /v1/me` | Bearer | _any active key_ | live |\n| Models | `GET /v1/models` | Bearer | _any active key_ | live |\n| Voices | `GET /v1/voices`, `GET /v1/voices/{id}` | Bearer | _any active key_ | live |\n| Hear (batch) | `POST /v1/audio/transcriptions` | Bearer | `hear:transcribe` | live |\n| Hear (streaming) | `GET /v1/audio/transcriptions/stream` (WS) | Subprotocol | `hear:stream` | live |\n| Cue | `GET /v1/audio/transcriptions/stream` + grounding (WS) | Subprotocol | `hear:stream` | live |\n| Hear (async batch) | `POST`/`GET /v1/transcription/jobs` | Bearer | `transcribe:jobs` | live |\n| Speak (TTS) | `POST /v1/audio/speech` | Bearer | `voice:synthesize` | live |\n| Speak (cloning) | `GET`/`POST /v1/voice/clones` | Bearer | `voice:clone` | live |\n| Speak (design) | `/v1/voice/design` | Bearer | `voice:design` | live |\n| Omni (native) | `wss …/v1/omni?agent_id=` | Subprotocol | `omni:session` | live |\n| Omni (OpenAI-compat) | `wss …/v1/realtime?model=pyai-omni-realtime` | Subprotocol | `omni:session` | live |\n| Omni (alias) | `wss …/v2/omni/chat` | Subprotocol | `omni:session` | deprecated |\n| Flow | `wss …/v1/realtime?model=pyai-flow-realtime` | Subprotocol | `flow:session` | legacy |\n| Agents (config) | `/v1/agents`, `/v1/agents/{id}` | Bearer | `omni:session` | live |\n| Trace (config) | `/v1/trace/config`, `/v1/trace/rule-packs` | Bearer | `trace:configure` | live |\n| Trace (reads) | `/v1/trace/interactions`, `/violations`, `/findings`, `/exposure` | Bearer | `trace:read` | live |\n| Recap (config) | `/v1/recap/config` | Bearer | `recap:configure` | live |\n| Recap (CRM) | `/v1/recap/crm-config` | Bearer | `recap:configure` | live |\n| Recap (reads) | `/v1/recap/calls` | Bearer | `recap:read` | live |\n| Telephony | `/v1/telephony/*` | Bearer | `telephony:manage` | live |\n| Agents (managed) | _coming soon_ | — | — | planned |\n\nWebSocket surfaces authenticate with the `Sec-WebSocket-Protocol: pyai-key.<API_KEY>` subprotocol (or `?api_key=` server-side); everything else takes the `Authorization: Bearer` key. Telephony's carrier-backed calls (search/provision/release) return 404 until a carrier is configured for the account.\n\n## Rate limits & billing\n\nEvery key has a per-second rate limit (with burst) and a cap on concurrent realtime sessions. Exceeding either returns `429` with a `Retry-After` header. Usage is metered per minute of audio — transcription minutes (Hear), synthesized audio minutes (Speak), and realtime session minutes (Cue, Omni) — and billed against your plan and credits. List prices: Hear $0.003/min (batch $0.0015/min), Speak $0.06/min streaming ($0.04/min async), Cue $0.015/min, Omni $0.05/min, Agents $0.10/min. Billing rounds to a 1-minute pulse by default (per-second available on request); per-character Speak billing is available on enterprise contracts.","contact":{"name":"PyAI","url":"https://pyai.com"}},"servers":[{"url":"https://api.pyai.com","description":"Production"}],"security":[{"apiKey":[]},{"xApiKey":[]}],"tags":[{"name":"Identity","description":"Introspect the calling key: org/project, env, granted scopes, and limits/credit posture. Use it to self-diagnose a 401/403/402."},{"name":"Hear","description":"Speech-to-text (streaming + batch)"},{"name":"Speak","description":"Text-to-speech and voice cloning"},{"name":"Realtime","description":"Full-duplex WebSocket sessions: Omni (agentic speech-to-speech with knowledge bases + tools). The legacy Flow engine is retired for new customers; its /v1/realtime alias routes to Omni."},{"name":"Models","description":"Model catalog"},{"name":"Transcription Jobs","description":"Async batch transcription"},{"name":"Agents","description":"Voice-agent configuration (persona, greeting, voice, conversation knobs)"},{"name":"Trace","description":"Compliance & guardrails: per-agent config, rule packs, and the exposure / violations / interaction-evidence read views"},{"name":"Telephony","description":"Managed phone numbers: search, provision, route to an agent, and release. Call minutes bill on telephony.minutes ($0.01/min)."}],"paths":{"/v1/me":{"get":{"tags":["Identity"],"summary":"Introspect the calling key (whoami)","description":"Return the identity the gateway resolved for your key: `key_id`, `org_id`, `project_id`, environment (`test`/`live`), `status`, the granted `scopes`, and the rate-limit/credit posture. Any active key may call it (no special scope), so it is the fastest way to self-diagnose a `403 insufficient_scope` (check `scopes`), a `402 credit_exhausted`/`key_budget_exceeded` (check `credit` and `key_budget_cents`), or an `org_suspended` (`org_status`).","operationId":"getMe","responses":{"200":{"description":"The calling key's identity and limits.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Identity"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/models":{"get":{"tags":["Models"],"summary":"List available models","operationId":"listModels","responses":{"200":{"description":"Model catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelList"}}}}}}},"/v1/audio/transcriptions":{"post":{"tags":["Hear"],"summary":"Transcribe audio","description":"Whisper-compatible transcription. Requires the `hear:transcribe` scope.","operationId":"createTranscription","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Audio file (wav, mp3, m4a, flac, ogg)."},"model":{"type":"string","default":"pyai-hear"},"response_format":{"type":"string","enum":["json","text","verbose_json"],"default":"json"},"language":{"type":"string","description":"ISO-639-1 hint, e.g. 'en'."},"seed":{"type":"integer","description":"Optional determinism seed for reproducible eval runs. Forwarded to the engine and honored once the engine supports it (PLATFORM_ASK_EVALS_ENGINE); no effect when omitted."},"temperature":{"type":"number","description":"Optional sampling temperature for reproducible eval runs. Forwarded to the engine and honored once the engine supports it (PLATFORM_ASK_EVALS_ENGINE); no effect when omitted."}}}}}},"responses":{"200":{"description":"Transcription result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transcription"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/audio/transcriptions/stream":{"get":{"tags":["Hear"],"summary":"Stream transcription (WebSocket)","description":"Upgrade to a WebSocket for low-latency streaming speech-to-text with eager partials (first partial typically within ~300 ms). Transcription-only — distinct from `/v1/realtime` (Omni duplex). Requires the `hear:stream` scope. With knowledge-base grounding enabled this surface becomes **Cue** (turn detection + retrieved context for your own LLM/voice pipeline), billed at the Cue rate.\n\n**Auth:** browsers can't set `Authorization` on a WebSocket, so send the key as a subprotocol: `Sec-WebSocket-Protocol: pyai-key.<API_KEY>` (server clients may use `?api_key=` instead). The key is validated and swapped for the internal upstream credential on the upgrade.\n\n**Client -> server:** stream binary audio frames continuously (PCM16 at the negotiated `sample_rate`, or opus). Send a JSON `{\"type\":\"commit\"}` text frame to force-finalize the current utterance (e.g. when your VAD detects end-of-turn); closing the socket also flushes a final for any buffered audio.\n\n**Server -> client (JSON text frames):**\n\n| `type` | When | Payload |\n| --- | --- | --- |\n| `partial` | every eager tick | `{text, stable_text, active_text, utterance_id, t_ms}` — the live hypothesis for that `utterance_id` |\n| `speech_final` | on endpoint/commit | `{text, utterance_id, t_ms, audio_ms}` — stable, end of an utterance |\n| `final` | follows `speech_final` | `{text, utterance_id, t_ms, audio_ms}` — corrected full-context transcript |\n| `error` | on fault | `{code, message}` |\n\n`t_ms` is the audio-timeline position of the hypothesis; `audio_ms` is the utterance's active-speech length (the billed signal); `utterance_id` groups partials/finals for one utterance. With grounding enabled (Cue), `speech_final`/`final` frames also carry a `grounding` array: `[{content, score}, ...]` (top-3 KB passages, `[]` when no KB is bound or retrieval times out).\n\n**Close codes:** `1000` normal · `1008` auth/policy (bad key, scope, revoked token) · `1011` engine error · `4429` over concurrency cap.\n\n**Billing:** metered active audio at the Hear rate ($0.003/min) — speech time derived from the transcript timing, not connection wall-clock. With grounding enabled (Cue), the session bills a single `cue.minutes` line at $0.015/min instead.","operationId":"openTranscriptionStream","parameters":[{"name":"model","in":"query","required":false,"schema":{"type":"string","default":"pyai-hear"},"description":"Streaming STT model."},{"name":"language","in":"query","required":false,"schema":{"type":"string"},"description":"ISO-639-1 hint, e.g. 'en'."},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","default":16000},"description":"Input PCM sample rate in Hz."},{"name":"encoding","in":"query","required":false,"schema":{"type":"string","enum":["pcm16","opus"],"default":"pcm16"},"description":"Audio frame encoding."},{"name":"interim_results","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Emit eager partial hypotheses."},{"name":"seed","in":"query","required":false,"schema":{"type":"integer"},"description":"Optional determinism seed for reproducible eval runs. Forwarded to the engine and honored once the engine supports it (PLATFORM_ASK_EVALS_ENGINE); no effect when omitted."},{"name":"temperature","in":"query","required":false,"schema":{"type":"number"},"description":"Optional sampling temperature for reproducible eval runs. Forwarded to the engine and honored once the engine supports it (PLATFORM_ASK_EVALS_ENGINE); no effect when omitted."}],"responses":{"101":{"description":"Switching Protocols — the streaming transcription WebSocket is open."},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/audio/speech":{"post":{"tags":["Speak"],"summary":"Synthesize speech","description":"OpenAI-compatible text-to-speech. Returns audio bytes. Requires the `voice:synthesize` scope.","operationId":"createSpeech","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["input"],"properties":{"model":{"type":"string","default":"pyai-voice"},"input":{"type":"string","description":"Text to synthesize."},"voice":{"type":"string","description":"A stock voice id from `GET /v1/voices` (e.g. `stock_emma_en_gb`) or a cloned voice id (e.g. `voice_abc`) created via `/v1/voice/clones`. Omit to use the account's default voice."},"response_format":{"type":"string","enum":["wav","mp3","opus","aac","flac","pcm","g711_ulaw","g711_alaw"],"default":"mp3","description":"Output audio format. The response `Content-Type` varies by format (`audio/wav`, `audio/mpeg`, `audio/ogg`, `audio/aac`, `audio/flac`, `audio/pcm`, `audio/basic`). `pcm` returns raw, headerless 16-bit little-endian mono samples (no container) at `sample_rate` — the format voice-agent orchestrators (e.g. Vapi custom-voice, LiveKit/Pipecat) feed directly into their pipelines. `g711_ulaw`/`g711_alaw` return raw, headerless G.711 telephony audio at a fixed 8 kHz mono (for Twilio/Plivo/FreeSWITCH); `sample_rate` does not apply and is rejected unless set to `8000`."},"sample_rate":{"type":"integer","minimum":8000,"maximum":48000,"description":"Optional output sample rate in Hz (8000-48000), e.g. `8000`/`16000` for telephony or `24000` for wideband. Omit to use the native 24 kHz. Most relevant with `response_format: pcm`. Does not apply to `g711_ulaw`/`g711_alaw`, which are always 8 kHz mono (a conflicting value is rejected)."},"speed":{"type":"number","default":1},"seed":{"type":"integer","description":"Optional determinism seed for reproducible eval runs. Forwarded to the engine and honored once the engine supports it (PLATFORM_ASK_EVALS_ENGINE); no effect when omitted."},"temperature":{"type":"number","description":"Optional sampling temperature for reproducible eval runs. Forwarded to the engine and honored once the engine supports it (PLATFORM_ASK_EVALS_ENGINE); no effect when omitted."}}}}}},"responses":{"200":{"description":"Audio stream. The `Content-Type` varies by `response_format`: `audio/wav` (wav), `audio/mpeg` (mp3), `audio/ogg` (opus), `audio/aac` (aac), `audio/flac` (flac), `audio/pcm` (pcm — raw/headerless), and `audio/basic` (g711_ulaw/g711_alaw — raw/headerless G.711 at 8 kHz mono).","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}},"audio/mpeg":{"schema":{"type":"string","format":"binary"}},"audio/ogg":{"schema":{"type":"string","format":"binary"}},"audio/aac":{"schema":{"type":"string","format":"binary"}},"audio/flac":{"schema":{"type":"string","format":"binary"}},"audio/pcm":{"schema":{"type":"string","format":"binary"}},"audio/basic":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/voices":{"get":{"tags":["Speak"],"summary":"List voices","description":"Your unified voice library: the prebuilt PyAI catalog (each with a persona, avatar, and audio preview) merged with your saved **designed** voices, each tagged by `source` (`stock` | `design`). Any active key may read it (no specific scope). Use a `voice_id` from here as the `voice` in `POST /v1/audio/speech` or on an agent. Cloned voices are listed separately via `GET /v1/voice/clones`.","operationId":"listVoices","parameters":[{"name":"gender","in":"query","required":false,"schema":{"type":"string","enum":["M","F"]},"description":"Filter stock voices by gender (case-insensitive exact match)."},{"name":"region","in":"query","required":false,"schema":{"type":"string"},"description":"Filter stock voices by region/accent (case-insensitive substring match, e.g. `us`, `india`, `scotland`)."},{"name":"source","in":"query","required":false,"schema":{"type":"string","enum":["stock","design"]},"description":"Return only one kind of voice. Omit for the merged library."}],"responses":{"200":{"description":"Voice library (stock + designed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StockVoiceList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/voices/{id}":{"get":{"tags":["Speak"],"summary":"Get a stock voice","operationId":"getVoice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stock voice id, e.g. `stock_emma_en_gb`."}],"responses":{"200":{"description":"Stock voice","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StockVoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such stock voice"}}},"delete":{"tags":["Speak"],"summary":"Delete a designed voice","description":"Remove a saved designed (prompt-to-voice) voice, freeing a library-cap slot. Tenant-isolated — you can only delete your own (otherwise `404`). Stock voices can't be deleted; cloned voices delete via `DELETE /v1/voice/clones/{id}`.","operationId":"deleteVoice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Designed voice id, e.g. `vd_7h16k`."}],"responses":{"200":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such designed voice for this tenant"}}}},"/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","description":"All active agents in your organization. Requires the `omni:session` scope.","operationId":"listAgents","responses":{"200":{"description":"Agents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Agents"],"summary":"Create an agent","description":"Create a voice agent (persona, greeting, voice, conversation knobs). Drive it with `wss://api.pyai.com/v1/omni?agent_id=agent_…`; per-call headers (`X-PyAI-Voice`, `X-PyAI-Persona`) override agent config. Requires the `omni:session` scope.","operationId":"createAgent","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentConfig"}}}},"responses":{"201":{"description":"Created agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"400":{"description":"Invalid field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get an agent","operationId":"getAgent","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Agent id, e.g. `agent_7f3a…`."}],"responses":{"200":{"description":"Agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such agent"}}},"post":{"tags":["Agents"],"summary":"Update an agent","description":"Partial update: present fields are set, `null` clears a field, absent fields are untouched. Config edits are live on the agent's next call.","operationId":"updateAgent","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentConfig"}}}},"responses":{"200":{"description":"Updated agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"400":{"description":"Invalid field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such agent"}}},"delete":{"tags":["Agents"],"summary":"Delete an agent","operationId":"deleteAgent","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deletion confirmation","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"agent.deleted"},"agent_id":{"type":"string"},"deleted":{"type":"boolean","example":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such agent"}}}},"/v1/voice/clones":{"get":{"tags":["Speak"],"summary":"List cloned voices","operationId":"listClones","responses":{"200":{"description":"Voices","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceList"}}}}}},"post":{"tags":["Speak"],"summary":"Create a cloned voice","description":"Enroll a custom voice from reference audio. EN-only today. Requires the `voice:clone` scope.","operationId":"createClone","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["name","file"],"properties":{"name":{"type":"string"},"file":{"type":"string","format":"binary","description":"Reference audio (>= 10s recommended)."}}}}}},"responses":{"201":{"description":"Created voice","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Voice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/voice/clones/{id}":{"delete":{"tags":["Speak"],"summary":"Delete a cloned voice","description":"Remove a cloned voice. Voices are tenant-isolated — you can only delete your own (otherwise `403`). Requires the `voice:clone` scope.","operationId":"deleteClone","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Cloned voice id."}],"responses":{"200":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such voice for this tenant"}}}},"/v1/voice/design":{"post":{"tags":["Speak"],"summary":"Design a voice from a prompt","description":"Generate a brand-new **synthetic** voice from a text description (distinct from cloning, which copies a real person). Async: returns `202` with a `design_id`; poll `GET /v1/voice/design/{id}` for candidate previews, then `POST /v1/voice/design/{id}/save` to keep one. Requires the `voice:design` scope.","operationId":"createDesign","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","description":"Natural-language description of the voice."},"candidates":{"type":"integer","default":3,"minimum":1,"maximum":4,"description":"How many candidate voices to generate."},"sample_text":{"type":"string","description":"Optional line the previews speak."},"attributes":{"type":"object","description":"Optional structured hints folded into the prompt.","properties":{"gender":{"type":"string"},"age":{"type":"string"},"accent":{"type":"string"},"pace":{"type":"number"},"energy":{"type":"number"}}}}}}}},"responses":{"202":{"description":"Design job accepted","content":{"application/json":{"schema":{"type":"object","properties":{"design_id":{"type":"string","example":"dsn_a1b2c3"},"status":{"type":"string","example":"queued"},"candidates":{"type":"integer","example":3},"estimated_seconds":{"type":"integer","example":25}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Design queue saturated — retry after the `Retry-After` delay."}}}},"/v1/voice/design/{id}":{"get":{"tags":["Speak"],"summary":"Get design candidates","description":"Poll a design job. When `status` is `completed`, `candidates` carries signed preview URLs (24h TTL). Candidates below the quality gate are omitted. Requires the `voice:design` scope.","operationId":"getDesign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Design id."}],"responses":{"200":{"description":"Design status + candidates","content":{"application/json":{"schema":{"type":"object","properties":{"design_id":{"type":"string","example":"dsn_a1b2c3"},"status":{"type":"string","enum":["queued","running","completed","failed"]},"candidates":{"type":"array","items":{"type":"object","properties":{"candidate_id":{"type":"string","example":"c1"},"preview_url":{"type":"string","format":"uri"},"quality":{"type":"object","properties":{"intelligibility":{"type":"number","example":1}}},"duration_s":{"type":"number","example":3.4}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such design for this tenant"}}}},"/v1/voice/design/{id}/save":{"post":{"tags":["Speak"],"summary":"Save a designed voice","description":"Enroll the chosen candidate as a permanent `voice_id`, usable immediately in `POST /v1/audio/speech` (`voice: vd_…`) and in Omni agents. Requires the `voice:design` scope.","operationId":"saveDesign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Design id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["candidate_id","name"],"properties":{"candidate_id":{"type":"string","example":"c1"},"name":{"type":"string","example":"Support — Nova"},"metadata":{"type":"object","additionalProperties":true}}}}}},"responses":{"201":{"description":"Saved voice","content":{"application/json":{"schema":{"type":"object","properties":{"voice_id":{"type":"string","example":"vd_7h16k"},"name":{"type":"string","example":"Support — Nova"},"source":{"type":"string","example":"design"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such design / candidate for this tenant"}}}},"/v1/transcription/jobs":{"post":{"tags":["Transcription Jobs"],"summary":"Create an async transcription job","description":"Submit audio for batch transcription. Provide **exactly one** source:\neither `audio_url` (an https URL we fetch — privacy-cleanest, the input is never stored)\n**or** a multipart upload (`multipart/form-data` with an `audio` file part and the same fields as form fields).\n\nReturns `202` immediately with a `queued` job; poll `GET /v1/transcription/jobs/{id}` or supply a `webhook_url` for a signed completion callback.\nSet `channel: true` for stereo (dual-channel) recordings to get exact, model-free speaker separation per channel.\nRequires the `transcribe:jobs` scope.","operationId":"createTranscriptionJob","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Opt-in safe retry (JSON body path). Reusing the key with an identical body replays the original 202 response; reusing it with a different body returns 409."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["audio_url"],"properties":{"audio_url":{"type":"string","format":"uri","description":"https URL of the audio to transcribe."},"model":{"type":"string","default":"pyai-hear-telephony"},"channel":{"type":"boolean","default":false,"description":"Dual-channel (stereo) diarization: transcribe each channel separately and label speakers per channel (exact)."},"diarize":{"type":"boolean","default":false,"description":"Single-track (mono) speaker diarization via Sortformer; words are aligned to speaker turns. Use `channel` instead for stereo recordings."},"numerals":{"type":"boolean","default":false,"description":"Format spoken numbers as digits."},"output_formats":{"type":"array","items":{"type":"string","enum":["json","srt","vtt"]},"default":["json"]},"webhook_url":{"type":"string","format":"uri","description":"https URL to POST the signed (X-PyAI-Signature) completion callback to."}}}},"multipart/form-data":{"schema":{"type":"object","required":["audio"],"properties":{"audio":{"type":"string","format":"binary"},"model":{"type":"string"},"channel":{"type":"string","enum":["true","false","stereo"]},"diarize":{"type":"string","enum":["true","false"]},"numerals":{"type":"string","enum":["true","false"]},"output_formats":{"type":"string","description":"Comma-separated, e.g. `json,srt`."},"webhook_url":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"description":"Job accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionJob"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Idempotency-Key reused with a different body (`code: idempotency_conflict`)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"tags":["Transcription Jobs"],"summary":"List transcription jobs","description":"Cursor-paginated, newest first. Pass `limit` (1–100, default 20) and the `next_cursor` from the previous page as `cursor` to continue. `next_cursor` is null on the last page.","operationId":"listTranscriptionJobs","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Max items to return (1–100)."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque token from a previous page's `next_cursor`. Omit for the first page."}],"responses":{"200":{"description":"Jobs","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TranscriptionJob"}},"has_more":{"type":"boolean","description":"True if another page is available."},"next_cursor":{"type":"string","nullable":true,"description":"Pass as `cursor` for the next page; null on the last page."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/transcription/jobs/{id}":{"get":{"tags":["Transcription Jobs"],"summary":"Get a transcription job","operationId":"getTranscriptionJob","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionJob"}}}},"404":{"description":"No such job for this tenant."}}},"delete":{"tags":["Transcription Jobs"],"summary":"Cancel a transcription job","description":"Cancels a `queued`/`running` job; idempotent on terminal jobs (returns them unchanged).","operationId":"cancelTranscriptionJob","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionJob"}}}}}}},"/v1/trace/config":{"get":{"tags":["Trace"],"summary":"Get Trace config for an agent (or the org default)","description":"Returns the per-agent Trace config (spec §5.1). Pass `agent_id` to read a specific\nagent's config; omit it for the org-wide default a new agent inherits. When nothing\nhas been configured yet, returns the safe default (`enabled:false`, `mode:warn`,\n`fail_open:true`). Requires the `trace:configure` scope.","operationId":"getTraceConfig","parameters":[{"name":"agent_id","in":"query","required":false,"schema":{"type":"string"},"description":"Agent to read config for; omit for the org default."}],"responses":{"200":{"description":"Trace config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceConfig"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"put":{"tags":["Trace"],"summary":"Set Trace config for an agent (or the org default)","description":"Upsert the per-agent Trace config (spec §5.1). The body is the §5.1 object (optionally\nwrapped as `{ agent_id, config }`). Modes: `warn` (log only, never blocks) · `modify`\n(redact PII / inject disclosures) · `block` · `human_handoff`. Always fail-open; the\ndeterministic inline gate runs models-side, the platform only distributes config.\nReturns the stored config with its content `ETag` (the models-side pull pins this).\nRequires the `trace:configure` scope.","operationId":"setTraceConfig","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceConfigInput"}}}},"responses":{"200":{"description":"Stored config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceConfig"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"agent_id does not belong to this tenant."}}}},"/v1/trace/rule-packs":{"get":{"tags":["Trace"],"summary":"List rule packs","description":"Built-in packs (TCPA, HIPAA, PII, brand-voice) plus this tenant's custom uploads. Requires the `trace:configure` scope.","operationId":"listTraceRulePacks","responses":{"200":{"description":"Rule packs","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TraceRulePack"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Trace"],"summary":"Upload a custom rule pack","description":"Register a custom rule pack (spec §5.3) in the Trace DSL. Structural validation only\nhere (`pack_id`, `version`, non-empty `rules`); the kernel compiles + deep-validates it\nmodels-side at pull time, and citations/wording are attorney-curated out of band.\nRequires the `trace:configure` scope.","operationId":"createTraceRulePack","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceRulePackSpec"}}}},"responses":{"201":{"description":"Rule pack created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceRulePack"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/trace/rule-packs/{id}":{"get":{"tags":["Trace"],"summary":"Get a rule pack","description":"Resolve a pack by `pack_id` (latest active by default; pass `version` to pin a specific version). Requires the `trace:configure` scope.","operationId":"getTraceRulePack","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The pack_id, e.g. `tcpa`."},{"name":"version","in":"query","required":false,"schema":{"type":"string"},"description":"Pin a specific version; omit for the latest active."}],"responses":{"200":{"description":"Rule pack (with authored spec)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceRulePack"}}}},"404":{"description":"No such rule pack."}}}},"/v1/trace/interactions":{"get":{"tags":["Trace"],"summary":"List scanned interactions (scorecards)","description":"Cursor-paginated, newest first. Each row is one call's Tier-0 compliance scorecard. Filter by `verdict` (PASS/WARN/FAIL) or `agent_id`. Requires the `trace:read` scope.","operationId":"listTraceInteractions","parameters":[{"name":"verdict","in":"query","required":false,"schema":{"type":"string","enum":["PASS","WARN","FAIL"]}},{"name":"agent_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Interactions","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TraceInteraction"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/trace/interactions/{id}":{"get":{"tags":["Trace"],"summary":"Get an interaction (the evidence view)","description":"The full per-call scorecard (findings with plain-English reasons + cited regulations, satisfied requirements, redactions, gate health, verdict) plus the tamper-evident `audit_hash`. With scorecard-v1 the response also carries the optional per-call `timeline` and `quality_metrics` eval blocks (empty until the engine emits them) and `derived_metrics` — the platform's score-ready rollup of the timeline (TTFB, turn counts, barge detect + recovery). Requires the `trace:read` scope.","operationId":"getTraceInteraction","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The call_id."}],"responses":{"200":{"description":"Interaction detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceInteractionDetail"}}}},"404":{"description":"No such interaction for this tenant."}}}},"/v1/trace/violations":{"get":{"tags":["Trace"],"summary":"List violations (findings)","description":"Cursor-paginated drill-down of every fired rule across scorecards. Filter by `rule_id`, `severity`, or `interaction_id`. Requires the `trace:read` scope.","operationId":"listTraceViolations","parameters":[{"name":"rule_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"severity","in":"query","required":false,"schema":{"type":"string","enum":["low","medium","high","critical"]}},{"name":"interaction_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Violations","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TraceViolation"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/trace/findings":{"get":{"tags":["Trace"],"summary":"List Tier-2 semantic findings","description":"Cursor-paginated Tier-2 (async semantic) findings — the model-judged concerns deterministic rules can't catch (HIPAA minimum-necessary, brand tone, hallucination-vs-knowledge-base, indirect opt-out, context-dependent PII). These are advisory and non-blocking, and are kept separate from the hash-chained Tier-0 violations. Filter by `check_id`, `action`, `severity`, or `interaction_id`. The CCO alerts feed is `action=escalate` and/or `severity=critical`. Requires the `trace:read` scope.","operationId":"listTraceFindings","parameters":[{"name":"check_id","in":"query","required":false,"schema":{"type":"string","example":"hallucination"}},{"name":"action","in":"query","required":false,"schema":{"type":"string","enum":["flag","preempt_next","escalate"]}},{"name":"severity","in":"query","required":false,"schema":{"type":"string","enum":["low","medium","high","critical"]}},{"name":"interaction_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Findings","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TraceFinding"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/trace/exposure":{"get":{"tags":["Trace"],"summary":"Compliance exposure summary","description":"The dashboard headline / Exposure Scan: interactions scanned, the share with a compliance gap, a per-rule exposure ranking, and the verdict mix over a trailing window. Requires the `trace:read` scope.","operationId":"getTraceExposure","parameters":[{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":365,"default":30},"description":"Trailing window in days."}],"responses":{"200":{"description":"Exposure summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceExposure"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/recap/config":{"get":{"tags":["Recap"],"summary":"Get Recap config","description":"Org Recap enablement, customer webhook URL, and default pack. Requires `recap:configure`.","operationId":"getRecapConfig","responses":{"200":{"description":"Recap config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapConfig"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"put":{"tags":["Recap"],"summary":"Update Recap config","description":"Enable Recap and set the customer webhook + default pack. Requires `recap:configure`.","operationId":"putRecapConfig","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapConfigInput"}}}},"responses":{"200":{"description":"Updated config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapConfig"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/recap/crm-config":{"get":{"tags":["Recap"],"summary":"Get Recap CRM config","description":"Salesforce field mapping and credentials (secrets redacted on GET). Requires `recap:configure`.","operationId":"getRecapCrmConfig","responses":{"200":{"description":"CRM config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapCrmConfig"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"}}},"put":{"tags":["Recap"],"summary":"Update Recap CRM config","description":"Hand-configured Salesforce mapping for design partners. Omit secret fields on update to preserve existing values.","operationId":"putRecapCrmConfig","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapCrmConfigInput"}}}},"responses":{"200":{"description":"Updated CRM config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapCrmConfig"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/v1/recap/calls":{"get":{"tags":["Recap"],"summary":"List recap records","description":"Recent post-call recaps for this org. Requires `recap:read` and the Recap add-on enabled.","operationId":"listRecapCalls","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["pending","processing","complete","failed"]}}],"responses":{"200":{"description":"Paginated recap list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapCallList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/v1/recap/calls/{call_id}":{"get":{"tags":["Recap"],"summary":"Get a recap record","operationId":"getRecapCall","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Recap detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapCall"}}}},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Recap"],"summary":"Manually trigger recap for a call","operationId":"triggerRecapCall","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["utterances"],"properties":{"pack_id":{"type":"string"},"call_duration_s":{"type":"number"},"utterances":{"type":"array","items":{"type":"object"}}}}}}},"responses":{"202":{"description":"Recap job accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapCallSummary"}}}},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/v1/telephony/available":{"get":{"tags":["Telephony"],"summary":"Search available numbers","description":"Search the carrier's available US local numbers to buy. Filter by `area_code` (NPA) or a `contains` digit pattern. Requires the `telephony:manage` scope.","operationId":"listAvailableNumbers","parameters":[{"name":"area_code","in":"query","required":false,"schema":{"type":"string","pattern":"^[2-9][0-9]{2}$"},"description":"3-digit US area code (NPA)."},{"name":"contains","in":"query","required":false,"schema":{"type":"string"},"description":"Digit pattern the number should contain."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"Available numbers","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TelephonyAvailableNumber"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Telephony is not enabled for this deployment."}}}},"/v1/telephony/numbers":{"get":{"tags":["Telephony"],"summary":"List your numbers","description":"Your org's managed numbers, newest first. Active only unless `include_released=true`. Requires the `telephony:manage` scope.","operationId":"listPhoneNumbers","parameters":[{"name":"include_released","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Your numbers","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TelephonyNumber"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Telephony"],"summary":"Provision (buy) a number","description":"Buy a specific available number and attach it to your org, optionally binding it to an `agent_id` for inbound routing. Carrier-side recording is never enabled — calls are recorded in PyAI's media bridge. Connected minutes bill on `telephony.minutes` ($0.01/min). Requires the `telephony:manage` scope.","operationId":"provisionPhoneNumber","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Opt-in safe retry. Reusing the key with an identical body replays the original 201 response (no second carrier purchase); reusing it with a different body returns 409 `idempotency_conflict`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyProvisionRequest"}}}},"responses":{"201":{"description":"Provisioned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyNumber"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"That number is already provisioned (`code: number_in_use`), or the Idempotency-Key was reused with a different body (`code: idempotency_conflict`)."}}}},"/v1/telephony/numbers/{id}/assign":{"post":{"tags":["Telephony"],"summary":"Route a number to an agent","description":"Bind the number to an `agent_id` (or pass `null` to unassign) so inbound calls open that agent's Omni session. Requires the `telephony:manage` scope.","operationId":"assignPhoneNumber","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyAssignRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyNumber"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/telephony/numbers/{id}":{"delete":{"tags":["Telephony"],"summary":"Release a number","description":"Release the number back to the carrier (stops the monthly rental). Idempotent. Requires the `telephony:manage` scope.","operationId":"releasePhoneNumber","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Released","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyNumber"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni":{"get":{"tags":["Realtime"],"summary":"Open an Omni voice-agent session (WebSocket)","description":"The native Omni API: a full-duplex WebSocket to a voice agent grounded in its knowledge bases + tools. Omni runs on a single engine tier (Ultra), so this is the canonical surface for agentic voice.\n\nUpgrade with `wss://api.pyai.com/v1/omni?agent_id=<id>`. Send PCM16 audio (`?format=pcm16&rate=24000` for browser/WebRTC, `rate=16000` for telephony) as binary frames and receive agent audio the same way.\n\nRequires the `omni:session` scope (or the `omni:*` wildcard). `agent_id` is an opaque label: the session is authorized by your key's org and `agent_id` is echoed to your own knowledge endpoint, so any id in your org's namespace is accepted (PyAI stores no per-agent state).\n\n**Auth:** browsers can't set `Authorization` on a WebSocket, so send the key as a subprotocol: `Sec-WebSocket-Protocol: pyai-key.<API_KEY>` (server clients may use `?api_key=` instead). The customer key is validated and swapped for the internal engine credential on the upgrade.\n\nSee the Omni wire protocol reference for the frame catalog and close codes.","operationId":"openOmni","parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string"},"description":"An opaque label for the agent you're driving. The session is authorized by your key's org and this value is echoed to your own kb_endpoint (PyAI stores no per-agent state); any id in your org's namespace is accepted. Must be safe as a header value (no control chars, ≤256 chars)."},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["pcm16"],"default":"pcm16"},"description":"Audio sample format for both directions."},{"name":"rate","in":"query","required":false,"schema":{"type":"integer","enum":[16000,24000],"default":24000},"description":"Audio sample rate in Hz. Use 24000 for browser/WebRTC, 16000 for telephony."}],"responses":{"101":{"description":"Switching Protocols — the Omni WebSocket is open."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/realtime":{"get":{"tags":["Realtime"],"summary":"Open a realtime duplex session (WebSocket)","description":"Upgrade to a WebSocket for full-duplex STT + turn-taking + TTS in one connection.\n\n- `model=pyai-omni-realtime` (default) — agentic voice grounded in an agent's knowledge bases + tools. Requires the `omni:session` scope and an `agent_id` your key's org owns. This is the OpenAI-realtime-compatible surface for Omni; for new integrations prefer the native [`/v1/omni`](#tag/Realtime/operation/openOmni) endpoint.\n- `model=pyai-flow-realtime` — _legacy_ voice duplex (Flow). Retired for new customers; existing Flow keys with the `flow:session` scope still connect.\n\n**Auth:** browsers can't set `Authorization` on a WebSocket, so send the key as a subprotocol: `Sec-WebSocket-Protocol: pyai-key.<API_KEY>` (server clients may use `?api_key=` / `?access_token=` instead). The customer key is validated and swapped for the internal upstream credential on the upgrade.","operationId":"openRealtime","parameters":[{"name":"model","in":"query","required":true,"schema":{"type":"string","enum":["pyai-omni-realtime","pyai-flow-realtime"],"default":"pyai-omni-realtime"},"description":"Realtime model. `pyai-omni-realtime` (default) enables the agent layer; `pyai-flow-realtime` is the retired legacy Flow engine."},{"name":"agent_id","in":"query","required":false,"schema":{"type":"string"},"description":"Omni only. The agent to drive (created in the console Agent Builder). Verified against the key's org; a foreign agent is rejected with 403."}],"responses":{"101":{"description":"Switching Protocols — the duplex WebSocket is open."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Use `Authorization: Bearer pyai_live_...` (or `pyai_test_...`)."},"xApiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Header alias for bearer auth on HTTP endpoints. WebSocket auth uses subprotocol `pyai-key.<API_KEY>`."}},"responses":{"BadRequest":{"description":"Invalid request (bad field, unsupported value)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Unauthorized":{"description":"Missing or invalid API key (`code: unauthorized`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PaymentRequired":{"description":"Billing gate: org out of prepaid credit, per-key budget hit, or plan quota exhausted (`code: credit_exhausted | key_budget_exceeded | insufficient_quota`). Do not retry; add credit or raise the limit. A brand-new key may see this on its first call until the account is funded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Key lacks the required scope or the origin is not allow-listed (`code: forbidden | origin_not_allowed`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found (`code: not_found`)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"RateLimited":{"description":"Too many requests or too many concurrent realtime sessions; see Retry-After header (`code: rate_limit_exceeded | concurrency_limit_exceeded | daily_cap_exceeded`)","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Identity":{"type":"object","description":"The identity the gateway resolved for the calling key, plus its enforced limits and credit posture. Returned by `GET /v1/me`.","properties":{"object":{"type":"string","example":"identity"},"key_id":{"type":"string","example":"key_7f3a0b12","description":"The calling key's id."},"org_id":{"type":"string","example":"org_abc123"},"project_id":{"type":"string","example":"proj_abc123"},"env":{"type":"string","enum":["test","live"],"description":"Key environment. `test` keys run in the sandbox tier (daily unit cap, no credit gate)."},"status":{"type":"string","enum":["active","revoked"],"description":"Key status."},"org_status":{"type":"string","enum":["active","suspended"],"description":"Owning org status; `suspended` explains a 403 `org_suspended`."},"scopes":{"type":"array","items":{"type":"string"},"description":"Granted scopes. A missing scope here explains a 403 `insufficient_scope`.","example":["voice:synthesize","hear:transcribe"]},"expires_at":{"type":["integer","null"],"description":"Key expiry (Unix ms); null = non-expiring."},"plan":{"type":"string","example":"payg","description":"Billing plan id."},"limits":{"type":"object","description":"Per-key gateway limits (what the edge actually enforces).","properties":{"rps":{"type":"integer","description":"Requests per second."},"burst":{"type":"integer","description":"Token-bucket burst."},"concurrency":{"type":"integer","description":"Max concurrent realtime sessions."},"monthly_units":{"type":["integer","null"],"description":"Hard monthly metered-unit cap (hard-capped plans); null = metered/no hard cap."},"daily_unit_cap":{"type":["integer","null"],"description":"Sandbox/publishable per-day unit ceiling; present for `test` keys."}}},"credit":{"type":"object","description":"Prepaid-credit posture. Explains a 402 `credit_exhausted`.","properties":{"gated":{"type":"boolean","description":"True when this key is subject to the org prepaid-credit gate (live keys on an org with no active paid subscription)."},"available_cents":{"type":"integer","description":"Available prepaid credit in USD cents (ledger balance plus this month's free grant when gated)."}}},"key_budget_cents":{"type":["integer","null"],"description":"Per-key monthly spend cap (USD cents); null = no per-key budget. Hitting it is a 402 `key_budget_exceeded`."}}},"ErrorCode":{"type":"string","description":"Stable, machine-readable error code. Branch on this rather than the human `message`.","enum":["invalid_request_error","invalid_agent_id","unauthorized","forbidden","origin_not_allowed","credit_exhausted","key_budget_exceeded","insufficient_quota","rate_limit_exceeded","concurrency_limit_exceeded","daily_cap_exceeded"]},"Problem":{"type":"object","description":"RFC 7807 problem+json returned by the control plane (request-validation and resource errors such as 400/404/409). The stable code is the last path segment of `type`.","required":["title","status"],"properties":{"type":{"type":"string","description":"Problem type URI; ends with the stable code."},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"request_id":{"type":"string"}}},"Error":{"type":"object","description":"OpenAI-compatible error envelope returned by the gateway data plane (401/402/403/429). Control-plane request/resource errors use Problem (application/problem+json) instead.","required":["error"],"properties":{"error":{"type":"object","required":["message"],"properties":{"message":{"type":"string","description":"Human-readable explanation."},"type":{"type":"string","description":"Error category, e.g. rate_limit_error."},"code":{"$ref":"#/components/schemas/ErrorCode"},"param":{"type":"string","nullable":true,"description":"Offending parameter when applicable, else null."}}}}},"Transcription":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"}}},"TranscriptionJob":{"type":"object","properties":{"job_id":{"type":"string","example":"job_aZ09..."},"status":{"type":"string","enum":["queued","running","completed","failed","cancelled"]},"created_at":{"type":"integer","description":"Unix ms."},"updated_at":{"type":"integer","description":"Unix ms."},"result":{"type":"object","description":"Present on completed jobs (inline). Large results are offloaded to result_url instead.","properties":{"text":{"type":"string"},"speakers":{"type":"integer"},"audio_seconds":{"type":"number"},"segments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"start":{"type":"number"},"end":{"type":"number"},"text":{"type":"string"},"speaker":{"type":"string"},"channel":{"type":"integer"}}}},"words":{"type":"array","items":{"type":"object"}},"formats":{"type":"object","description":"Map of output format -> signed GET URL (srt/vtt)."}}},"result_url":{"type":"string","format":"uri","description":"Signed GET URL for an offloaded large result."},"error":{"type":"string","description":"Present on failed jobs."}}},"ModelList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"type":"object"}}}},"TraceGuardrails":{"type":"object","description":"Inline guardrails compiled into a synthesized rule pack (spec §5.1).","properties":{"mode":{"type":"string","enum":["warn","modify","block","human_handoff"],"default":"warn","description":"warn = log only (never blocks); modify = redact/inject; block = suppress; human_handoff = escalate."},"block_pii":{"type":"object","properties":{"patterns":{"type":"array","items":{"type":"string","enum":["ssn","credit_card","email","us_phone"]},"description":"Deterministically-redactable PII (context-dependent PII like DOB is Tier-2, not inline)."}}},"mandatory_disclosures":{"type":"array","items":{"type":"object","required":["text"],"properties":{"text":{"type":"string","description":"Phrase the agent must speak, e.g. an AI / recording disclosure."},"trigger":{"type":"string","enum":["call_start","any"],"description":"call_start = must appear on the first agent turn."},"required_within_seconds":{"type":"number","description":"Deadline in seconds; otherwise a 2-turn default applies."}}}},"blocked_phrases":{"type":"array","items":{"type":"string"},"description":"Phrases the agent must never say."},"fail_open":{"type":"boolean","default":true,"description":"Trace down must never make a call worse — log 'unavailable' instead of blocking."},"inline_timeout_ms":{"type":"number","default":10,"description":"Inline budget; the in-process gate is ~14µs p99, far under this."}}},"TraceConfigInput":{"type":"object","description":"Per-agent Trace config (spec §5.1). May be wrapped as { agent_id, config } or sent raw.","properties":{"agent_id":{"type":"string","description":"Agent to configure; omit for the org-wide default."},"enabled":{"type":"boolean","default":false},"channels":{"type":"array","items":{"type":"string","enum":["voice","text"]},"default":["voice"]},"rule_packs":{"type":"object","additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"version":{"type":"string","nullable":true}}},"description":"Map of pack_id → { enabled, version }, e.g. { tcpa: { enabled: true } }."},"guardrails":{"$ref":"#/components/schemas/TraceGuardrails"}}},"TraceConfig":{"type":"object","properties":{"object":{"type":"string","example":"trace.config"},"agent_id":{"type":"string","nullable":true},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["warn","modify","block","human_handoff"]},"etag":{"type":"string","description":"Content hash the models-side gate pins (version-pinned, ETag-cached pull)."},"updated_at":{"type":"integer","description":"Unix ms."},"config":{"$ref":"#/components/schemas/TraceConfigInput"}}},"TraceRulePackSpec":{"type":"object","required":["pack_id","version","rules"],"description":"An authored rule pack in the Trace DSL (rule_pack_schema.json).","properties":{"pack_id":{"type":"string","pattern":"^[a-z0-9_]+$"},"version":{"type":"string"},"jurisdiction":{"type":"string"},"legal_status":{"type":"string","description":"Provenance / attorney-curation status."},"rules":{"type":"array","minItems":1,"items":{"type":"object"}}}},"TraceRulePack":{"type":"object","properties":{"object":{"type":"string","example":"trace.rule_pack"},"id":{"type":"string","example":"tpack_..."},"pack_id":{"type":"string","example":"tcpa"},"version":{"type":"string"},"builtin":{"type":"boolean","description":"True for PyAI's bundled packs; false for tenant uploads."},"jurisdiction":{"type":"string","nullable":true},"legal_status":{"type":"string","nullable":true},"etag":{"type":"string"},"status":{"type":"string","enum":["active","deprecated"]},"created_at":{"type":"integer","description":"Unix ms."},"spec":{"type":"object","description":"The authored DSL (only on the single-pack GET)."}}},"TraceInteraction":{"type":"object","properties":{"object":{"type":"string","example":"trace.interaction"},"id":{"type":"string","description":"call_id"},"agent_id":{"type":"string","nullable":true},"product":{"type":"string","nullable":true,"enum":["hear","voice","speak","clone","cue","flow","omni","agents",null]},"verdict":{"type":"string","enum":["PASS","WARN","FAIL"]},"findings":{"type":"integer"},"blocked_turns":{"type":"integer"},"modified_turns":{"type":"integer"},"packs_enforced":{"type":"string"},"scored_at":{"type":"integer","description":"Unix ms."}}},"TraceInteractionDetail":{"allOf":[{"$ref":"#/components/schemas/TraceInteraction"},{"type":"object","properties":{"audit_hash":{"type":"string","description":"Hash-chain link proving this record is unaltered (the signed-evidence guarantee)."},"scorecard":{"type":"object","description":"The full trace-scorecard-v0/v1 record: findings, satisfied requirements, redactions, gate health, verdict, and (scorecard-v1) the optional `timeline`/`quality_metrics` eval blocks."},"tier2_findings":{"type":"array","description":"Tier-2 (async semantic) findings for this call, joined at read time. They arrive after the scorecard and never alter `audit_hash`.","items":{"$ref":"#/components/schemas/TraceFinding"}},"timeline":{"type":"array","description":"scorecard-v1 per-call timeline (transcript turns + latency stamps used for eval scoring), hoisted from the scorecard for convenience. Empty `[]` until the engine emits it.","items":{"$ref":"#/components/schemas/TraceTimelineEvent"}},"quality_metrics":{"allOf":[{"$ref":"#/components/schemas/TraceQualityMetrics"}],"nullable":true,"description":"scorecard-v1 per-call rolled-up quality metrics, hoisted from the scorecard. `null` until the engine emits it."},"derived_metrics":{"allOf":[{"$ref":"#/components/schemas/TraceDerivedCallMetrics"}],"description":"Platform-computed rollup of the `timeline` into score-ready eval aggregates (TTFB, turn counts, barge detect + recovery). Always present; zero counts and `null` percentiles until the engine emits a timeline. Percentiles use the same method as the offline eval harness, so an online per-call score lines up with the offline benchmark."}}}]},"TraceLatencySummary":{"type":"object","description":"Distribution of one latency sample set (ms). Percentiles are `null` when there are no samples.","properties":{"count":{"type":"integer","description":"Number of samples."},"p50":{"type":"number","nullable":true,"description":"Median (ms)."},"p95":{"type":"number","nullable":true,"description":"p95 (ms)."},"p99":{"type":"number","nullable":true,"description":"p99 (ms)."}}},"TraceDerivedCallMetrics":{"type":"object","description":"Read-time per-call eval aggregates derived from the scorecard-v1 timeline (ServiceAgent feedback §13c / PYAI_EVALS_PLATFORM_PLAN Layer B). Pure and inert (zero counts, `null` percentiles) until the engine emits a timeline.","properties":{"turns":{"type":"integer","description":"Total timeline turns."},"turns_by_role":{"type":"object","additionalProperties":{"type":"integer"},"description":"Turn count per role (e.g. `agent` / `caller`)."},"ttfb_ms":{"$ref":"#/components/schemas/TraceLatencySummary"},"endpointing_ms":{"$ref":"#/components/schemas/TraceLatencySummary"},"barge":{"type":"object","description":"Barge-in (caller interrupts the agent) counts + detect-latency distribution.","properties":{"count":{"type":"integer","description":"Barge-in events detected."},"recovered":{"type":"integer","description":"Barge-ins the agent recovered from."},"recovery_rate":{"type":"number","nullable":true,"description":"recovered / count (0..1); `null` when there were no barge-ins."},"detect_ms":{"$ref":"#/components/schemas/TraceLatencySummary"}}},"tool_calls":{"type":"integer","description":"Total tool/function calls across all turns."}}},"TraceTimelineEvent":{"type":"object","description":"One per-turn event on the scorecard-v1 timeline (PLATFORM_ASK_EVALS_ENGINE Ask 1). Emitted by the engine per call; inert until then.","properties":{"seq":{"type":"integer","description":"Turn sequence number within the call."},"t_ms":{"type":"number","description":"Offset from call start, in milliseconds."},"role":{"type":"string","description":"Who spoke this turn, e.g. `agent` | `caller`."},"text":{"type":"string","description":"Transcript for the turn."},"ttfb_ms":{"type":"number","description":"Time-to-first-audio for the turn (latency scoring)."},"endpointing_ms":{"type":"number","description":"Turn-detection (endpointing) latency."},"barge":{"type":"object","description":"Barge-in (caller interrupts the agent) detect latency + recovery for the turn.","properties":{"detect_ms":{"type":"number","description":"Time to detect the barge-in."},"recovered":{"type":"boolean","description":"Whether the agent recovered gracefully."}}},"tool_calls":{"type":"array","description":"Tool/function calls the agent made during the turn (tool-use scoring).","items":{"type":"object","properties":{"name":{"type":"string"},"args":{"description":"Tool arguments (free-form JSON)."},"result":{"description":"Tool result (free-form JSON)."},"t_ms":{"type":"number","description":"Offset from call start, in milliseconds."}}}}}},"TraceQualityMetrics":{"type":"object","description":"scorecard-v1 per-call rolled-up quality metrics (PLATFORM_ASK_EVALS_ENGINE Ask 1). All optional + inert until the engine emits them.","properties":{"wer":{"type":"number","description":"Word error rate (0..1)."},"ttfb_ms":{"type":"number","description":"Time-to-first-audio for the call, in milliseconds."},"turn_p95_ms":{"type":"number","description":"p95 end-to-end turn latency, in milliseconds."},"barge_recovery":{"type":"number","description":"Barge-in recovery rate (0..1)."},"task_success":{"type":"number","description":"Task-success rate (0..1)."},"vaqi":{"type":"number","description":"Voice-agent quality index."}}},"TraceFinding":{"type":"object","description":"A Tier-2 (async semantic) finding — a model-judged concern that deterministic rules can't catch. Advisory and non-blocking.","properties":{"object":{"type":"string","example":"trace.finding"},"id":{"type":"string"},"interaction_id":{"type":"string","description":"call_id"},"agent_id":{"type":"string","nullable":true},"check_id":{"type":"string","example":"hallucination","description":"minimum_necessary | brand_tone | hallucination | ambiguous_optout | context_pii | …"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"verdict":{"type":"string","example":"concern"},"confidence":{"type":"number","description":"Model confidence, 0..1."},"action":{"type":"string","enum":["flag","preempt_next","escalate"],"description":"What the finding asks for; never blocks the current turn."},"speaker":{"type":"string","nullable":true,"enum":["agent","caller","any",null]},"reason":{"type":"string","description":"Plain-English explanation."},"preempt_instruction":{"type":"string","nullable":true,"description":"Constraint applied to the NEXT turn when action is preempt_next."},"at_t":{"type":"number","nullable":true,"description":"Seconds since call start."},"tier":{"type":"integer","example":2}}},"TraceViolation":{"type":"object","properties":{"object":{"type":"string","example":"trace.violation"},"id":{"type":"string"},"interaction_id":{"type":"string","description":"call_id"},"agent_id":{"type":"string","nullable":true},"rule_id":{"type":"string","example":"tcpa-003"},"pack_id":{"type":"string","nullable":true,"example":"tcpa"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"action_taken":{"type":"string","enum":["pass","flag","modify","block"]},"citation":{"type":"string","description":"Cited regulation, e.g. '47 CFR § 64.1200(b)(1)'."},"reason":{"type":"string","description":"Plain-English explanation."},"at_t":{"type":"number","nullable":true,"description":"Seconds since call start."}}},"TraceExposure":{"type":"object","properties":{"object":{"type":"string","example":"trace.exposure"},"window_days":{"type":"integer"},"interactions_scanned":{"type":"integer"},"with_a_gap":{"type":"integer","description":"Interactions whose verdict is not PASS."},"gap_rate":{"type":"number","description":"with_a_gap / interactions_scanned (0..1)."},"by_rule":{"type":"array","items":{"type":"object","properties":{"rule_id":{"type":"string"},"pack_id":{"type":"string","nullable":true},"count":{"type":"integer"},"rate":{"type":"number"}}}},"by_verdict":{"type":"object","properties":{"PASS":{"type":"integer"},"WARN":{"type":"integer"},"FAIL":{"type":"integer"}}},"top_exposure":{"type":"string","nullable":true,"description":"The rule_id with the most occurrences."}}},"RecapConfig":{"type":"object","properties":{"object":{"type":"string","example":"recap.config"},"enabled":{"type":"boolean"},"webhook_url":{"type":"string","nullable":true,"format":"uri"},"default_pack_id":{"type":"string","example":"sales_outbound"},"updated_at":{"type":"integer","description":"Unix ms."}}},"RecapConfigInput":{"type":"object","properties":{"enabled":{"type":"boolean"},"webhook_url":{"type":"string","nullable":true,"format":"uri"},"default_pack_id":{"type":"string"}}},"RecapCrmConfig":{"type":"object","properties":{"object":{"type":"string","example":"recap.crm_config"},"salesforce":{"$ref":"#/components/schemas/RecapSalesforceConfig","nullable":true},"updated_at":{"type":"integer","description":"Unix ms."}}},"RecapCrmConfigInput":{"type":"object","properties":{"salesforce":{"$ref":"#/components/schemas/RecapSalesforceConfigInput","nullable":true}}},"RecapSalesforceConfig":{"type":"object","properties":{"enabled":{"type":"boolean"},"instance_url":{"type":"string","format":"uri"},"client_id":{"type":"string","description":"Masked on GET."},"client_secret":{"type":"string","description":"Redacted on GET."},"refresh_token":{"type":"string","description":"Redacted on GET."},"object":{"type":"string","example":"Opportunity"},"record_id_field":{"type":"string","example":"salesforce_id"},"field_map":{"type":"object","additionalProperties":{"type":"string"},"description":"Recap logical field → Salesforce API field name."},"create_activity":{"type":"boolean","description":"Create a Task with TL;DR + summary after PATCH."}}},"RecapSalesforceConfigInput":{"type":"object","properties":{"enabled":{"type":"boolean"},"instance_url":{"type":"string","format":"uri"},"client_id":{"type":"string"},"client_secret":{"type":"string"},"refresh_token":{"type":"string"},"object":{"type":"string"},"record_id_field":{"type":"string"},"field_map":{"type":"object","additionalProperties":{"type":"string"}},"create_activity":{"type":"boolean"}}},"RecapCallSummary":{"type":"object","properties":{"object":{"type":"string","example":"recap.call"},"call_id":{"type":"string"},"pack_id":{"type":"string"},"status":{"type":"string","enum":["pending","processing","complete","failed"]},"call_duration_s":{"type":"number","nullable":true},"created_at":{"type":"integer"},"completed_at":{"type":"integer","nullable":true}}},"RecapCall":{"allOf":[{"$ref":"#/components/schemas/RecapCallSummary"},{"type":"object","properties":{"record":{"type":"object","description":"Full Recap output JSON when status is complete."},"error":{"type":"string","nullable":true},"crm_write_status":{"type":"string","nullable":true}}}]},"RecapCallList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RecapCallSummary"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}},"TelephonyAvailableNumber":{"type":"object","properties":{"object":{"type":"string","example":"telephony.available_number"},"phone_number":{"type":"string","example":"+14155550123","description":"E.164."},"country":{"type":"string","example":"US"},"area_code":{"type":"string","nullable":true,"example":"415"},"locality":{"type":"string","nullable":true,"example":"San Francisco"},"region":{"type":"string","nullable":true,"example":"CA"},"capabilities":{"type":"object","properties":{"voice":{"type":"boolean"},"sms":{"type":"boolean"}}},"monthly_cost_cents":{"type":"integer","description":"Carrier rental, display-only."}}},"TelephonyNumber":{"type":"object","properties":{"object":{"type":"string","example":"telephony.number"},"id":{"type":"string","example":"pn_..."},"phone_number":{"type":"string","example":"+14155550123","description":"E.164."},"country":{"type":"string","example":"US"},"area_code":{"type":"string","nullable":true,"example":"415"},"capabilities":{"type":"object","properties":{"voice":{"type":"boolean"},"sms":{"type":"boolean"}}},"agent_id":{"type":"string","nullable":true,"description":"Agent that answers inbound calls to this number."},"recording":{"type":"boolean","description":"Carrier-side recording — always false; calls are recorded in PyAI's media bridge."},"monthly_cost_cents":{"type":"integer"},"status":{"type":"string","enum":["active","released"]},"created_at":{"type":"integer","description":"Unix ms."},"released_at":{"type":"integer","nullable":true,"description":"Unix ms."}}},"TelephonyProvisionRequest":{"type":"object","required":["phone_number"],"properties":{"phone_number":{"type":"string","example":"+14155550123","description":"A US number (E.164) from the available-numbers search."},"agent_id":{"type":"string","nullable":true,"description":"Optional agent to route inbound calls to."}}},"TelephonyAssignRequest":{"type":"object","properties":{"agent_id":{"type":"string","nullable":true,"description":"Agent to bind, or null to unassign."}}},"Voice":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","example":"voice"},"name":{"type":"string"},"status":{"type":"string","enum":["pending","ready","failed"]}}},"VoiceList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Voice"}}}},"StockVoice":{"type":"object","properties":{"object":{"type":"string","example":"voice"},"voice_id":{"type":"string","example":"stock_emma_en_gb"},"name":{"type":"string","example":"Imogen"},"gender":{"type":"string","enum":["M","F"]},"region":{"type":"string","example":"UK (England / RP)","description":"Accent / regional flavor."},"age":{"type":"string","example":"32"},"tone":{"type":"string","example":"polished, reassuring"},"bio":{"type":"string","example":"Calm, articulate London front-desk."},"language":{"type":"string","example":"en"},"avatar_url":{"type":"string","format":"uri","description":"Voice-matched avatar image (PNG)."},"preview_url":{"type":"string","format":"uri","description":"Short audio preview (MP3)."}}},"StockVoiceList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/StockVoice"}}}},"AgentConfig":{"type":"object","description":"Writable agent fields. On update, present fields are set, `null` clears, absent fields are untouched.","properties":{"name":{"type":"string","maxLength":200,"description":"Display name. Required on create."},"persona_system_prompt":{"type":["string","null"],"maxLength":32000,"description":"The agent's entire character, role, policies, and business context."},"greeting":{"type":["string","null"],"maxLength":1000,"description":"Opening line spoken instantly on connect."},"voice_id":{"type":["string","null"],"description":"A stock voice id from `GET /v1/voices` or a cloned voice id."},"brain_model":{"type":["string","null"],"description":"Per-agent model selection. Omit for the platform default."},"barge_sensitivity":{"type":["string","null"],"description":"How eagerly the agent yields when talked over: `low`, `normal`, `high`, or an exact dBFS value."},"ack_mode":{"type":["string","null"],"description":"Conversational-acknowledgment mode. Default `auto`."},"recordings_enabled":{"type":["boolean","null"],"description":"Enable stereo call recordings. Default false."},"consent_line":{"type":["string","null"],"maxLength":500,"description":"Compliance line spoken first when recordings are enabled."},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"},"description":"Up to 16 key/value annotations (keys ≤64 chars, values ≤512 chars)."},"keyterms":{"type":["array","null"],"items":{"type":"string"},"maxItems":100,"description":"Vocabulary-boost terms for speech recognition (stored now; engine biasing rolls out per the keyterms roadmap)."},"goals":{"type":["array","null"],"items":{"type":"string"},"maxItems":20,"description":"Goal checklist for post-call outcome scoring (stored now; scoring ships with summaries)."}}},"Agent":{"type":"object","properties":{"object":{"type":"string","example":"agent"},"agent_id":{"type":"string","example":"agent_7f3a0b12"},"name":{"type":"string"},"persona_system_prompt":{"type":["string","null"]},"greeting":{"type":["string","null"]},"voice_id":{"type":["string","null"]},"brain_model":{"type":"string","example":"default"},"barge_sensitivity":{"type":"string","example":"normal"},"ack_mode":{"type":"string","example":"auto"},"recordings_enabled":{"type":"boolean"},"consent_line":{"type":["string","null"]},"keyterms":{"type":"array","items":{"type":"string"}},"goals":{"type":"array","items":{"type":"string"}},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"created_at":{"type":"integer","description":"Unix seconds."}}},"AgentList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}},"has_more":{"type":"boolean","example":false}}}}}}