{"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** — PyAI's feature to create, manage & track your Omni voice agents (no-code builder, hosted knowledge, evals, monitoring). _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 receive up to $50 of prepaid credits after phone verification (graduated signup); `pyai_test_` keys skip the credit gate entirely.\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 is **zero-state — there is nothing to create first.** Open a WebSocket, pass your key as a subprotocol, and send the agent's behavior (voice, persona, knowledge endpoint) in the first `configure` frame:\n\n```\nwss://api.pyai.com/v1/omni?session_label=support&format=pcm16&rate=24000\n  Sec-WebSocket-Protocol: pyai-key.$PYAI_API_KEY\n```\n\nThe session is authorized by your key's **organization**; `session_label` is an **optional, opaque** tag (echoed to your own knowledge endpoint for correlation) — omit it or use any value. `format` and `rate` are load-bearing on the connect URL (the SDK sets them). Send PCM16 audio as binary frames and receive the agent's speech the same way. **Optional convenience:** instead of sending the behavior each time, pre-store a persona+voice+KB+tools **agent profile** via `POST /v1/agents` and pass its id as the `session_label` (a.k.a. the deprecated `agent_id` alias) — purely optional sugar over the `configure` frame, not a requirement and not the upcoming **Agents** feature (the no-code create/manage/track surface). 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; 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), `/v1/realtime` (Omni), and `POST /v1/omni/sessions` (mint a browser session token) |\n| `omni:read` | `/v1/omni/calls` (Omni post-call records) |\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| Agent profiles (optional 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| Omni call records | `/v1/omni/calls`, `/v1/omni/calls/{id}` | Bearer | `omni:read` | live |\n| Telephony | `/v1/telephony/*` | Bearer | `telephony:manage` | live |\n| Agents (create/manage/track feature) | _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.08/min (the create/manage/track feature; rolling out). AI products (Hear, Speak, Cue, Omni) bill **per second by default** — the pulse is applied once to each meter's invoice-period total, so many short sessions are summed and rounded a single time (never minute-rounded per call), and an empty/failed call bills nothing. Coarser pulses are available as an optional enterprise override. Managed telephony minutes keep a 1-minute pulse (carrier economics). 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":"Sandbox","description":"Zero-friction onboarding for coding agents: mint a free, instant, no-card sandbox key with no human steps."},{"name":"Transcription Jobs","description":"Async batch transcription"},{"name":"Agents","description":"Agent profiles — OPTIONAL pre-stored Omni session config (persona, greeting, voice, conversation knobs) you can reference by id instead of sending a `configure` frame each call. Optional convenience over the zero-state `/v1/omni` primitive; NOT required to connect. (Distinct from the upcoming Agents feature — the no-code create/manage/track surface.)"},{"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/sandbox/keys":{"post":{"tags":["Sandbox"],"summary":"Mint a sandbox key (no auth)","description":"Create a free, instant `pyai_test_` sandbox key with **no human steps** — no email, no password, no card. Built for AI coding agents (Cursor, Lovable, Claude Code, Codex) and the PyAI MCP server's `create_sandbox_key` tool, so agent-generated code runs on its first execution instead of stalling at a credential wall. The key is a normal sandbox key: it **skips the credit gate (never returns 402)**, is limited to Hear/Speak/Omni starter scopes, expires automatically, and is bounded by a daily usage cap. Public and unauthenticated; rate-limited per source network. Treat the returned key as an opaque secret (read it from an env var). For production, create a live key in the console. May be disabled on some deployments (returns 404).","operationId":"createSandboxKey","security":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","maxLength":64,"description":"Optional human label for the throwaway org/key, shown in your console (e.g. your app name). Sanitized; never used as an identifier."}}}}}},"responses":{"201":{"description":"A freshly minted sandbox key.","content":{"application/json":{"schema":{"type":"object","required":["object","api_key","environment","expires_at","base_url"],"properties":{"object":{"type":"string","enum":["sandbox.key"]},"api_key":{"type":"string","description":"The plaintext `pyai_test_...` secret. Shown once — store it in an env var."},"key_id":{"type":"string"},"org_id":{"type":"string"},"project_id":{"type":"string"},"environment":{"type":"string","enum":["test"]},"scopes":{"type":"array","items":{"type":"string"},"description":"Narrow starter scopes: Hear transcription/streaming, Speak synthesis, and Omni realtime.","example":["hear:transcribe","hear:stream","voice:synthesize","omni:session"]},"expires_at":{"type":"integer","description":"Key expiry, Unix epoch milliseconds."},"base_url":{"type":"string","description":"REST base URL to use with this key."},"docs":{"type":"string"},"note":{"type":"string"}}}}}},"404":{"description":"Sandbox-key minting is disabled on this deployment."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/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 language hint (e.g. `en`). English is the GA, benchmarked language today; other codes are accepted as hints but accuracy is not yet published — see the Language support reference."},"seed":{"type":"integer","description":"Optional determinism seed for reproducible eval runs. Forwarded to the engine and honored once the engine supports it; 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; 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); the literal text frame `EOF` is also accepted as an equivalent flush. 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| `usage` | just before a graceful close | `{product, meter, audio_seconds, minutes}` — the session's billed active-audio, so you can reconcile realtime spend in-band (a realtime WS carries no `x-pyai-units` response header). Best-effort; absent if the session had no billable audio or closed abnormally. |\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-K KB passages, `[]` when no KB is bound or retrieval times out). Enable + tune Cue with the first JSON frame `{\"type\":\"config\",\"grounding\":true}`, optionally `grounding_k` (passages to retrieve, 1-20, default 3), `grounding_min_score` (drop passages below this score, 0-1, default 0), and `grounding_timeout_ms` (max wait at the final before failing open to `[]`, 50-2000, default 450). Retrieval is fail-open: a slow/failed lookup yields `grounding: []` and never stalls the turn.\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 language hint forwarded to the engine (e.g. `en`). English is the GA, benchmarked language today; other codes are accepted as hints but recognition accuracy is not yet published — see the Language support reference. One hint per session; there is no mid-session auto-detect."},{"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":"numerals","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Format spoken numbers as digits in the transcript (e.g. 'one two three' → '123'). Same semantics as the batch `numerals` flag; forwarded to the engine. Useful for voice agents that read back phone numbers, codes, and amounts. Default false (spoken form)."},{"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; 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; no effect when omitted."},{"name":"endpointing_ms","in":"query","required":false,"schema":{"type":"integer","minimum":50,"maximum":5000},"description":"Optional turn-segmentation tuning: the trailing-pause length (ms, 50-5000) that ends an utterance. Useful where turn-taking pacing differs. Clamped to the valid range and forwarded to the engine; honored once the engine supports it, a no-op when omitted. You can always force end-of-turn from the client with `{\"type\":\"commit\"}`."}],"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. For drop-in OpenAI compatibility, the preset names `alloy`, `echo`, `fable`, `onyx`, `nova`, and `shimmer` are also accepted and map to PyAI stock voices."},"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; 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; 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 agent profiles","description":"All active agent profiles in your organization. Agent profiles are optional pre-stored Omni session config; they are not required to open an Omni session. 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 profile","description":"Create an OPTIONAL agent profile (persona, greeting, voice, conversation knobs) so you can reference it by id instead of sending a full `configure` frame each call. Drive it with `wss://api.pyai.com/v1/omni?session_label=agent_…` (the profile id doubles as the opaque session label; `agent_id` is the deprecated alias); per-call headers (`X-PyAI-Voice`, `X-PyAI-Persona`) and the `configure` frame override profile config. Purely optional sugar over the zero-state primitive — distinct from the upcoming Agents feature (the no-code create/manage/track surface). 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/agents/{id}/tools":{"put":{"tags":["Agents"],"summary":"Bind tools to an agent","description":"Replace the agent's tool bindings. Each entry references a tool id from `GET /v1/tools`. Requires the `omni:session` scope.","operationId":"setAgentTools","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentToolBinding"}}}}},"responses":{"200":{"description":"Updated bindings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentToolBindingList"}}}},"400":{"description":"Invalid binding","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such agent or tool"}}}},"/v1/tools":{"get":{"tags":["Agents"],"summary":"List tools","description":"Org-owned custom tools plus PyAI prebuilt tools. Requires the `omni:session` scope.","operationId":"listTools","responses":{"200":{"description":"Tools","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Agents"],"summary":"Create a custom tool","description":"Register a custom tool. In `server` mode PyAI calls your `webhook_url` (signed, network-isolated) — works on phone calls; `client` mode runs on the WebSocket. The `hmac_secret` is returned once at creation. Requires the `omni:session` scope.","operationId":"createTool","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolCreate"}}}},"responses":{"201":{"description":"Created tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolCreated"}}}},"400":{"description":"Invalid field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/tools/{id}":{"get":{"tags":["Agents"],"summary":"Get a tool","operationId":"getTool","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tool"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such tool"}}},"post":{"tags":["Agents"],"summary":"Update a tool","operationId":"updateTool","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolUpdate"}}}},"responses":{"200":{"description":"Updated tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tool"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such tool"}}},"delete":{"tags":["Agents"],"summary":"Delete a custom tool","operationId":"deleteTool","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":"tool.deleted"},"tool_id":{"type":"string"},"deleted":{"type":"boolean","example":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such tool"}}}},"/v1/tools/{id}/test":{"post":{"tags":["Agents"],"summary":"Test a tool configuration","operationId":"testTool","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Dry-run result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolTestResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such tool"}}}},"/v1/tools/calls":{"get":{"tags":["Agents"],"summary":"List recent tool calls","description":"Audit log of recent tool invocations across the org's agents — tool name, execution mode, outcome, error and latency. No arguments or results are stored. Requires the `omni:session` scope.","operationId":"listToolCalls","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":500}}],"responses":{"200":{"description":"Tool calls","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolCallList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/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/omni/calls":{"get":{"tags":["Omni Calls"],"summary":"List Omni call records","description":"Recent Omni realtime sessions for this org, newest first. Each record carries the transcript, an optional recording URL, and an optional summary. Requires the `omni:read` scope. Filter by `session_label` (the opaque tag you passed on the connect URL).","operationId":"listOmniCalls","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Pass the previous page's `next_cursor`."},{"name":"session_label","in":"query","required":false,"schema":{"type":"string"},"description":"Only calls connected with this opaque session tag."}],"responses":{"200":{"description":"Paginated Omni call list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniCallList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/omni/calls/{call_id}":{"get":{"tags":["Omni Calls"],"summary":"Get an Omni call record","description":"Full record for one Omni session: transcript (inline or via `transcript_url`), `recording_url` when available, and `summary` when generated. Requires `omni:read`.","operationId":"getOmniCall","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Omni call detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniCall"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni/calls/{call_id}/recording":{"get":{"tags":["Omni Calls"],"summary":"Download an Omni call recording","description":"Fetch the call's audio recording, when one exists (recording is off by default). Requires `omni:read`.\n\nBy default this **`302`-redirects to a short-lived signed URL** for the audio (served directly from object storage — droppable into a browser `<audio src>`, and any HTTP client that follows redirects gets the bytes). Pass `?response=url` to receive the signed URL as JSON instead (handy for SDKs that embed it). On deployments without URL signing configured, the endpoint streams the audio bytes back inline (`Content-Type` reflects the stored format, e.g. `audio/wav`).\n\n`404` if there is no recording for the call.","operationId":"getOmniCallRecording","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string"}},{"name":"response","in":"query","required":false,"schema":{"type":"string","enum":["url"]},"description":"Set to `url` to return the signed URL as JSON instead of a 302 redirect."}],"responses":{"200":{"description":"The signed URL as JSON (`?response=url`), or the audio bytes streamed inline when signing is not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniCallRecording"}},"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"302":{"description":"Redirect to the short-lived signed recording URL (or a legacy externally-hosted recording)."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni/calls/{call_id}/summary":{"get":{"tags":["Omni Calls"],"summary":"Get an Omni call summary","description":"The structured post-call summary, when one was generated. `404` if there is no summary for the call. Requires `omni:read`.","operationId":"getOmniCallSummary","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Call summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniCallSummary"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni/sessions":{"post":{"tags":["Omni"],"summary":"Mint an ephemeral Omni session token","description":"Mint a short-lived, origin-locked token a browser can use to open ONE Omni realtime session **directly** — the public/private split for realtime. Call this from your server with your secret key (which must hold `omni:session`); never ship the secret key to a page. The returned `token` carries only `omni:session`, is limited to one concurrent session, is locked to the `allowed_origins` you supply, and expires after `ttl_seconds` (default 60s, max 600s). Use it as the WebSocket subprotocol `pyai-key.<token>` against the returned `url`. See the Omni browser guide for the full flow.","operationId":"createOmniSession","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["allowed_origins"],"properties":{"allowed_origins":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"description":"Browser origins (scheme://host[:port]) the token may connect from. Required and non-empty — a browser token must be origin-locked. `*` is not allowed.","example":["https://acme.com"]},"ttl_seconds":{"type":"integer","minimum":1,"maximum":600,"default":60,"description":"Token lifetime in seconds. Keep it short; a leaked token is worth seconds, not minutes."},"session_label":{"type":"string","description":"Optional opaque tag echoed back to your `kb_endpoint` and recorded on the call."}}}}}},"responses":{"201":{"description":"Minted session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniSession"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/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` or `rate=8000` for telephony) as binary frames and receive agent audio the same way. Use `rate=8000` for an 8 kHz G.711/Twilio leg so the only conversion is μ-law companding — no resampling (see the Telephony audio reference).\n\nRequires the `omni:session` scope (or the `omni:*` wildcard). The session is authorized by your key's **org** — there is nothing to create first. The optional `session_label` (alias: `agent_id`) is an opaque tag echoed to your own knowledge endpoint so you can branch per session; any value in your org's namespace is accepted (PyAI stores no per-agent state). The agent's behavior — voice, persona, knowledge endpoint — travels in the post-handshake `configure` frame, not in a pre-built record.\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":"session_label","in":"query","required":false,"schema":{"type":"string"},"description":"Optional opaque tag for this session, echoed to your own kb_endpoint so you can branch per session. The session is authorized by your key's org (PyAI stores no per-agent state); any value in your org's namespace is accepted. Must be safe as a header value (no control chars, ≤256 chars). Omit it entirely if you don't need per-session correlation."},{"name":"agent_id","in":"query","required":false,"schema":{"type":"string"},"description":"Deprecated alias for `session_label`. Accepted for back-compat; prefer `session_label`. If both are present, `session_label` wins."},{"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":[8000,16000,24000],"default":24000},"description":"Audio sample rate in Hz. Use `24000` for browser/WebRTC, `16000` for wideband telephony, or `8000` for an 8 kHz G.711/Twilio leg (μ-law companding only, no resampling). Load-bearing on the connect URL; the gateway preserves it verbatim."}],"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. Requires the `omni:session` scope; the session is authorized by your key's org. An optional `session_label` (alias: `agent_id`) is an opaque tag echoed to your own knowledge endpoint — there is nothing to pre-create. 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":"session_label","in":"query","required":false,"schema":{"type":"string"},"description":"Omni only. Optional opaque tag echoed to your own kb_endpoint (PyAI stores no per-agent state). Nothing to pre-create; the session is authorized by your key's org."},{"name":"agent_id","in":"query","required":false,"schema":{"type":"string"},"description":"Omni only. Deprecated alias for `session_label`; prefer `session_label`. If both are present, `session_label` wins."}],"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. 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. 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}}},"OmniCallListItem":{"type":"object","properties":{"object":{"type":"string","example":"omni.call"},"call_id":{"type":"string"},"session_label":{"type":"string","nullable":true,"description":"The opaque tag passed on the connect URL, if any."},"status":{"type":"string","enum":["completed","failed"]},"duration_s":{"type":"number","nullable":true},"has_recording":{"type":"boolean"},"has_summary":{"type":"boolean"},"started_at":{"type":"integer","nullable":true,"description":"Unix ms when the session started."},"created_at":{"type":"integer","description":"Unix ms when the record was written."}}},"OmniCall":{"allOf":[{"$ref":"#/components/schemas/OmniCallListItem"},{"type":"object","properties":{"transcript":{"type":"object","nullable":true,"description":"Inline transcript document; omitted in favor of transcript_url when offloaded."},"transcript_url":{"type":"string","description":"Signed URL to the transcript when it was offloaded instead of inlined."},"recording_url":{"type":"string","description":"Signed URL to the audio recording, when one exists."},"summary":{"type":"object","description":"Structured post-call summary, when generated."},"error":{"type":"string","description":"Present when status is failed."}}}]},"OmniCallList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/OmniCallListItem"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}},"OmniCallRecording":{"type":"object","properties":{"object":{"type":"string","example":"omni.call.recording"},"call_id":{"type":"string"},"url":{"type":"string","description":"Short-lived signed URL to the recording audio."},"expires_at":{"type":"integer","description":"Unix-ms expiry of the signed URL."},"expires_in":{"type":"integer","description":"Seconds until the URL expires."}}},"OmniCallSummary":{"type":"object","properties":{"object":{"type":"string","example":"omni.call.summary"},"call_id":{"type":"string"},"summary":{"type":"object","description":"The structured post-call summary document."}}},"OmniSession":{"type":"object","properties":{"object":{"type":"string","example":"omni.session"},"token":{"type":"string","description":"The ephemeral session token. Use as the WebSocket subprotocol `pyai-key.<token>`. Short-lived and origin-locked; safe to hand to the browser.","example":"pyai_live_sess_a1B2…"},"expires_at":{"type":"integer","description":"Token expiry, Unix epoch milliseconds."},"url":{"type":"string","description":"The Omni realtime WebSocket URL to connect to.","example":"wss://api.pyai.com/v1/omni?format=pcm16&rate=24000"},"session_label":{"type":"string","description":"Echoed back when supplied on the request.","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)."},"extraction_schema":{"type":["object","null"],"additionalProperties":true,"description":"JSON Schema of fields to capture from each completed call's transcript. When set with extraction_webhook_url, PyAI runs a post-call extraction pass and POSTs the structured JSON to your webhook (signed with X-PyAI-Signature). Null disables extraction."},"extraction_webhook_url":{"type":["string","null"],"format":"uri","description":"HTTPS URL that receives the signed post-call extraction result (event `omni.call.extracted`). Requires extraction_schema. The call's agent is resolved from the connect-URL session_label when it equals this agent's id."},"tools":{"type":["array","null"],"items":{"$ref":"#/components/schemas/AgentToolBinding"},"description":"Tool bindings for this agent profile (references tools from `GET /v1/tools`). Inline Omni `configure.tools[]` definitions are separate — see the Omni protocol docs."}}},"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"}},"extraction_schema":{"type":["object","null"],"additionalProperties":true,"description":"Post-call extraction JSON Schema, or null."},"extraction_webhook_url":{"type":["string","null"],"description":"Signed delivery target for post-call extraction, or null."},"tools":{"type":"array","items":{"$ref":"#/components/schemas/AgentToolBinding"}},"created_at":{"type":"integer","description":"Unix seconds."}}},"AgentToolBinding":{"type":"object","required":["tool_id"],"properties":{"tool_id":{"type":"string"},"enabled":{"type":"boolean","default":true},"config":{"type":"object","additionalProperties":true,"description":"Customer SETTINGS for this tool on this agent, keyed by the tool's config_schema field keys (e.g. { from_number, provider_api_key }). Fields marked secret are encrypted at rest and returned masked ('********'); re-send the mask (or omit) to keep the stored value."},"name":{"type":"string","description":"Present on agent reads when the tool resolves."},"description":{"type":"string"}}},"ToolConfigField":{"type":"object","required":["key","label","type"],"description":"One customer-supplied setting a tool needs to run (distinct from input_schema, which is the per-call model arguments).","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["string","number","boolean","select"]},"required":{"type":"boolean","default":false},"secret":{"type":"boolean","default":false,"description":"When true, the value is encrypted at rest and never returned in plaintext."},"help":{"type":"string"},"placeholder":{"type":"string"},"options":{"type":"array","items":{"type":"string"},"description":"Allowed values when type is 'select'."}}},"ToolConfigSchema":{"type":"object","description":"Declares the settings a tool needs. Render these as a form in your agent builder; capture answers on the agent's tool binding config.","properties":{"fields":{"type":"array","items":{"$ref":"#/components/schemas/ToolConfigField"}}}},"AgentToolBindingList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentToolBinding"}}}},"ToolCreate":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"config_schema":{"allOf":[{"$ref":"#/components/schemas/ToolConfigSchema"}],"description":"Optional. Declare customer settings this tool needs (captured per agent on the binding config)."},"webhook_url":{"type":"string","format":"uri","description":"HTTPS endpoint for the tool. Validated against an SSRF allow-list at registration (public HTTPS only; private/loopback/metadata rejected)."},"execution":{"type":"string","enum":["server","client"],"description":"How the tool runs. server = the Platform tool-executor calls webhook_url directly (default for endpoint tools; works on telephony). client = your connected app executes it (legacy client-loop). Defaults to server when webhook_url is set."},"auth_header":{"type":"string","description":"Header name the executor injects the webhook auth value into (e.g. Authorization). Pair with auth_secret."},"auth_secret":{"type":"string","description":"Auth value for your webhook (e.g. 'Bearer ...'). Stored encrypted at rest, injected by the executor, never echoed back."},"side_effect":{"type":"string","enum":["read","action"]},"timeout_ms":{"type":"integer","minimum":100,"maximum":15000,"default":5000}}},"ToolUpdate":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"config_schema":{"allOf":[{"$ref":"#/components/schemas/ToolConfigSchema"}]},"webhook_url":{"type":["string","null"],"format":"uri"},"execution":{"type":"string","enum":["server","client"]},"auth_header":{"type":["string","null"]},"auth_secret":{"type":["string","null"],"description":"Replace the stored webhook auth value, or null to clear it."},"side_effect":{"type":"string","enum":["read","action"]},"timeout_ms":{"type":"integer","minimum":100,"maximum":15000},"status":{"type":"string","enum":["active","disabled"]}}},"Tool":{"type":"object","properties":{"object":{"type":"string","example":"tool"},"id":{"type":"string"},"org_id":{"type":["string","null"]},"name":{"type":"string"},"kind":{"type":"string","enum":["prebuilt","custom"]},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"config_schema":{"allOf":[{"$ref":"#/components/schemas/ToolConfigSchema"}],"description":"Customer settings this tool needs to run. Render as a form in your builder; save answers on the agent binding config."},"webhook_url":{"type":["string","null"]},"execution":{"type":"string","enum":["hosted","server","engine","client"],"description":"How the tool runs. hosted = PyAI runs it (prebuilt read catalog). server = the tool-executor calls your webhook. engine = the Omni call engine runs it natively (call control: transfer_to_human, send_dtmf, play_hold, collect, end_call) — not routed to the executor. client = your connected app."},"auth_header":{"type":["string","null"]},"has_auth":{"type":"boolean","description":"Whether a webhook auth secret is configured (the secret itself is never returned)."},"side_effect":{"type":"string","enum":["read","action"]},"timeout_ms":{"type":"integer"},"status":{"type":"string","enum":["active","disabled"]},"created_at":{"type":"integer"}}},"ToolCreated":{"allOf":[{"$ref":"#/components/schemas/Tool"},{"type":"object","properties":{"hmac_secret":{"type":"string","description":"Returned once at creation for webhook signature verification."}}}]},"ToolList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Tool"}},"has_more":{"type":"boolean","example":false}}},"ToolTestResult":{"type":"object","properties":{"object":{"type":"string","example":"tool.test"},"tool_id":{"type":"string"},"name":{"type":"string"},"ok":{"type":"boolean"},"message":{"type":"string"}}},"ToolCall":{"type":"object","description":"One tool invocation audit record (no arguments or results stored).","properties":{"id":{"type":"string"},"agent_id":{"type":"string","nullable":true},"tool_name":{"type":"string"},"execution":{"type":"string","enum":["hosted","server","client"],"nullable":true},"ok":{"type":"boolean"},"error":{"type":"string","nullable":true},"latency_ms":{"type":"integer","nullable":true},"call_id":{"type":"string","nullable":true},"ts":{"type":"integer","description":"Unix epoch milliseconds."}}},"ToolCallList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ToolCall"}},"has_more":{"type":"boolean","example":false}}},"AgentList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}},"has_more":{"type":"boolean","example":false}}}}}}