{"openapi":"3.1.0","info":{"title":"PyAI API","version":"2.3.1","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- **Cast**, auto-directed expressive voiceovers · `/v1/cast`\n- **Cue (not active on the serving stream)**, reserved grounding configuration for `GET /v1/audio/transcriptions/stream`\n- **Omni**, full-duplex agentic voice (speech-to-speech, grounded in your knowledge bases + tools) · `/v1/omni`\n- **Knowledge Bases**, hosted grounding for Omni: create bases, add documents (file, URL, or text), crawl a public website, bind to agents or org defaults · `/v1/knowledgebases`\n- **AMD API**, answering-machine detection: know *who or what* answered a call (human, voicemail, IVR, iPhone/Google screening, dead number) with the reason it decided · `wss …/v1/amd/stream` (Twilio Media Streams drop-in), `POST /v1/amd/config`, `GET /v1/amd/calls/{id}`\n- **Agents Beta**, the live console feature to create, configure, test, and connect Omni voice agents without code. Beta features and limits may change.\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). `POST /v1/sandbox/keys` creates an instant, short-lived test key without login or billing. Account signup also creates a sandbox key. Live keys consume prepaid credit; phone verification may unlock promotional credit under graduated-signup rules, but credit is not guaranteed at signup.\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). Your key is authenticated on the upgrade and never reaches the model.\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-speak\",\"input\":\"Hello from PyAI.\",\"voice\":\"voice_abc\"}' \\\n  --output speech.wav\n```\n\n`voice` is a stock voice id from `GET /v1/voices` (the curated prebuilt catalog with personas and avatars) 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. When `session_label` equals a **`/v1/agents` profile id**, the engine loads persona, voice, and **greeting message** from that profile (turn-0 playback). `format` and `rate` are load-bearing on the connect URL (the SDK sets them). Prefix each PCM16 frame with byte `0x01`; prefix control JSON with byte `0x03`. **Optional convenience:** pre-store config via `POST /v1/agents` (including `greeting`, `consent_line`, `recordings_enabled`) and pass its id as `session_label`, or send everything inline in the post-handshake `configure` frame. Not required to connect.\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| `hear:configure` | `GET`/`PUT /v1/hear/vocabulary` |\n| `speak:synthesize` | `POST /v1/audio/speech` (Speak) |\n| `speak:clone` | `/v1/voice/clones` (Speak) |\n| `speak:design` | `/v1/voice/design` (Speak) |\n| `omni:session` | `/v1/omni` and `POST /v1/omni/sessions` (mint a browser session token) |\n| `omni:read` | `/v1/omni/calls` (Omni post-call records) |\n| `kb:manage` | `/v1/knowledgebases/*` (hosted knowledge bases for Omni grounding) |\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 and speaker-role corrections) |\n| `amd:detect` | `wss …/v1/amd/stream` (AMD realtime detection, Twilio drop-in) |\n| `amd:configure` | `/v1/amd/config` (AMD operating-point dial + webhook) |\n| `amd:read` | `/v1/amd/calls` (AMD decision records) |\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:*`, `speak:*`, …, 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; **beta** = available now with features or limits that may change; **unavailable** = reserved in the contract but not active on the serving route.\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 (vocabulary settings) | `GET`/`PUT /v1/hear/vocabulary` | Bearer | `hear:configure` | 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` | unavailable |\n| Hear (async batch) | `POST`/`GET /v1/transcription/jobs` | Bearer | `transcribe:jobs` | live |\n| Speak (TTS) | `POST /v1/audio/speech` | Bearer | `speak:synthesize` | live |\n| Speak (cloning) | `GET`/`POST /v1/voice/clones` | Bearer | `speak:clone` | live |\n| Speak (design) | `/v1/voice/design` | Bearer | `speak:design` | live |\n| Omni | `wss …/v1/omni?session_label=` | Subprotocol | `omni:session` | live |\n| Agent profiles (optional config) | `/v1/agents`, `/v1/agents/{id}` | Bearer | `omni:session` | live |\n| Knowledge Bases (hosted grounding) | `/v1/knowledgebases/*`, `PUT /v1/agents/{id}/knowledgebases` | Bearer | `kb:manage` (`omni:session` for the binding) | live |\n| Trace (config) | `/v1/trace/config`, `/v1/trace/rule-packs` | Bearer | `trace:configure` | beta |\n| Trace (reads) | `/v1/trace/interactions`, `/violations`, `/findings`, `/exposure` | Bearer | `trace:read` | beta |\n| Recap (config) | `/v1/recap/config` | Bearer | `recap:configure` | live |\n| Recap (CRM) | `/v1/recap/crm-config` | Bearer | `recap:configure` | live |\n| Integrations (Zapier) | `/v1/integrations/events`, `/v1/integrations/zapier/hooks` | Bearer | _any active key_ | 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| AMD (stream) | `wss …/v1/amd/stream` (Twilio Media Streams drop-in) | TwiML `<Parameter name=\"api_key\">` (from Twilio) or subprotocol (server-side) | `amd:detect` | live |\n| AMD (config) | `GET`/`POST /v1/amd/config` | Bearer | `amd:configure` | live |\n| AMD (reads) | `GET /v1/amd/calls`, `/v1/amd/calls/{id}` | Bearer | `amd:read` | live |\n| Telephony | `/v1/telephony/*` | Bearer | `telephony:manage` | live |\n| Agents (console builder) | `https://console.pyai.com/agents` | Console session |, | beta |\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. Managed-number calls return 404 until the PyAI network is enabled 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 (Omni), and billed against your plan and credits. List prices: Hear $0.001/min (async Transcribe $0.0005/min), Speak $0.04/min, Omni $0.05/min including speech plus brain, and Agents Live Beta $0.08/min. Managed telephony is separate at $0.01/min. English Natural (`en1`) is available on Speak (streaming and buffered) and Omni; Omni acknowledges the canonical id and `voice_tier: natural`. Hindi uses the Standard-tier voices `hi1`–`hi4` on Omni; the former Hindi Natural aliases (`hi5`–`hi8`) are retired and no longer in the catalog. Standard and Natural voices are included in their product's base rate with no voice-tier add-on. The AMD API bills per **answered** call, the first 5,000 answered calls each month are free, then $0.004/answered call (no-answers, busies, and failed calls are free; AMD bundled with PyAI telephony/Omni is included at no charge). AI products (Hear, Speak, 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. 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":"Omni","description":"The flagship: build an AI voice agent with one WebSocket (`GET /v1/omni`) and one `configure` frame, nothing to pre-create. This group also holds the optional browser-token mint and the post-call records."},{"name":"Knowledge Bases","description":"Hosted knowledge bases for Omni grounding: create a base, add documents (file upload, URL fetch, or pasted text), then bind it to agent profiles or set org-wide defaults. Bound bases are retrieved per turn, no `kb_endpoint` of your own required."},{"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":"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":"Cast","description":"Auto-directed, expressive multi-line voiceover projects and asynchronous renders."},{"name":"Transcription Jobs","description":"Async batch transcription"},{"name":"Agents","description":"Agent profiles used by the live Agents Beta console and available directly through the API. Store Omni session config (persona, greeting, voice, conversation knobs) and reference it by id instead of sending a full `configure` frame each call. Profiles remain optional for direct `/v1/omni` integrations."},{"name":"Trace","description":"Compliance & guardrails: per-agent config, rule packs, and the exposure / violations / interaction-evidence read views"},{"name":"AMD","description":"Answering-machine detection: know who or what answered a call (human, voicemail, IVR, iPhone/Google screening, dead number), with the reason it decided. Twilio Media Streams drop-in over `wss …/v1/amd/stream`; one operating-point dial; billed per answered call."},{"name":"Telephony","description":"Managed phone numbers: search, provision, route to an agent, and release. Call minutes bill on telephony.minutes ($0.01/min)."},{"name":"WhatsApp","description":"WhatsApp Business Calling: register a WhatsApp Business number, enable calling, and let an Omni agent answer (and, with the user's permission, place) WhatsApp voice calls. Requires the `telephony:manage` scope."},{"name":"Call Integrations","description":"Signed provider webhooks that import completed calls into Hear, Recap, and offline Trace."}],"paths":{"/auth/signup":{"post":{"tags":["Identity"],"summary":"Create an account and organization","operationId":"signup","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email","maxLength":320},"password":{"type":"string","minLength":8},"name":{"type":"string","maxLength":200},"org_name":{"type":"string","maxLength":200},"referral_token":{"type":"string","maxLength":2048,"description":"Optional signed first-touch attribution token. Invalid, expired, or tampered values never block signup and receive no attribution."}}}}}},"responses":{"201":{"description":"Account and organization created."},"400":{"description":"Invalid account input."},"429":{"description":"Signup anti-abuse limit reached."}}}},"/public/integrations/calls/aircall/events":{"post":{"tags":["Call Integrations"],"summary":"Receive Aircall call events","description":"Receives `call.ended` and `call.comm_assets_generated`. The Aircall subscription token in the payload identifies and authenticates the connection. Dark unless call integrations and the Aircall provider gate are enabled.","operationId":"ingestAircallCallEvent","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event","timestamp","token","data"]}}}},"responses":{"202":{"description":"Accepted or idempotently coalesced."},"401":{"description":"Invalid token or stale delivery."}}}},"/public/integrations/calls/dialpad/events":{"post":{"tags":["Call Integrations"],"summary":"Receive Dialpad call events","description":"Receives `hangup`, `recording`, and `call_transcription` states as an HS256 JWT. Unsigned JSON is rejected. Dark unless call integrations and the Dialpad provider gate are enabled.","operationId":"ingestDialpadCallEvent","security":[],"parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"Opaque connection-routing token generated by PyAI."}],"requestBody":{"required":true,"content":{"application/jwt":{"schema":{"type":"string"}}}},"responses":{"202":{"description":"Accepted or idempotently coalesced."},"401":{"description":"Invalid routing token, JWT, or stale delivery."}}}},"/public/integrations/calls/justcall/events":{"post":{"tags":["Call Integrations"],"summary":"Receive JustCall completed-call events","description":"Receives JustCall `call.completed` with its documented v1 signature. Existing Phase 1 behavior is unchanged.","operationId":"ingestJustCallCompletedEvent","security":[],"parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"202":{"description":"Accepted or duplicate."},"401":{"description":"Invalid token or signature."}}}},"/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)**, includes Hear/Speak/Omni/AMD/Recap starter scopes, enables Recap on the new org, 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."},"referral_token":{"type":"string","maxLength":2048,"description":"Optional signed first-touch attribution token."}}}}}},"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":"Starter scopes: Hear, Speak, Omni, AMD, Recap, and Cast.","example":["hear:transcribe","hear:stream","hear:configure","transcribe:jobs","speak:synthesize","omni:session","amd:detect","amd:configure","amd:read","cast:render","recap:configure","recap:read"]},"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":"OpenAI-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","enum":["en","es","fr","de","hi","it","pt","nl"],"description":"ISO-639-1 language code. Omit for English. Values outside the published set are rejected with `400 unsupported_language`, never silently auto-detected."},"numerals":{"type":"boolean","description":"Tri-state inverse-text normalization for English **final** transcripts (never interim partials). `true` renders spoken numbers as digits (phones, currency, dates, ordinals). `false` keeps those spans in spoken form. Omitted keeps the live engine default: number formatting is ON for finals. Independent of `smart_format`."},"smart_format":{"type":"boolean","default":false,"description":"Opt-in English punctuation and sentence capitalization on **final** transcripts only. Interim partials are never formatted. May change only case and punctuation; any failure returns the unformatted final. Default `false`. Independent of `numerals`. Non-English requests are unchanged."},"dictation":{"type":"boolean","default":false,"description":"Opt-in spoken punctuation commands on English **final** transcripts only: `period`, `comma`, `new paragraph`, and `question mark`. Separate from `smart_format` and off by default. Interim partials are never rewritten."},"drop_fillers":{"type":"boolean","default":false,"description":"Opt-in stripping of filled pauses (`um`, `uh`, `umm`, `uhh`, `er`) on English **final** transcripts. Off by default. Do not enable on legal or compliance audio by default. Interim partials are never rewritten."},"vocabulary":{"type":"string","description":"Per-call phrase list (max 32 phrases, 64 characters each). Re-cases matching spans to the supplied form and boosts those phrases for this request only. Not a stored project glossary. Send a comma-separated list or a JSON array string."},"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 streaming speech-to-text with eager partials (PyAI Hear first partial measured at about 200 ms in-region. It is revisable and not an SLA). Requires the `hear:stream` scope. The only frame protocol is `pyai-hear-v1`, the published frame catalog below. Cue grounding configuration is reserved but is not active on the serving route.\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\nSet `vocabulary` only when the session has known names, brands, products, or other distinctive terms. This biases known vocabulary only when supplied for the session or enabled through the stored `hear_stream` profile. Request terms take priority and the effective list is fixed when the session opens.\n\n**Client -> server:** stream binary audio frames continuously (PCM16 at the negotiated `sample_rate`, or opus). Send a JSON `{\"type\":\"commit\"}` text frame to force-finalize the current utterance (e.g. when your VAD detects end-of-turn). Closing the socket also flushes a final for any buffered audio.\n\nSet `endpointing_ms` on the connect URL or send `{\"type\":\"config\",\"endpointing_ms\":800}` mid-session. It is the minimum trailing-pause length before an utterance may end. Turn detection may wait longer; an utterance always completes within `max(endpointing_ms, 1500 ms)` of streamed silence. Keep sending audio frames, including silence, while the caller pauses. Pausing the stream pauses the frame-driven clock.\n\n**Server -> client (JSON text frames):**\n\n| `type` | When | Payload |\n| --- | --- | --- |\n| `config_ack` | after connect-time or mid-session endpointing config | `{endpointing_ms, effective_floor_ms, effective_ceiling_ms, score_interval_ms, warnings}`, the applied values and any validation warnings |\n| `partial` | every eager tick | `{text, stable_text, active_text, utterance_id, t_ms}`, the live hypothesis for that `utterance_id` |\n| `partial_stable` | when a prefix locks in | `{text, utterance_id, t_ms}`, the portion the recognizer no longer expects to revise |\n| `speech_final` | on endpoint/commit | `{text, utterance_id, t_ms, audio_ms, endpoint_reason}`, stable, end of an utterance |\n| `final` | follows `speech_final` | `{text, utterance_id, t_ms, audio_ms, endpoint_reason}`, 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. Assert that `config_ack.warnings` is empty instead of inferring that a setting applied. Warning reasons include `clamped_to_range`, `not_a_number`, and `unknown_config_field`; invalid config leaves the session open. `endpoint_reason` reports why automatic endpointing fired, including `peak_te_early` for the high-confidence path and `silence_backstop` for the bounded fallback. Log it when tuning conversational turns. The Cue grounding config frame and grounding result fields are reserved but currently have no effect on the serving stream.\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.001/min), speech time derived from transcript timing rather than connection wall-clock.","operationId":"openTranscriptionStream","parameters":[{"name":"protocol","in":"query","required":false,"schema":{"type":"string","const":"pyai-hear-v1","default":"pyai-hear-v1"},"description":"Canonical frame protocol. Omit to use the same `pyai-hear-v1` default."},{"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","enum":["en","es","fr","de","hi","it","pt","nl"]},"description":"ISO-639-1 language code. Omit for English. The language is forced for the session. Values outside the published set are rejected at the upgrade with `400 unsupported_language`."},{"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"},"description":"Tri-state inverse-text normalization for English **final** transcripts (never interim partials). `true` renders spoken numbers as digits (phones, currency, dates, ordinals). `false` keeps those spans in spoken form. Omitted keeps the live engine default: number formatting is ON for finals. Independent of `smart_format`."},{"name":"smart_format","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Opt-in English punctuation and sentence capitalization on **final** transcripts only. Interim partials are never formatted. May change only case and punctuation; any failure returns the unformatted final. Default `false`. Independent of `numerals`. Non-English requests are unchanged."},{"name":"dictation","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Opt-in spoken punctuation commands on English **final** transcripts only: `period`, `comma`, `new paragraph`, and `question mark`. Separate from `smart_format` and off by default. Interim partials are never rewritten."},{"name":"drop_fillers","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Opt-in stripping of filled pauses (`um`, `uh`, `umm`, `uhh`, `er`) on English **final** transcripts. Off by default. Do not enable on legal or compliance audio by default. Interim partials are never rewritten."},{"name":"vocabulary","in":"query","required":false,"schema":{"type":"string","maxLength":2048},"description":"Optional per-session terms for known names, brands, products, and other distinctive phrases. PyAI trims entries, removes case-insensitive duplicates, and keeps the first spelling and order. Entries shorter than 4 characters, longer than 64 characters, longer than 5 words, or made only of common words are ignored. At most 5 effective terms are used. When stored vocabulary is enabled for `hear_stream`, request terms come first and stored suggestions fill any remaining slots. The effective list is fixed for the session and does not select transcription language. Send a comma-separated list or a JSON array string."},{"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":"Minimum trailing-pause length in milliseconds (50-5000, clamped) before an utterance may end. Turn detection may wait longer; an utterance always completes within `max(endpointing_ms, 1500 ms)` of streamed silence. The same setting can be changed mid-session with `{\"type\":\"config\",\"endpointing_ms\":800}`. Both paths emit `config_ack`; assert that `warnings` is empty. A non-empty `warnings` means a value was not applied verbatim, but the session remains open. Timing counts only audio you stream, including silence, so pausing the stream pauses the clock. `{\"type\":\"commit\"}` still forces immediate end-of-turn on the STT socket."},{"name":"call_id","in":"query","required":false,"schema":{"type":"string"},"description":"Stable call identifier used for the post-call Recap when Recap is enabled. Omit to use the stream session id."},{"name":"pack_id","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z0-9_]+$"},"description":"Optional Recap pack for the post-call record."},{"name":"call_direction","in":"query","required":false,"schema":{"type":"string","enum":["inbound","outbound"]},"description":"Optional call direction attached to the post-call Recap."}],"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/hear/vocabulary":{"get":{"tags":["Hear"],"summary":"Get stored Hear vocabulary","description":"Return the organization's stored custom vocabulary and its explicit activation profiles. Stored terms do not affect any transcription unless `enabled_for` includes `batch` or `hear_stream`. Requires the `hear:configure` scope.","operationId":"getHearVocabulary","responses":{"200":{"description":"Stored Hear vocabulary settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HearVocabulary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"put":{"tags":["Hear"],"summary":"Set stored Hear vocabulary","description":"Replace the organization's stored custom vocabulary and activation profiles. PyAI keeps at most five sanitized terms. Stored terms bias known vocabulary only for the profiles named in `enabled_for`. Per-job or per-session vocabulary comes first, then stored suggestions fill remaining slots up to five. Requires the `hear:configure` scope.","operationId":"setHearVocabulary","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HearVocabularyInput"}}}},"responses":{"200":{"description":"Updated Hear vocabulary settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HearVocabulary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/audio/speech":{"post":{"tags":["Speak"],"summary":"Synthesize speech","description":"OpenAI-compatible text-to-speech. Returns audio bytes. The serving adapter defaults to incremental delivery from the streaming lane. Set `stream: false` when you require a complete buffered body with `Content-Length`. Requires the `speak:synthesize` scope.","operationId":"createSpeech","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["input"],"properties":{"model":{"type":"string","default":"pyai-speak","description":"Use `pyai-speak`. The OpenAI `tts-1` and `tts-1-hd` aliases remain accepted for drop-in compatibility."},"input":{"type":"string","description":"Text to synthesize."},"voice":{"type":"string","description":"A stock voice id from `GET /v1/voices` (e.g. `stock_dorit_en_us`) 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":"wav","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","description":"Reserved but not active on Speak. Sending this field currently returns `400 unsupported_parameter`."},"emotion":{"type":"string","description":"Optional emotion for voices that support expressive rendering (cloned voices and the multilingual natural tier): one of `neutral`, `happy`, `sad`, `angry`, `fearful`, `surprised`. The emotion is rendered in the same voice. Voices that do not support it reject the field with `unsupported_parameter`. Intensity is not exposed on Speak; use Cast for graded intensity."},"stream":{"type":"boolean","default":true,"description":"Use incremental audio delivery. The default is `true`: the adapter forwards validated chunks as they arrive. Set `false` to use the blocking lane and receive a complete buffered body. Voices whose catalog `synthesis_modes` is only `async` require `false`."},"seed":{"type":"integer","description":"Reserved but not active on Speak. Sending this field currently returns `400 unsupported_parameter`."},"temperature":{"type":"number","description":"Reserved but not active on Speak. Sending this field currently returns `400 unsupported_parameter`."}}}}}},"responses":{"200":{"description":"Audio bytes. Bytes are delivered incrementally by default; `stream: false` returns a complete buffered body. 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 (with presentation metadata; some entries include an avatar or pre-generated audio preview) merged with your saved **designed** voices, each tagged by `source` (`stock` | `design`). Any active key may read it (no specific scope). Inspect `available_on` before using a stock voice with Speak or Omni, and inspect `synthesis_modes` before choosing streaming or async Speak delivery. `tier` is the customer-facing quality tier. `pricing` reports only the voice-specific amount on top of that product's base rate. `aliases` are permanent convenience inputs on the advertised surfaces; responses report the canonical `voice_id`. 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":"language","in":"query","required":false,"schema":{"type":"string","enum":["en","fr","es","de","hi"]},"description":"Filter stock voices by stable language code."},{"name":"tier","in":"query","required":false,"schema":{"type":"string","enum":["standard","natural"]},"description":"Filter stock voices by customer-facing quality tier."},{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Rank stock voices by matching tokens across name, aliases, accent, language, age band, pitch, tone, use cases, and search tags."},{"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":"Canonical stock voice id or permanent alias. Alias lookups return the canonical `voice_id`."}],"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 message**, voice, **language**, recording disclosure, 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); per-call headers (`X-PyAI-Voice`, `X-PyAI-Persona`) and the `configure` frame override profile config. The engine speaks `greeting` at turn 0 and `consent_line` before recording when enabled. 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 or update a custom tool","description":"Register a custom tool. **Idempotent upsert on `(org, name)`:** if your org already has a tool with this `name`, it is updated in place (HTTP `200`, no duplicate, `hmac_secret` preserved), so re-syncing the same tool from a multi-tenant deploy is safe; otherwise a new tool is created (HTTP `201`, `hmac_secret` returned once). A prebuilt/global tool of the same name never blocks creating your org's tool. In `server` mode PyAI calls your `webhook_url` (signed, network-isolated), works on phone calls; `client` mode runs on the WebSocket. Requires the `omni:session` scope.","operationId":"createTool","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolCreate"}}}},"responses":{"200":{"description":"Updated existing tool (idempotent upsert by name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tool"}}}},"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","description":"Update a tool by id. Pass `rotate_secret: true` to mint a new webhook signing secret, returned once as `hmac_secret`. Requires the `omni:session` scope.","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 (includes `hmac_secret` only when `rotate_secret` was set)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolCreated"}}}},"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":25,"minimum":1,"maximum":100}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"minimum":0,"maximum":10000}},{"name":"search","in":"query","required":false,"description":"Case-insensitive search across tool name, agent ID, execution mode, error and call ID. Tool arguments and results are never searched.","schema":{"type":"string","maxLength":100}}],"responses":{"200":{"description":"Tool calls","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolCallList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/webhooks/signing-secret":{"get":{"tags":["Agents"],"summary":"Get webhook signing secret status","description":"Whether your org has a per-org webhook signing secret used to verify post-call extraction (and transcription-job) webhooks. Returns status only, never the secret value. Requires the `omni:session` scope.","operationId":"getWebhookSigningSecret","responses":{"200":{"description":"Secret status","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"webhook_signing_secret"},"configured":{"type":"boolean","description":"True once a per-org secret is minted; otherwise PyAI signs with the platform-wide fallback."},"last4":{"type":["string","null"],"description":"Last 4 chars of the active secret for identification; null when not configured."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Agents"],"summary":"Mint or rotate the webhook signing secret","description":"Mint (or rotate) your org's webhook signing secret and return it **once**. Verify the `X-PyAI-Signature` on post-call extraction (and transcription-job) webhooks with it: HMAC-SHA256 over `\"<t>.<rawBody>\"`. Zero-drop rotation: deploy verification that accepts both the old and new secret, call this, then drop the old one. Requires the `omni:session` scope.","operationId":"rotateWebhookSigningSecret","responses":{"200":{"description":"The new secret (shown once)","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"webhook_signing_secret"},"secret":{"type":"string","description":"The signing secret (`whsec_…`). Stored server-side; shown only in this response."},"created_at":{"type":"integer"}}}}}},"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. Send the audio in the canonical multipart field `file`; SDKs, examples, and the console use this field. EN-only today. Requires the `speak: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":"A clear 6–15 second reference clip in WAV, MP3, M4A/MP4, AAC, WebM, OGG, or FLAC format (50 MB maximum)."}}}}}},"responses":{"201":{"description":"Created voice","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Voice"}}}},"400":{"description":"Missing, empty, or ambiguous upload (`missing_file`, `empty_file`, or `ambiguous_upload`). The error `param` is `file`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"description":"The upload exceeds 50 MB (`file_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"The upload is not a supported audio container (`unsupported_file_type`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The reference audio did not pass enrollment validation (`invalid_reference_audio`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Voice enrollment is temporarily unavailable (`enrollment_unavailable`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/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 `speak: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. Send a stable `Idempotency-Key` when retrying create: the same organization and key receive the original response without another job or billable unit. Requires the `speak:design` scope.","operationId":"createDesign","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[^\\s]+$"},"description":"Customer-stable retry token. Reuse it only for retries of the same create request."}],"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 `speak: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}/preview/{candidate_id}":{"get":{"tags":["Speak"],"summary":"Preview a designed-voice candidate","description":"Fetch a short-lived audio preview for a candidate owned by the authenticated organization. Cross-organization and malformed lookups return the same not-found response. Requires the `speak:design` scope.","operationId":"previewDesignCandidate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Design job id."},{"name":"candidate_id","in":"path","required":true,"schema":{"type":"string"},"description":"Candidate id returned by the completed job."}],"responses":{"200":{"description":"Candidate preview audio.","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such design candidate 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 `speak: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/cast/capabilities":{"get":{"tags":["Cast"],"summary":"Get live Cast capabilities","description":"Returns the voice IDs, emotions, intensity tiers, and languages currently available to Cast. Requires `cast:render`. Clients must render controls from this response instead of assuming every voice in the general catalog is Cast-compatible.","operationId":"getCastCapabilities","responses":{"200":{"description":"Current Cast capabilities, filtered by platform policy.","content":{"application/json":{"schema":{"type":"object","required":["voices","emotions","intensity_tiers","languages"],"properties":{"voices":{"type":"array","items":{"type":"string"},"description":"Voice IDs accepted by Cast speech and render jobs."},"emotions":{"type":"array","items":{"type":"string"}},"intensity_tiers":{"type":"array","items":{"type":"string"}},"languages":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/cast/direct":{"post":{"tags":["Cast"],"summary":"Auto-direct a script","description":"Preserves non-empty newline-delimited performance units, or splits plain text by sentence, then assigns emotion and intensity to every unit. Requires `cast:render`.","operationId":"autoDirectCastScript","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","minLength":1}}}}}},"responses":{"200":{"description":"Directed lines.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CastLine"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/cast/speech":{"post":{"tags":["Cast"],"summary":"Preview one directed line","description":"Synchronously renders a single line for quick preview. Long-form renders must use render jobs. Requires `cast:render`.","operationId":"previewCastLine","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text","voice","emotion","intensity"],"properties":{"text":{"type":"string","minLength":1},"voice":{"type":"string","minLength":1},"emotion":{"type":"string","minLength":1},"intensity":{"oneOf":[{"type":"string"},{"type":"number"}]}}}}}},"responses":{"200":{"description":"WAV preview audio.","headers":{"x-pyai-units":{"schema":{"type":"number"},"description":"Billable Cast minutes."}},"content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"The requested voice is not supported by Cast (`unsupported_voice`).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/cast/render_jobs":{"post":{"tags":["Cast"],"summary":"Create a durable Cast render","description":"Persists and submits an asynchronous long-form render. Returns the job id and initial status; monitor progress and download the artifact from the Cast project in the console. Requires `cast:render`.","operationId":"createCastRenderJob","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["cast_project_id","voice","lines"],"properties":{"cast_project_id":{"type":"string"},"voice":{"type":"string","minLength":1},"lines":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/CastLine"}},"steps":{"type":"integer","minimum":1}}}}}},"responses":{"202":{"description":"Render accepted.","content":{"application/json":{"schema":{"type":"object","required":["job_id","cast_project_id","status","progress"],"properties":{"job_id":{"type":"string"},"cast_project_id":{"type":"string"},"status":{"type":"string","enum":["submitting","queued"]},"progress":{"type":"number"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Idempotency key reused with different input."},"422":{"description":"The requested voice is not supported by Cast (`unsupported_voice`).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/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; the input is processed transiently and never written to durable input storage)\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.\nCompleted JSON results include word- and segment-level offsets in decimal seconds from the decoded source-media timeline. Silence is not removed or compacted; resampling and internal chunking do not shift later offsets.\nSet `channel: true` for stereo (dual-channel) recordings to get exact speaker separation per channel. Use `diarize: true` for model-derived speaker labels on mono audio; do not set both.\nSet `vocabulary` only when this job has known names, brands, products, or distinctive terms. PyAI uses up to five sanitized terms. If stored vocabulary is enabled for `batch`, request terms come first and stored suggestions fill remaining slots.\n\n**Input limits:** multipart uploads are limited to 1 GiB; `audio_url` downloads are limited to 512 MiB. There is no separate media-duration ceiling. Inputs must contain a decodable audio stream; the stable output formats are JSON, SRT, and VTT.\n\n**Retention:** URL-fetched input bytes are not persisted. Uploaded input audio is retained for up to 7 days and result artifacts for up to 30 days. This endpoint has no `store: false` mode. `DELETE /v1/transcription/jobs/{id}` cancels queued/running work; it is not an erasure endpoint.\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. PyAI fetches the input transiently without writing it to durable input storage. Maximum response body: 512 MiB."},"model":{"type":"string","default":"pyai-hear-telephony"},"channel":{"type":"boolean","default":false,"description":"Dual-channel (stereo) separation. Channel 0 is labelled `speaker_1`, channel 1 `speaker_2`; labels are neutral and do not infer agent/customer roles. Do not combine with `diarize`."},"diarize":{"type":"boolean","default":false,"description":"Model-derived speaker separation for mono audio. Labels identify turns within this result, not stable people across separate jobs. Use `channel` instead for stereo recordings."},"numerals":{"type":"boolean","description":"Tri-state inverse-text normalization for English **final** transcripts (never interim partials). `true` renders spoken numbers as digits (phones, currency, dates, ordinals). `false` keeps those spans in spoken form. Omitted keeps the live engine default: number formatting is ON for finals. Independent of `smart_format`."},"smart_format":{"type":"boolean","default":false,"description":"Opt-in English punctuation and sentence capitalization on **final** transcripts only. Interim partials are never formatted. May change only case and punctuation; any failure returns the unformatted final. Default `false`. Independent of `numerals`. Non-English requests are unchanged."},"dictation":{"type":"boolean","default":false,"description":"Opt-in spoken punctuation commands on English **final** transcripts only: `period`, `comma`, `new paragraph`, and `question mark`. Separate from `smart_format` and off by default. Interim partials are never rewritten."},"drop_fillers":{"type":"boolean","default":false,"description":"Opt-in stripping of filled pauses (`um`, `uh`, `umm`, `uhh`, `er`) on English **final** transcripts. Off by default. Do not enable on legal or compliance audio by default. Interim partials are never rewritten."},"vocabulary":{"type":"array","items":{"type":"string","minLength":4,"maxLength":64},"maxItems":5,"description":"Optional per-job terms for known names, brands, products, and other distinctive phrases. PyAI trims entries, removes case-insensitive duplicates, and keeps the first spelling and order. Entries shorter than 4 characters, longer than 64 characters, sentence-shaped input, non-string entries, and conservative common words are ignored. At most the first 5 valid terms are used. Invalid entries do not reject the job. When stored vocabulary is enabled for `batch`, request terms come first and stored suggestions fill any remaining slots. The effective list applies only to this job and does not select transcription language."},"output_formats":{"type":"array","items":{"type":"string","enum":["json","srt","vtt"]},"default":["json"]},"webhook_url":{"type":"string","format":"uri","description":"HTTPS URL for `transcription.job.completed` or `transcription.job.failed`. PyAI POSTs `{type, created, data}` and signs the exact body in `X-PyAI-Signature: t=<unix_seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 over `<t>.<rawBody>`."},"trace":{"type":"boolean","default":false,"description":"Trace compliance add-on: deterministic PII scan + redaction over the final transcript (SSN, card numbers, CVV-in-context, email, US phone — the pii_v0 entity set). The result carries the redacted transcript plus a `trace` summary (verdict, PII count). Patterns run on the formatted transcript, so pair with the default `numerals` (digits) for full effect. Requires the org's Trace entitlement on this surface (else `402`); bills one Trace call. Not supported together with `diarize`/`channel`."},"rule_pack":{"type":"object","additionalProperties":true,"description":"Optional Trace rule pack (only used when `trace` is true). `entities` (or `redact`) narrows the scan to a subset of: ssn, credit_card, cvv, email, us_phone; unknown names are ignored and an empty selection means the full set."},"call_id":{"type":"string","description":"Optional stable call identifier for Recap; defaults to the transcription job id."},"pack_id":{"type":"string","pattern":"^[a-z0-9_]+$","description":"Optional Recap pack id."},"call_direction":{"type":"string","enum":["inbound","outbound"],"description":"Optional Recap call direction."},"customer_name":{"type":"string","description":"Optional Recap customer label."},"language":{"type":"string","enum":["en","fr","es","de","hi"],"description":"Optional Recap summarization language. It does not affect transcription: async jobs transcribe all eight Hear languages (`en`/`es`/`fr`/`de`/`hi`/`it`/`pt`/`nl`) and the spoken language is auto-detected per call."},"crm_fields":{"type":"object","additionalProperties":true,"description":"Optional CRM metadata delivered durably with the Recap trigger."}}}},"multipart/form-data":{"schema":{"type":"object","required":["audio"],"properties":{"audio":{"type":"string","format":"binary","description":"Audio file, maximum 1 GiB. Must contain a decodable audio stream."},"model":{"type":"string"},"channel":{"type":"string","enum":["true","false","stereo"]},"diarize":{"type":"string","enum":["true","false"]},"numerals":{"type":"string","enum":["true","false"],"description":"Tri-state inverse-text normalization for English **final** transcripts (never interim partials). `true` renders spoken numbers as digits (phones, currency, dates, ordinals). `false` keeps those spans in spoken form. Omitted keeps the live engine default: number formatting is ON for finals. Independent of `smart_format`."},"smart_format":{"type":"string","enum":["true","false"],"description":"Opt-in English punctuation and sentence capitalization on **final** transcripts only. Interim partials are never formatted. May change only case and punctuation; any failure returns the unformatted final. Default `false`. Independent of `numerals`. Non-English requests are unchanged."},"dictation":{"type":"string","enum":["true","false"],"description":"Opt-in spoken punctuation commands on English **final** transcripts only: `period`, `comma`, `new paragraph`, and `question mark`. Separate from `smart_format` and off by default. Interim partials are never rewritten."},"drop_fillers":{"type":"string","enum":["true","false"],"description":"Opt-in stripping of filled pauses (`um`, `uh`, `umm`, `uhh`, `er`) on English **final** transcripts. Off by default. Do not enable on legal or compliance audio by default. Interim partials are never rewritten."},"vocabulary":{"type":"string","maxLength":1024,"description":"Optional per-job terms for known names, brands, products, and other distinctive phrases. PyAI trims entries, removes case-insensitive duplicates, and keeps the first spelling and order. Entries shorter than 4 characters, longer than 64 characters, sentence-shaped input, non-string entries, and conservative common words are ignored. At most the first 5 valid terms are used. Invalid entries do not reject the job. When stored vocabulary is enabled for `batch`, request terms come first and stored suggestions fill any remaining slots. The effective list applies only to this job and does not select transcription language. Send a comma-separated list or a JSON array string."},"output_formats":{"type":"string","description":"Comma-separated, e.g. `json,srt`."},"webhook_url":{"type":"string","format":"uri","description":"HTTPS completion/failure webhook. Signed as documented on the JSON request."},"trace":{"type":"string","enum":["true","false"],"description":"Trace compliance add-on (see JSON body). Requires the Trace entitlement; not supported with diarize/channel."},"rule_pack":{"type":"string","description":"JSON-encoded Trace rule pack (only used when trace=true)."},"call_id":{"type":"string"},"pack_id":{"type":"string","pattern":"^[a-z0-9_]+$"},"call_direction":{"type":"string","enum":["inbound","outbound"]},"customer_name":{"type":"string"},"language":{"type":"string","enum":["en","fr","es","de","hi"]},"crm_fields":{"type":"string","description":"JSON-encoded CRM metadata delivered with the Recap trigger."}}}}}},"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). This operation does not erase completed results, uploaded input objects, logs, or backups.","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/integrations/events":{"get":{"tags":["Integrations"],"summary":"List integration events","description":"The completed-call event catalog for outbound integrations, with representative delivery payloads for field mapping. Any active key may call this.","operationId":"listIntegrationEvents","responses":{"200":{"description":"Event catalog","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationEventCatalogEntry"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/integrations/zapier/hooks":{"post":{"tags":["Integrations"],"summary":"Subscribe a Zapier REST hook","description":"Zapier REST-hook subscribe: delivers every completed call of the given event type to `target_url` as `{id, type, occurred_at, data}`, signed with `X-PyAI-Signature` and carrying an `Idempotency-Key`. Idempotent on `(org, event, target_url)` — re-subscribing refreshes in place (HTTP `201` with the same id). Deliveries are durable: bounded retries with backoff, then a visible dead letter. Any active key may call this.","operationId":"subscribeZapierHook","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZapierHookInput"}}}},"responses":{"201":{"description":"Subscribed hook","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationDestination"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/integrations/zapier/hooks/{id}":{"delete":{"tags":["Integrations"],"summary":"Unsubscribe a Zapier REST hook","description":"Zapier REST-hook unsubscribe. Only the owning org can delete its hooks.","operationId":"unsubscribeZapierHook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Unsubscribed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/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"},"call_direction":{"type":"string","enum":["inbound","outbound"]},"customer_name":{"type":"string"},"language":{"type":"string","enum":["en","fr","es","de","hi"],"description":"Language for the Recap output; this does not transcribe audio."},"crm_fields":{"type":"object","additionalProperties":true},"utterances":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/RecapUtterance"}}}}}}},"responses":{"202":{"description":"Recap job accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapCallSummary"}}}},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/v1/recap/calls/{call_id}/transcript/roles":{"post":{"tags":["Recap"],"summary":"Correct transcript speaker roles","description":"Tenant-scoped correction of Recap speaker roles. Requires `recap:read` and the Recap add-on. Marks each edited utterance as a customer correction; does not rename upstream carriers.","operationId":"correctRecapSpeakerRoles","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapSpeakerRoleCorrection"}}}},"responses":{"200":{"description":"Updated recap detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecapCall"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/amd/stream":{"get":{"tags":["AMD"],"summary":"Answering-machine detection (WebSocket)","description":"Realtime answering-machine detection over a WebSocket. **This surface speaks Twilio's Media Streams protocol natively** (`start` / `media` / `stop` frames, G.711 μ-law 8 kHz base64, ~20 ms), so migrating from Twilio AMD is a one-line-TwiML change, point the call's media at PyAI, keep your carrier and your code.\n\n```xml\n<Response><Start>\n  <Stream url=\"wss://api.pyai.com/v1/amd/stream\">\n    <Parameter name=\"api_key\" value=\"YOUR_PYAI_KEY\"/>\n    <Parameter name=\"aggressiveness\" value=\"0.25\"/>\n    <Parameter name=\"webhook\" value=\"https://you/amd-events\"/>\n  </Stream>\n</Start>\n  <!-- your existing call flow continues here -->\n</Response>\n```\n\nUse `<Start><Stream>`, NOT `<Connect><Stream>`. `<Start>` forks the audio and TwiML continues to your next verb, so the call still goes where it was going; `<Connect>` hands the media path to the socket and blocks TwiML until the stream ends, and because AMD is listen-only and never sends audio back the caller would hear dead air and a dialer would never reach the agent. (`<Connect>` is correct for Omni, which is a two-way voice agent.) Drop `machineDetection` from the call and keep your carrier.\n\nOn a Twilio-originated stream TWILIO owns the socket and relays only media/mark frames, so the pushed `amd` event does not reach you: a Twilio integration must read the decision from the `webhook` `<Parameter>` (or `GET /v1/amd/calls/{id}`). The socket push is for clients that drive the socket themselves.\n\nFrom Twilio, authenticate with the `api_key` `<Parameter>` shown above (Twilio strips query strings from the `<Stream>` URL and cannot send headers; PyAI verifies the key from the stream's `start` frame before processing any audio, and closes connections that never present a valid key). Server-side clients may instead authenticate at the handshake with the `Sec-WebSocket-Protocol: pyai-key.<API_KEY>` subprotocol or `?api_key=`. Requires the `amd:detect` scope. Mid-call, PyAI pushes an `amd` decision event on the socket (and to the per-call TwiML `webhook` parameter): `answered_by` (the routing class: `human`, `machine`, `sit_invalid`, `unknown`), `answered_by_twilio` (Twilio's exact `AnsweredBy` enum for drop-in routing parity), `confidence`, `decision_ms`, and a human-readable `reason`. A `machine` decision's subtype (`voicemail`, `ivr`, `screening`, `music`) is stored on the call record rather than pushed on the wire: read it with `GET /v1/amd/calls/{id}` or receive it on the account-wide `amd.call.completed` webhook (`webhook_url` in `POST /v1/amd/config`). The per-call `aggressiveness` `<Parameter>` overrides the account default from `POST /v1/amd/config`.\n\n\n### When the decision arrives\n\nMeasured over ~1,850 real answered calls (US telephony, 8 kHz μ-law), streamed at real time:\n\n| verdict | typical | 9 in 10 by |\n|---|---|---|\n| `human` | ~1.4 s | ~3.0 s |\n| `machine` | ~2.2 s | ~3.2 s |\n\nAbout 1 human decision in 4 lands under a second. There is a hard deadline at 6 s: if nothing is decisive by then you get a verdict anyway, `unknown` at a human-safe operating point. Size your fallback timer past 6 s, not past the typical case, and prefer reacting to the event over polling.\n\n### What to do with each verdict\n\n| `answered_by` | subtype | do |\n|---|---|---|\n| `human` | — | connect the agent |\n| `machine` | `voicemail` | safe to drop a message; wait for the record tone rather than assuming one |\n| `machine` | `ivr` | a phone tree, **do not drop a message** — navigate or abandon |\n| `machine` | `screening` | an AI screener (iPhone/Google) is relaying to a person; treat as a live-ish path, not voicemail |\n| `machine` | `music` | tonal audio and nothing transcribed — hold music or ringback, but also a greeting we failed to transcribe. Keep waiting; do **not** read it as a positive hold-music signal, and do not gate a drop on it |\n| `sit_invalid` | — | dead/invalid number, stop retrying it |\n| `unknown` | `silence` | answered but nothing came down the line, retry later rather than burning an agent slot |\n| `unknown` | — | no decisive evidence; your default decides |\n\n**Gate your voicemail drop on the subtype, not on `answered_by`.** `voicemail`, `ivr`, `screening` and `music` are all `machine`, and a message dropped into a phone tree or an AI screener is wasted. Drop only on `voicemail`: `music` means we heard tonal audio and never transcribed anything, which covers hold music and an untranscribed greeting alike, so it is not evidence either way. The subtype is on the call record and the `amd.call.completed` webhook, not on the wire event.\n\n### Choosing `aggressiveness`\n\nThe dial only changes what happens when the 6 s deadline is reached with nothing decisive; it never overrides positive evidence. Measured on the same corpus, calls that reach that deadline are roughly 85% machines, so:\n\n- **live-agent dialers** (a person hearing voicemail is cheap, a machine reaching an agent wastes a seat): keep it low. You get `unknown` and your own default decides.\n- **automated voicemail-drop bots** (nobody is waiting): raise it. You trade about 1 wrong `machine` in 7 of those deadline cases for never stalling.\n\nUndecided calls are ~2% of answered calls at the default operating point, so this dial affects a small tail either way.\n\nBilled per **answered** call (`amd.calls`), the first 5,000 answered calls/month are free, then $0.004/answered call; AMD bundled with PyAI telephony/Omni is included.","operationId":"amdStream","responses":{"101":{"description":"WebSocket upgrade, Twilio Media Streams protocol; PyAI emits `amd` decision events."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/amd/config":{"get":{"tags":["AMD"],"summary":"Get AMD config","description":"The org's AMD operating point (`aggressiveness`, 0-1) and webhook URL. Requires `amd:configure`.","operationId":"getAmdConfig","responses":{"200":{"description":"AMD config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmdConfig"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["AMD"],"summary":"Set AMD config","description":"Set the account-default operating point and webhook. `aggressiveness` is one dial on the ROC curve: near **0** is human-safe (never hang up on a person, for live-agent dialers; on the deadline it returns `unknown` rather than risk a false `machine`), near **1** fires `machine` fast (for AI voicemail-drop bots). A per-call TwiML `<Parameter name=\"aggressiveness\">` overrides this. Requires `amd:configure`.","operationId":"setAmdConfig","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmdConfigInput"}}}},"responses":{"200":{"description":"Updated AMD config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmdConfig"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/amd/calls":{"get":{"tags":["AMD"],"summary":"List AMD decisions","description":"Recent AMD decisions for this org, newest first. Requires `amd:read`. Filter by `session_label` (the opaque tag you passed on the connect URL / TwiML).","operationId":"listAmdCalls","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"}}],"responses":{"200":{"description":"Paginated AMD decision list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmdCallList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/amd/calls/{call_id}":{"get":{"tags":["AMD"],"summary":"Get an AMD decision","description":"The full decision for one call: `answered_by`, `answered_by_twilio`, `confidence`, `decision_ms`, and the word-level `reason`. Requires `amd:read`.","operationId":"getAmdCall","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"AMD decision detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmdCall"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni/calls":{"get":{"tags":["Omni"],"summary":"List Omni call records","description":"Recent Omni realtime sessions for this org, newest first. Each record has one stable, opaque PyAI call identifier plus recording and summary availability. Use that same `call_id` for detail, recording, transcript, and summary reads. 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"],"summary":"Get an Omni call record","description":"Full record for one Omni session, addressed by the stable opaque PyAI `call_id` returned by the list: transcript (inline or via `transcript_url`), recording availability, and `summary` when generated. Recording bytes are retrieved from the dedicated recording sub-resource. Requires `omni:read`.","operationId":"getOmniCall","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"$ref":"#/components/schemas/OmniCallId"}}],"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"],"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 the endpoint streams audio bytes from the tenant-scoped PyAI resource (`Content-Type` reflects the stored format, e.g. `audio/wav`). Pass `?response=url` to receive the canonical authenticated PyAI resource URL as JSON. Storage locations, raw recording URLs, and internal source identifiers are never returned.\n\n`404` if there is no recording for the call.","operationId":"getOmniCallRecording","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"$ref":"#/components/schemas/OmniCallId"}},{"name":"response","in":"query","required":false,"schema":{"type":"string","enum":["url"]},"description":"Set to `url` to return the canonical authenticated PyAI recording resource as JSON."}],"responses":{"200":{"description":"The canonical PyAI resource as JSON (`?response=url`), or the audio bytes streamed inline.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniCallRecording"}},"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni/calls/{call_id}/summary":{"get":{"tags":["Omni"],"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":{"$ref":"#/components/schemas/OmniCallId"}}],"responses":{"200":{"description":"Call summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniCallSummary"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni/calls/{call_id}/transcript":{"get":{"tags":["Omni"],"summary":"Get an Omni call transcript","description":"The full conversation transcript for the call. Resolves whether the transcript is stored inline or offloaded to an external URL, the response is always the transcript document itself. `404` if no transcript exists (e.g. the call failed before any speech, or the engine has not yet pushed the record). Requires `omni:read`.","operationId":"getOmniCallTranscript","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"$ref":"#/components/schemas/OmniCallId"}}],"responses":{"200":{"description":"Call transcript","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniCallTranscript"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni/sessions":{"post":{"tags":["Omni"],"summary":"Mint an ephemeral Omni session token","description":"**Most integrations do not need this endpoint.** Server-side apps, telephony, and backend agents connect to `wss /v1/omni` directly with their API key (see \"Open an Omni voice-agent session\"). Use this only when a browser or other untrusted client must open an Omni session without holding your secret key.\n\nIt mints a short-lived, origin-locked token the browser can use to open ONE Omni realtime session, 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/knowledgebases":{"get":{"tags":["Knowledge Bases"],"summary":"List knowledge bases","description":"All hosted knowledge bases in your organization. Requires the `kb:manage` scope.","operationId":"listKnowledgebases","responses":{"200":{"description":"Knowledge bases","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgebaseList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Knowledge Bases"],"summary":"Create a knowledge base","description":"Create a hosted knowledge base. Names are unique per organization (case-insensitive). Requires the `kb:manage` scope.","operationId":"createKnowledgebase","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":200},"embed_model":{"type":"string","description":"Embedding model id. Defaults to the platform standard; change only with guidance."}}}}}},"responses":{"201":{"description":"Knowledge base created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Knowledgebase"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"description":"A knowledge base with that name already exists in this organization."}}}},"/v1/knowledgebases/default":{"get":{"tags":["Knowledge Bases"],"summary":"List org-default KB bindings","description":"The knowledge bases every Omni session in your org grounds against when its agent has no specific binding. Requires the `kb:manage` scope.","operationId":"listKnowledgebaseDefaults","responses":{"200":{"description":"Default bindings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbBindingList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"put":{"tags":["Knowledge Bases"],"summary":"Set org-default KB bindings","description":"Replace the org-default knowledge base bindings. Omni sessions opened without an agent profile (or whose profile binds nothing) ground against these. Requires the `kb:manage` scope.","operationId":"setKnowledgebaseDefaults","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KbBinding"}}}}},"responses":{"200":{"description":"Default bindings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbBindingList"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/v1/knowledgebases/{id}":{"get":{"tags":["Knowledge Bases"],"summary":"Get a knowledge base (with documents)","operationId":"getKnowledgebase","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Knowledge base with its documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgebaseWithDocuments"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Knowledge Bases"],"summary":"Rename a knowledge base","operationId":"renameKnowledgebase","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Updated knowledge base","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Knowledgebase"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Name already in use in this organization."}}},"delete":{"tags":["Knowledge Bases"],"summary":"Delete a knowledge base","description":"Marks the knowledge base deleted; its documents and chunks are removed. Agents bound to it fall back to the org defaults. Requires the `kb:manage` scope.","operationId":"deleteKnowledgebase","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/knowledgebases/{id}/documents":{"get":{"tags":["Knowledge Bases"],"summary":"List documents in a knowledge base","operationId":"listKnowledgebaseDocuments","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbDocumentList"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Knowledge Bases"],"summary":"Add a document (file, URL, or text)","description":"Three ways to add content: a multipart file upload (pdf, docx, xlsx, txt, md, csv, html, json; 25MB max), a JSON `{ \"url\" }` to fetch and parse, or a JSON `{ \"text\" }` to paste content directly. The document registers as `pending` and is chunked + embedded asynchronously; poll its `status` until `indexed`. Requires the `kb:manage` scope.","operationId":"addKnowledgebaseDocument","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"text":{"type":"string"},"title":{"type":"string","maxLength":200}}}},"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"},"title":{"type":"string","maxLength":200}}}}}},"responses":{"202":{"description":"Document registered (pending ingestion)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbDocument"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"description":"File exceeds the 25MB limit."},"415":{"description":"Unsupported document type."}}}},"/v1/knowledgebases/{id}/crawls":{"post":{"tags":["Knowledge Bases"],"summary":"Crawl a public website into a knowledge base","description":"Discover same-origin pages from a public seed URL (robots.txt / sitemap, then a shallow link walk), rank them, and register each selected page as a normal URL document. Private, loopback, and metadata addresses are rejected. Defaults to 25 pages, hard-capped at 40. Already-ingested URLs in this knowledge base are reused. Poll document `status` until `indexed` before treating the site as ready. Requires the `kb:manage` scope.","operationId":"crawlKnowledgebaseWebsite","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Public http(s) seed URL."},"max_pages":{"type":"integer","minimum":1,"maximum":40,"default":25,"description":"Page cap after ranking. Defaults to 25, never above 40."}}}}}},"responses":{"202":{"description":"Pages registered (pending ingestion)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KnowledgebaseCrawl"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/knowledgebases/{id}/documents/{docId}":{"patch":{"tags":["Knowledge Bases"],"summary":"Edit a pasted-text document","description":"Edit in place and re-ingest. Only documents added as pasted text (`source: \"api\"`) can be edited; re-add files or URLs instead. Requires the `kb:manage` scope.","operationId":"updateKnowledgebaseDocument","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"docId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string"},"title":{"type":"string","maxLength":200}}}}}},"responses":{"202":{"description":"Updated and re-ingesting","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbDocument"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Knowledge Bases"],"summary":"Remove a document","operationId":"deleteKnowledgebaseDocument","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"docId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/knowledgebases/{id}/documents/{docId}/content":{"get":{"tags":["Knowledge Bases"],"summary":"Inspect a document's extracted content","description":"Returns the document, its source text (for pasted-text documents), and the first 200 indexed chunks, so you can verify what agents will retrieve. Requires the `kb:manage` scope.","operationId":"getKnowledgebaseDocumentContent","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"docId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document content and indexed chunks"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/knowledgebases/{id}/documents/{docId}/retry":{"post":{"tags":["Knowledge Bases"],"summary":"Retry document ingestion","description":"Re-queue a `pending` or `failed` document for chunking + embedding. Requires the `kb:manage` scope.","operationId":"retryKnowledgebaseDocument","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"docId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Re-queued for ingestion","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbDocument"}}}},"404":{"$ref":"#/components/responses/NotFound"},"503":{"description":"Document indexing is not configured on this deployment."}}}},"/v1/agents/{id}/knowledgebases":{"put":{"tags":["Agents"],"summary":"Bind knowledge bases to an agent","description":"Replace the agent's knowledge base bindings (with per-base retrieval weights). Omni sessions opened with `session_label` equal to this agent's id ground against these; sessions without an agent profile use the org defaults (`/v1/knowledgebases/default`). Requires the `omni:session` scope.","operationId":"setAgentKnowledgebases","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KbBinding"}}}}},"responses":{"200":{"description":"The agent's knowledge base bindings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbBindingList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/public/widgets/{publicId}":{"get":{"tags":["Agents"],"summary":"Resolve a hosted website widget","description":"Returns hosted runtime version 7, caller-only transcript capability, the published widget's safe display configuration, and public profile. Agent text is not inferred from audio. Requires an exact `Origin` header match. The opaque public id does not expose the organization, project, Agent, or credentials.","operationId":"getPublicAgentWidget","security":[],"parameters":[{"name":"publicId","in":"path","required":true,"schema":{"type":"string","pattern":"^wdgt_[A-Za-z0-9_-]{32,64}$"}},{"name":"Origin","in":"header","required":true,"schema":{"type":"string","format":"uri"}}],"responses":{"200":{"description":"Safe widget configuration","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/public/widgets/{publicId}/session":{"post":{"tags":["Agents"],"summary":"Mint a hosted widget voice session","description":"Validates the published widget, exact browser Origin, Agent/account status, credit posture, and durable per-widget/per-IP daily limits, then returns one short-lived origin-locked `omni:session` token. The request body cannot select an organization, project, or Agent.","operationId":"createPublicAgentWidgetSession","security":[],"parameters":[{"name":"publicId","in":"path","required":true,"schema":{"type":"string","pattern":"^wdgt_[A-Za-z0-9_-]{32,64}$"}},{"name":"Origin","in":"header","required":true,"schema":{"type":"string","format":"uri"}}],"responses":{"201":{"description":"Short-lived widget session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OmniSession"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"description":"Account credit is exhausted."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"Widget or visitor daily mint cap reached."}}}},"/v1/telephony/compliance-cases":{"post":{"tags":["Telephony"],"summary":"Start a number compliance or porting case","description":"Starts a project-scoped workflow for a new number or port-in request in the US, Canada, India, the UK, or Australia. The applicable requirements and execution budgets are frozen at creation. Missing market rules fail closed. Requires `telephony:manage` and an `Idempotency-Key`.","operationId":"createNumberComplianceCase","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCaseCreate"}}}},"responses":{"201":{"description":"Case created or idempotently replayed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCase"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Idempotency conflict."}}},"get":{"tags":["Telephony"],"summary":"List number compliance cases","operationId":"listNumberComplianceCases","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"Project-scoped cases","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/NumberComplianceCase"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/telephony/compliance-cases/{id}":{"get":{"tags":["Telephony"],"summary":"Get a number compliance case","operationId":"getNumberComplianceCase","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Case","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCase"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/telephony/compliance-cases/{id}/checklist":{"post":{"tags":["Telephony"],"summary":"Compute the current blocking checklist","operationId":"computeNumberComplianceChecklist","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current gate results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceChecklist"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/telephony/compliance-cases/{id}/uploads:grant":{"post":{"tags":["Telephony"],"summary":"Create a short-lived document upload grant","description":"Returns an opaque object reference and a five-minute, single-object V4 upload URL. The client must send every returned required header. The declared MIME type, size, and SHA-256 are pinned into object metadata; no URL or document bytes are persisted in the control-plane database.","operationId":"grantNumberComplianceUpload","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceUploadGrantCreate"}}}},"responses":{"200":{"description":"Short-lived scoped upload grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceSignedGrant"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"description":"Secure document storage is not configured."}}}},"/v1/telephony/compliance-cases/{id}/uploads":{"post":{"tags":["Telephony"],"summary":"Register document upload metadata","description":"Finalizes a previously granted upload. The immutable object generation, ownership path, signed metadata, byte length, SHA-256, and actual file signature are verified before a bounded malware scan runs. Extraction remains blocked unless the terminal scan result is clean.","operationId":"registerNumberComplianceUpload","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceUploadCreate"}}}},"responses":{"201":{"description":"Upload metadata registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCase"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"description":"Concurrent case update."}}}},"/v1/telephony/compliance-cases/{id}/uploads/{uploadId}/download:grant":{"post":{"tags":["Telephony"],"summary":"Create a short-lived clean-document download grant","description":"Returns a five-minute V4 URL pinned to the verified object generation. Pending, failed, or infected documents fail closed.","operationId":"grantNumberComplianceDownload","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Short-lived scoped download grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceSignedGrant"}}}},"409":{"description":"Document has not passed verification."}}}},"/v1/telephony/compliance-cases/{id}/extractions":{"post":{"tags":["Telephony"],"summary":"Run document extraction","description":"Runs bounded extraction for clean uploads. PyAI selects the processing route. Every selected upload receives a terminal success, failure, timeout, or cancellation record.","operationId":"startNumberComplianceExtraction","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"upload_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Extraction attempts terminalized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCase"}}}},"409":{"description":"Workflow gate or CAS conflict."}}}},"/v1/telephony/compliance-cases/{id}/corrections":{"post":{"tags":["Telephony"],"summary":"Correct an extracted field","operationId":"correctNumberComplianceField","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["extraction_id","field","corrected_value","reason"],"properties":{"extraction_id":{"type":"string"},"field":{"type":"string"},"corrected_value":{"type":"string"},"reason":{"type":"string"}}}}}},"responses":{"201":{"description":"Correction recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCase"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/v1/telephony/compliance-cases/{id}/attestations":{"post":{"tags":["Telephony"],"summary":"Sign a required attestation","operationId":"attestNumberComplianceCase","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["requirement_id","signer_name","signature_method"],"properties":{"requirement_id":{"type":"string"},"signer_name":{"type":"string"},"signer_title":{"type":["string","null"]},"signature_method":{"type":"string","enum":["typed_name","external_esign"],"description":"typed_name is the built-in attestation. external_esign fails closed until a verified provider capability is configured."},"external_envelope_ref":{"type":["string","null"],"description":"Opaque reference from an external e-sign system."}}}}}},"responses":{"201":{"description":"Attestation recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCase"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/v1/telephony/compliance-cases/{id}/submit":{"post":{"tags":["Telephony"],"summary":"Human-approve and submit for network review","description":"Submits for network review only after all evidence, confidence, matching, malware, and attestation gates pass and a human approver is named. Requires `Idempotency-Key`.","operationId":"submitNumberComplianceCase","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["approved_by"],"properties":{"approved_by":{"type":"string","minLength":1}}}}}},"responses":{"200":{"description":"Submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCase"}}}},"409":{"description":"Blocking gate or capability is unresolved."}}}},"/v1/telephony/compliance-cases/{id}/cancel":{"post":{"tags":["Telephony"],"summary":"Cancel a number compliance case","operationId":"cancelNumberComplianceCase","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NumberComplianceCase"}}}},"409":{"description":"Case is already terminal."}}}},"/v1/telephony/compliance-cases/{id}/network-operations":{"get":{"tags":["Telephony"],"summary":"List normalized network status","description":"Returns the provider-neutral network timeline for a compliance or porting case. Automated and operator-assisted paths use the same normalized statuses; internal notes and operator identities are never returned.","operationId":"listNumberComplianceNetworkOperations","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Network operations","content":{"application/json":{"schema":{"type":"object","required":["object","data"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/NumberComplianceNetworkOperation"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/telephony/available":{"get":{"tags":["Telephony"],"summary":"Search available numbers","description":"Search PyAI's managed number inventory in the US, Canada, or India. `country` selects the market (US default; India numbers are subject to regulatory review). Filter by `area_code` (NPA, US and Canada) or a `contains` digit pattern. Monthly line prices are $1 for US and Canada numbers and $6 for India numbers. The response's `provisioning` field reports whether automated ordering is available or assisted ordering is required. Requires the `telephony:manage` scope.","operationId":"listAvailableNumbers","parameters":[{"name":"country","in":"query","required":false,"schema":{"type":"string","enum":["US","CA","IN"],"default":"US"},"description":"ISO-2 country to search in."},{"name":"area_code","in":"query","required":false,"schema":{"type":"string","pattern":"^[2-9][0-9]{2}$"},"description":"3-digit US or Canada 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"}},"provisioning":{"$ref":"#/components/schemas/TelephonyProvisioningStatus"}}}}}},"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. When search reports assisted ordering, support may pre-order the number and you can explicitly set `provisioning_mode=adopt_preowned`; this never places a new order. Recording runs 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":true,"schema":{"type":"string","maxLength":255},"description":"Required safe retry key. Reusing the key with an identical body replays the original 201 response (no second number 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 managed number (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/telephony/compliance/policy":{"get":{"tags":["Telephony"],"summary":"Get outbound compliance policy","operationId":"getTelephonyCompliancePolicy","responses":{"200":{"description":"Tenant policy and setup guidance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyCompliancePolicy"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"put":{"tags":["Telephony"],"summary":"Configure outbound compliance policy","description":"Set bounded calling windows, countries, velocity limits, and at most two pre-answer attempts. Strong defaults apply to omitted fields.","operationId":"putTelephonyCompliancePolicy","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyCompliancePolicyInput"}}}},"responses":{"200":{"description":"Updated policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyCompliancePolicy"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/telephony/compliance/consents":{"post":{"tags":["Telephony"],"summary":"Record destination consent","description":"Store tenant-scoped consent evidence. Sales requires express written consent. The response masks the destination.","operationId":"createTelephonyConsent","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["destination","level","purposes","timezone","evidence_ref","captured_at"],"properties":{"destination":{"type":"string","example":"+14155550123"},"level":{"type":"string","enum":["express","express_written"]},"purposes":{"type":"array","items":{"type":"string","enum":["transactional","support","sales","collections","survey"]}},"timezone":{"type":"string","example":"America/Los_Angeles"},"evidence_ref":{"type":"string"},"captured_at":{"type":"integer","format":"int64"},"expires_at":{"type":["integer","null"],"format":"int64"}}}}}},"responses":{"201":{"description":"Consent recorded"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/telephony/compliance/suppressions":{"post":{"tags":["Telephony"],"summary":"Suppress an outbound destination","description":"Add a tenant-scoped opt-out or do-not-call suppression. The response masks the destination.","operationId":"createTelephonySuppression","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["destination","reason"],"properties":{"destination":{"type":"string","example":"+14155550123"},"source":{"type":"string","enum":["internal_opt_out","manual"]},"reason":{"type":"string"}}}}}},"responses":{"201":{"description":"Destination suppressed"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/telephony/compliance/caller-identities":{"get":{"tags":["Telephony"],"summary":"List caller identity readiness","description":"List masked, tenant-scoped caller identity and attestation readiness.","operationId":"listTelephonyCallerIdentities","responses":{"200":{"description":"Caller identities"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/telephony/calls":{"get":{"tags":["Telephony"],"summary":"List outbound calls","description":"Recent outbound call requests for your organization, with normalized network outcomes. Requires `telephony:manage`.","operationId":"listTelephonyCalls","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Outbound calls","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/TelephonyCall"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Telephony"],"summary":"Start an outbound agent call","description":"Start an outbound call from one of your active managed numbers into an Omni agent. A valid Idempotency-Key is mandatory. PyAI never retries an ambiguous dispatch and never changes routes after answer; only a confirmed retryable pre-answer result may advance an approved route plan. Limited availability: returns 404 until outbound telephony is enabled for the deployment. Requires `telephony:manage`.","operationId":"createTelephonyCall","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"Required safe-retry key. Identical retries return the original call; a different body returns 409."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyCallCreateRequest"}}}},"responses":{"201":{"description":"Call accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyCall"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Source number/agent not found, or outbound telephony is not enabled."},"409":{"description":"Idempotency conflict or source number is not voice-capable."}}}},"/v1/telephony/calls/{id}":{"get":{"tags":["Telephony"],"summary":"Get an outbound call","description":"Current state of one outbound call. Requires `telephony:manage`.","operationId":"getTelephonyCall","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Outbound call","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelephonyCall"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/whatsapp/numbers":{"get":{"tags":["WhatsApp"],"summary":"List your WhatsApp numbers","description":"WhatsApp Business numbers registered to your org, newest first. Active only unless `include_released=true`. Requires the `telephony:manage` scope.","operationId":"listWhatsAppNumbers","parameters":[{"name":"include_released","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Your WhatsApp numbers","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WhatsAppNumber"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"WhatsApp calling is not enabled for this deployment."}}},"post":{"tags":["WhatsApp"],"summary":"Register a WhatsApp Business number","description":"Attach a WhatsApp Business number you already own on Meta's Cloud API to your org so an Omni agent can answer its calls. You bring the WhatsApp Business Account id, the number's `phone_number_id`, and a system-user access token with `whatsapp_business_messaging`; PyAI stores the token encrypted and never returns it. Point your Meta app's `calls` and `messages` webhooks at `https://api.pyai.com/public/whatsapp/webhook`. Calling stays off until you enable it with `POST /v1/whatsapp/numbers/{id}/calling`. Requires the `telephony:manage` scope.","operationId":"registerWhatsAppNumber","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppRegisterNumberRequest"}}}},"responses":{"201":{"description":"Registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppNumber"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"That phone_number_id is already registered (`code: number_in_use`)."},"503":{"description":"WhatsApp calling is not enabled on this deployment (`code: whatsapp_disabled`)."}}}},"/v1/whatsapp/numbers/{id}":{"get":{"tags":["WhatsApp"],"summary":"Get a WhatsApp number","operationId":"getWhatsAppNumber","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The number","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppNumber"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["WhatsApp"],"summary":"Release a WhatsApp number","description":"Detach the number from your org. Inbound WhatsApp calls to it are rejected from then on; nothing changes on Meta's side. Requires the `telephony:manage` scope.","operationId":"releaseWhatsAppNumber","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Released","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppNumber"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/whatsapp/numbers/{id}/assign":{"post":{"tags":["WhatsApp"],"summary":"Route a WhatsApp number to an agent","description":"Bind (or unbind with `agent_id: null`) the Omni agent that answers inbound WhatsApp calls to this number and is the default for outbound calls from it. Requires the `telephony:manage` scope.","operationId":"assignWhatsAppNumber","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppAssignRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppNumber"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/whatsapp/numbers/{id}/calling":{"post":{"tags":["WhatsApp"],"summary":"Enable or disable calling on a WhatsApp number","description":"Pushes Meta's calling configuration for the number (`POST /{phone_number_id}/settings`): turns the call button on or off, and whether a user calling you automatically grants a 7-day call-back permission. Meta requires the number to have a messaging limit of at least 2,000 conversations per day before calling can be enabled. Requires the `telephony:manage` scope.","operationId":"configureWhatsAppCalling","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppCallingSettingsRequest"}}}},"responses":{"200":{"description":"Settings applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppNumber"}}}},"400":{"description":"Meta rejected the settings (`code: meta_graph_error`)."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"description":"Meta's API was unreachable or refused the token (`code: meta_graph_error`)."}}}},"/v1/whatsapp/call-permissions":{"get":{"tags":["WhatsApp"],"summary":"Check call permission for a user","description":"Whether you may place a business-initiated call to `user` from `number_id`. Temporary permissions last 7 days; permanent ones until the user revokes them. Served from PyAI's cache when live, otherwise refreshed from Meta (`refresh=true` forces it). Requires the `telephony:manage` scope.","operationId":"getWhatsAppCallPermission","parameters":[{"name":"number_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"user","in":"query","required":true,"schema":{"type":"string"},"description":"The user's WhatsApp number (E.164)."},{"name":"refresh","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Permission state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppCallPermission"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["WhatsApp"],"summary":"Ask a user for permission to call them","description":"Sends Meta's interactive `call_permission_request` message from `number_id` to `user` with your `text`. Meta limits this to 1 request per 24 hours and 2 per 7 days per user; the user's reply is recorded from the webhook and gates `POST /v1/whatsapp/calls`. Requires the `telephony:manage` scope.","operationId":"requestWhatsAppCallPermission","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppPermissionRequest"}}}},"responses":{"202":{"description":"Request sent","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"whatsapp.call_permission_request"},"number_id":{"type":"string"},"user":{"type":"string"},"message_id":{"type":"string","nullable":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/whatsapp/calls":{"get":{"tags":["WhatsApp"],"summary":"List WhatsApp calls","operationId":"listWhatsAppCalls","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Calls, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WhatsAppCall"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["WhatsApp"],"summary":"Place a WhatsApp call","description":"Business-initiated WhatsApp call from `number_id` to `to`, answered by `agent_id` (defaults to the number's agent). Fails with 403 `call_permission_required` unless the user has a live call permission. Not available in every country (Meta blocks business-initiated calls in the US, Canada, Egypt, Vietnam and Nigeria at the time of writing). Requires an `Idempotency-Key` and the `telephony:manage` scope.","operationId":"createWhatsAppCall","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"Safe retry key. Reusing it with the same input replays the original call; different input returns 409 `idempotency_conflict`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppCallCreateRequest"}}}},"responses":{"201":{"description":"Call placed (ringing)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppCall"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"No live call permission from the user (`code: call_permission_required`)."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Calling is disabled on the number (`code: calling_disabled`) or the Idempotency-Key was reused with different input."},"502":{"description":"Meta refused the call or the media edge was unavailable."}}}},"/v1/whatsapp/calls/{id}":{"get":{"tags":["WhatsApp"],"summary":"Get a WhatsApp call","operationId":"getWhatsAppCall","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The call","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppCall"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/whatsapp/calls/{id}/terminate":{"post":{"tags":["WhatsApp"],"summary":"Hang up a WhatsApp call","operationId":"terminateWhatsAppCall","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Terminated (idempotent on already-ended calls)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppCall"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/omni":{"get":{"tags":["Omni"],"summary":"Open an Omni voice-agent session (WebSocket)","description":"**This is the primary way to build an AI voice agent on PyAI.** Open this WebSocket, send one `configure` frame (voice, persona, knowledge endpoint, tools), then stream PCM16 audio both ways. There is nothing to create first: the session is authorized by your key's org, and the whole agent travels in the `configure` frame.\n\nConnect with `wss://api.pyai.com/v1/omni?format=pcm16&rate=24000` (`rate=16000` or `rate=8000` for telephony; 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). Every binary frame starts with a one-byte kind: send audio as `0x01 || pcm16_bytes` and control JSON as `0x03 || utf8_json`. Server audio uses the same `0x01` prefix. Untagged PCM is not audio and is ignored.\n\nRequires the `omni:session` scope (or the `omni:*` wildcard). The optional `session_label` 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).\n\n**Auth:** send the key as the subprotocol `Sec-WebSocket-Protocol: pyai-key.<API_KEY>` (browsers can't set headers on a WebSocket; server clients may use `?api_key=` instead). **Browser apps:** if the client can't hold your secret key, mint a short-lived origin-locked token with `POST /v1/omni/sessions` first and use that token as the subprotocol.\n\nSee the Omni wire protocol reference for the frame catalog and close codes.\n\nFunction calling transports: hosted catalog tools (enable by name), server tools registered with `POST /v1/tools` and bound to the agent, or client-loop tools on this socket (`tool_call` / `tool_result`). An inline `endpoint` or `webhook_url` on `configure.tools[]` is rejected with `{event:\"error\", code:\"unsupported_tool_transport\"}` and the configure is not applied.","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":"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":"Caller-input sample rate in Hz. Use `24000` for browser/WebRTC, `16000` for wideband input, or `8000` for telephony. Agent output is 24 kHz for both 24 kHz and 16 kHz input sessions; 8 kHz sessions receive 8 kHz output. Read `hello.audio_out`, which is authoritative."},{"name":"api_key","in":"query","required":false,"schema":{"type":"string","format":"password","maxLength":512},"description":"Intentional server-side WebSocket auth option. Prefer the `pyai-key.<API_KEY>` subprotocol in browsers so the key is not placed in a URL."}],"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"}}}}},"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"pyai_session","description":"Console session cookie. PyAI-admin routes also require the configured platform-admin email allowlist."},"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":{"CastLine":{"type":"object","required":["text","emotion","intensity"],"properties":{"text":{"type":"string","minLength":1},"emotion":{"type":"string","minLength":1},"intensity":{"oneOf":[{"type":"string"},{"type":"number"}]}}},"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":["speak: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_session_label","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"}}},"HearVocabularyInput":{"type":"object","required":["terms","enabled_for"],"properties":{"terms":{"type":"array","maxItems":5,"items":{"type":"string","minLength":4,"maxLength":64},"description":"Organization-owned names, brands, products, and other distinctive terms. PyAI keeps the first five entries that pass the same sanitizer used by async jobs."},"enabled_for":{"type":"array","maxItems":2,"uniqueItems":true,"items":{"type":"string","enum":["batch","hear_stream"]},"description":"Explicit use cases that may use stored terms. An empty array stores the list without applying it."}}},"HearVocabulary":{"type":"object","required":["object","terms","enabled_for","updated_at"],"properties":{"object":{"type":"string","const":"hear.vocabulary"},"terms":{"type":"array","maxItems":5,"items":{"type":"string","minLength":4,"maxLength":64},"description":"The sanitized effective stored list."},"enabled_for":{"type":"array","maxItems":2,"uniqueItems":true,"items":{"type":"string","enum":["batch","hear_stream"]}},"updated_at":{"type":["integer","null"],"description":"Unix milliseconds, or null when no settings have been saved."}}},"TranscriptionWord":{"type":"object","required":["word","start","end"],"description":"Word-level timestamps when alignment is available. `smart_format` may attach case or punctuation without moving timestamps. Options that add or remove tokens (`dictation` and `drop_fillers`) clear this array when the rewritten words cannot be safely realigned.","properties":{"word":{"type":"string","description":"Recognized token. Punctuation may be attached when final-transcript formatting is enabled.","example":"Hello"},"start":{"type":"number","minimum":0,"description":"Offset in decimal seconds from the start of the decoded source-media timeline. Silence is not removed or compacted; resampling and internal chunking do not shift later offsets.","example":0.42},"end":{"type":"number","minimum":0,"description":"Offset in decimal seconds from the start of the decoded source-media timeline. Silence is not removed or compacted; resampling and internal chunking do not shift later offsets.","example":0.81},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Recognition confidence when supplied by the active Hear model. Omitted when unavailable.","example":0.98},"speaker":{"type":"string","description":"Neutral speaker label. With `channel: true`, `speaker_1` maps to channel 0 and `speaker_2` to channel 1; this is exact channel separation, not an inferred agent/customer role. With mono `diarize: true`, labels are model-derived and must not be treated as stable identities across separate jobs.","example":"speaker_1"},"channel":{"type":"integer","minimum":0,"description":"Zero-based source channel. Present on `channel: true` results.","example":0},"entity":{"type":"string","description":"Normalized entity category when supplied, such as `phone_number`, `account_id`, or `date`."}}},"TranscriptionSegment":{"type":"object","required":["id","start","end","text"],"properties":{"id":{"type":"integer","minimum":0,"description":"Zero-based segment index.","example":0},"start":{"type":"number","minimum":0,"description":"Offset in decimal seconds from the start of the decoded source-media timeline. Silence is not removed or compacted; resampling and internal chunking do not shift later offsets.","example":0.42},"end":{"type":"number","minimum":0,"description":"Offset in decimal seconds from the start of the decoded source-media timeline. Silence is not removed or compacted; resampling and internal chunking do not shift later offsets.","example":1.8},"text":{"type":"string","example":"Hello everyone."},"speaker":{"type":"string","description":"Neutral speaker label. With `channel: true`, `speaker_1` maps to channel 0 and `speaker_2` to channel 1; this is exact channel separation, not an inferred agent/customer role. With mono `diarize: true`, labels are model-derived and must not be treated as stable identities across separate jobs.","example":"speaker_1"},"channel":{"type":"integer","minimum":0,"description":"Zero-based source channel. Present on `channel: true` results.","example":0}}},"TranscriptionJobResult":{"type":"object","required":["text","speakers","audio_seconds","segments","words"],"description":"Completed async Hear result. The spoken language is auto-detected per call across the eight supported Hear languages and the transcript is returned in the detected language. This object does not currently return the detected language code.","properties":{"text":{"type":"string","description":"Complete transcript. Diarized results prefix segments with neutral speaker labels."},"speakers":{"type":"integer","minimum":1,"description":"Number of distinct speaker labels in this result."},"audio_seconds":{"type":"number","minimum":0,"description":"Decoded source duration in decimal seconds, including silence.","example":120},"segments":{"type":"array","items":{"$ref":"#/components/schemas/TranscriptionSegment"},"description":"Timestamped subtitle-ready spans. Segment boundaries may change when speaker assignment or the one-second readable-cue gap changes."},"words":{"type":"array","items":{"$ref":"#/components/schemas/TranscriptionWord"},"description":"Word-level timestamps when alignment is available. `smart_format` may attach case or punctuation without moving timestamps. Options that add or remove tokens (`dictation` and `drop_fillers`) clear this array when the rewritten words cannot be safely realigned."},"formats":{"type":"object","additionalProperties":{"type":"string","format":"uri"},"description":"Requested SRT/VTT format names mapped to short-lived signed GET URLs. JSON is the result object itself."},"trace":{"type":"object","description":"Present when the Trace add-on was requested.","properties":{"verdict":{"type":"string"},"n_pii":{"type":"integer","minimum":0},"redacted":{"type":"boolean"}}}},"example":{"text":"[speaker_1] Hello everyone.","audio_seconds":120,"speakers":1,"words":[{"word":"Hello","start":0.42,"end":0.81,"confidence":0.98,"speaker":"speaker_1","channel":0}],"segments":[{"id":0,"text":"Hello everyone.","start":0.42,"end":1.8,"speaker":"speaker_1","channel":0}]}},"TranscriptionJob":{"type":"object","required":["job_id","status","created_at","updated_at"],"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":{"$ref":"#/components/schemas/TranscriptionJobResult","description":"Present on completed jobs (inline). Large results are offloaded to result_url instead."},"result_url":{"type":"string","format":"uri","description":"Signed GET URL for an offloaded large result."},"error":{"type":"string","description":"Human-readable normalized failure message on failed jobs. This field is not a stable machine-readable failure code."}}},"ModelList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"type":"object"}}}},"Knowledgebase":{"type":"object","properties":{"id":{"type":"string"},"orgId":{"type":"string"},"name":{"type":"string"},"embedModel":{"type":"string"},"docCount":{"type":"integer"},"bytes":{"type":"integer"},"status":{"type":"string","enum":["active","deleted"]},"createdAt":{"type":"integer","description":"Unix ms."}}},"KnowledgebaseList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Knowledgebase"}},"has_more":{"type":"boolean"}}},"KnowledgebaseWithDocuments":{"allOf":[{"$ref":"#/components/schemas/Knowledgebase"},{"type":"object","properties":{"documents":{"type":"array","items":{"$ref":"#/components/schemas/KbDocument"}}}}]},"KbDocument":{"type":"object","description":"A knowledge base document and its ingestion state.","properties":{"id":{"type":"string"},"kbId":{"type":"string"},"orgId":{"type":"string"},"source":{"type":"string","enum":["upload","url","api"],"description":"upload = file, url = fetched page, api = pasted text."},"uri":{"type":["string","null"]},"title":{"type":["string","null"]},"filename":{"type":["string","null"]},"mime":{"type":["string","null"]},"sizeBytes":{"type":"integer"},"chunks":{"type":"integer","description":"Indexed chunk count (0 until indexed)."},"status":{"type":"string","enum":["pending","indexed","failed"]},"error":{"type":["string","null"],"description":"Human-readable failure/stall reason."},"indexedAt":{"type":["integer","null"],"description":"Unix ms."},"createdAt":{"type":"integer","description":"Unix ms."}}},"KbDocumentList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/KbDocument"}},"has_more":{"type":"boolean"}}},"KbBinding":{"type":"object","required":["kb_id"],"properties":{"kb_id":{"type":"string","description":"Knowledge base id."},"weight":{"type":"number","default":1,"description":"Retrieval weight relative to other bound bases."}}},"KbBindingList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/KbBinding"}}}},"KnowledgebaseCrawl":{"type":"object","properties":{"object":{"type":"string","example":"knowledgebase.crawl"},"seed_url":{"type":"string","format":"uri"},"max_pages":{"type":"integer"},"discovered":{"type":"integer","description":"Same-origin candidates considered before ranking."},"accepted":{"type":"integer","description":"New URL documents registered by this crawl."},"reused":{"type":"integer","description":"Selected URLs that already existed in this knowledge base."},"skipped":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"reason":{"type":"string"}}}},"documents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"title":{"type":"string"},"reused":{"type":"boolean"}}}}}},"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","speak","clone","cue","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."},"source":{"type":"string","nullable":true,"description":"Call origin, for example external."}}},"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."}}},"IntegrationEventCatalogEntry":{"type":"object","properties":{"id":{"type":"string","enum":["recap.call.completed","amd.call.completed","trace.scorecard.completed","transcription.job.completed","cast.render.completed"],"description":"Event type you can subscribe a Zapier hook to."},"label":{"type":"string"},"description":{"type":"string"},"sample":{"type":"object","additionalProperties":true,"description":"Representative delivery payload, for mapping Zap fields."}}},"IntegrationDestination":{"type":"object","properties":{"object":{"type":"string","example":"integration.destination"},"id":{"type":"string","example":"idst_9fK2mQ7xR4wLpN8vTzA1"},"kind":{"type":"string","enum":["slack","zapier","hubspot","zendesk","pipedrive","zoho","apollo","clay","sheets"]},"name":{"type":"string"},"events":{"type":"array","items":{"type":"string","enum":["recap.call.completed","amd.call.completed","trace.scorecard.completed","transcription.job.completed","cast.render.completed"]}},"status":{"type":"string","enum":["active","error","disabled"]},"last_delivery_at":{"type":"integer","nullable":true,"description":"Unix ms."},"last_error":{"type":"string","nullable":true},"config":{"type":"object","additionalProperties":true,"description":"Kind-specific config with secrets redacted (URLs masked, tokens shortened)."},"created_at":{"type":"integer","description":"Unix ms."},"updated_at":{"type":"integer","description":"Unix ms."}}},"ZapierHookInput":{"type":"object","required":["event","target_url"],"properties":{"event":{"type":"string","enum":["recap.call.completed","amd.call.completed","trace.scorecard.completed","transcription.job.completed","cast.render.completed"],"description":"Event to deliver to this hook."},"target_url":{"type":"string","format":"uri","description":"Zapier REST-hook target URL (https). Every completed call POSTs a signed envelope {id, type, occurred_at, data} here."}}},"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},"source":{"type":"string","nullable":true,"description":"Call origin, for example external."},"headline":{"type":"string","nullable":true,"description":"Best available summary headline for list and tracking views."},"customer_name":{"type":"string","nullable":true},"call_direction":{"type":"string","enum":["inbound","outbound"],"nullable":true}}},"RecapUtterance":{"type":"object","required":["speaker_role","text","offset_s","duration_s"],"properties":{"speaker_role":{"type":"string","enum":["agent","customer"]},"text":{"type":"string"},"offset_s":{"type":"number","minimum":0},"duration_s":{"type":"number","minimum":0},"role_source":{"type":"string","enum":["keyword","participant_name","diarization","channel","inferred","corrected"],"description":"How Recap decided this speaker role: a role keyword, a participant name, Hear diarization (speaker 1 = agent, speaker 2 = customer), audio channel, conversation-role inference, or a customer correction."},"role_confidence":{"type":"number","minimum":0,"maximum":1,"description":"Confidence in the speaker role, from 0 to 1."}}},"RecapSpeakerRoleCorrection":{"type":"object","required":["corrections"],"properties":{"corrections":{"type":"array","minItems":1,"items":{"type":"object","required":["index","speaker_role"],"properties":{"index":{"type":"integer","minimum":0,"description":"Zero-based index into transcript.utterances."},"speaker_role":{"type":"string","enum":["agent","customer"]}}}}}},"RecapTranscript":{"type":"object","required":["format","utterances"],"properties":{"format":{"type":"string","enum":["utterances.v1"]},"utterances":{"type":"array","items":{"$ref":"#/components/schemas/RecapUtterance"}}}},"RecapProcessing":{"type":"object","properties":{"stage":{"type":"string"},"elapsed_s":{"type":"number","minimum":0},"utterance_count":{"type":"integer","minimum":0},"coverage_gaps":{"type":"integer","minimum":0},"polish_available":{"type":"boolean"}}},"RecapActionItem":{"type":"object","required":["task"],"properties":{"owner":{"type":"string","nullable":true},"task":{"type":"string"},"due":{"type":"string","nullable":true}}},"RecapTalkRatio":{"type":"object","required":["agent","customer"],"properties":{"agent":{"type":"number","minimum":0,"maximum":1},"customer":{"type":"number","minimum":0,"maximum":1}}},"RecapSignal":{"type":"object","required":["kind","text"],"properties":{"kind":{"type":"string","description":"signal, risk, buying, objection, or competitor."},"text":{"type":"string"},"at_s":{"type":"number","nullable":true,"minimum":0}}},"RecapRecord":{"type":"object","required":["format","tldr","summary","action_items","disposition","next_steps","talk_ratio","signals","fields"],"properties":{"format":{"type":"string","enum":["recap.record.v1"]},"tldr":{"type":"string","nullable":true,"description":"One-line headline. Intentionally short."},"summary":{"type":"string","nullable":true,"description":"Detailed notes for the call."},"action_items":{"type":"array","items":{"$ref":"#/components/schemas/RecapActionItem"}},"disposition":{"type":"string","nullable":true},"next_steps":{"type":"string","nullable":true},"talk_ratio":{"allOf":[{"$ref":"#/components/schemas/RecapTalkRatio"}],"nullable":true},"signals":{"type":"array","items":{"$ref":"#/components/schemas/RecapSignal"}},"fields":{"type":"object","additionalProperties":true,"description":"Pack-specific extracted fields. Stable v1 keys live at the root, not here."},"rep":{"type":"object","additionalProperties":true,"description":"Processor nested surface, passed through for older clients."},"manager":{"type":"object","additionalProperties":true,"description":"Processor nested surface, passed through for older clients."},"ops":{"type":"object","additionalProperties":true,"description":"Processor nested surface, passed through for older clients."}}},"RecapCall":{"allOf":[{"$ref":"#/components/schemas/RecapCallSummary"},{"type":"object","properties":{"record":{"allOf":[{"$ref":"#/components/schemas/RecapRecord"}],"description":"Typed Recap intelligence when status is complete (`recap.record.v1`)."},"transcript":{"allOf":[{"$ref":"#/components/schemas/RecapTranscript"}],"description":"Full speaker-labelled transcript retained from the Recap trigger."},"processing":{"allOf":[{"$ref":"#/components/schemas/RecapProcessing"}],"description":"Customer-safe processing diagnostics for this Recap."},"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}}},"AmdConfig":{"type":"object","properties":{"object":{"type":"string","example":"amd.config"},"aggressiveness":{"type":"number","minimum":0,"maximum":1,"example":0.25,"description":"Operating point on the ROC curve. Near 0 = human-safe (never hang up on a person); near 1 = fire `machine` fast. Per-call TwiML `<Parameter>` overrides this."},"webhook_url":{"type":"string","nullable":true,"format":"uri"},"updated_at":{"type":"integer","description":"Unix ms."}}},"AmdConfigInput":{"type":"object","properties":{"aggressiveness":{"type":"number","minimum":0,"maximum":1},"webhook_url":{"type":"string","nullable":true,"format":"uri"}}},"AmdCallListItem":{"type":"object","properties":{"object":{"type":"string","example":"amd.call"},"call_id":{"type":"string"},"session_label":{"type":"string","nullable":true,"description":"The opaque tag passed on the connect URL / TwiML, if any."},"status":{"type":"string","enum":["completed","failed"]},"answered_by":{"type":"string","enum":["human","voicemail","live_voicemail","screening","ivr","human_gatekeeper","sit_invalid","fax","silence","unknown"],"description":"PyAI's richer answered-by vocabulary. `screening` = an iPhone/Google call-screening assistant; `sit_invalid` = a dead/disconnected number."},"answered_by_twilio":{"type":"string","nullable":true,"description":"Twilio's exact `AnsweredBy` enum (human | machine_start | machine_end_beep | machine_end_silence | machine_end_other | fax | unknown), so a Twilio drop-in keeps its routing logic unchanged."},"confidence":{"type":"number","nullable":true,"minimum":0,"maximum":1},"decision_ms":{"type":"number","nullable":true,"description":"Latency from answer to decision, in ms."},"created_at":{"type":"integer","description":"Unix ms when the decision was written."}}},"AmdCall":{"allOf":[{"$ref":"#/components/schemas/AmdCallListItem"},{"type":"object","properties":{"reason":{"type":"string","nullable":true,"description":"Human-readable evidence, e.g. \"machine phrase: 'please leave a message' at 1.2s\"."},"aggressiveness":{"type":"number","nullable":true,"description":"Operating point used for this call."},"started_at":{"type":"integer","nullable":true,"description":"Unix ms when the call was answered."},"meta":{"type":"object","nullable":true},"error":{"type":"string","nullable":true,"description":"Present when status is failed."}}}]},"AmdCallList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AmdCallListItem"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}},"OmniCallId":{"type":"string","pattern":"^ocl_[A-Za-z0-9]{20}$","example":"ocl_7Hk2Qm9Zs4Wp8Lc3Nv6R","description":"Stable opaque PyAI-owned call identifier. Use it unchanged across every Omni call sub-resource."},"OmniCallListItem":{"type":"object","properties":{"object":{"type":"string","example":"omni.call"},"call_id":{"$ref":"#/components/schemas/OmniCallId"},"session_label":{"type":"string","nullable":true,"description":"The opaque tag passed on the connect URL, if any."},"external_id":{"type":"string","nullable":true,"description":"Customer-supplied correlation id echoed from the call's `meta` (the engine carries `configure.meta` into the ingest). Use it to join a call to your own record; null when none was set."},"status":{"type":"string","enum":["completed","failed"]},"duration_s":{"type":"number","nullable":true},"language":{"type":"string","enum":["en","fr","es","de","hi"],"description":"Language the session was served in. Records from before multilingual rollout render the default, `en`."},"voice_tier":{"type":"string","enum":["standard","natural"],"description":"Voice tier served for the session. English `en1` sessions report `natural` while that tier is active; Standard voices report `standard`. Both are included in the Omni base rate with no voice-tier add-on. Inspect the configured acknowledgement and call record rather than inferring the served tier from the requested alias."},"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."},"summary":{"type":"object","description":"Structured post-call summary, when generated."},"recording_result":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["success","intentionally_disabled","failed"]},"reason":{"type":"string","enum":["recording_not_requested","recording_not_finalized"]}},"additionalProperties":false,"description":"Provider-neutral recording outcome. Internal source and storage details are never returned."},"error":{"type":"string","enum":["call_failed"],"description":"Normalized failure reason, 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":{"$ref":"#/components/schemas/OmniCallId"},"url":{"type":"string","format":"uri","description":"Canonical authenticated PyAI endpoint for the recording audio."}}},"OmniCallSummary":{"type":"object","properties":{"object":{"type":"string","example":"omni.call.summary"},"call_id":{"$ref":"#/components/schemas/OmniCallId"},"summary":{"type":"object","description":"The structured post-call summary document."}}},"OmniCallTranscript":{"type":"object","properties":{"object":{"type":"string","example":"omni.call.transcript"},"call_id":{"$ref":"#/components/schemas/OmniCallId"},"transcript":{"type":"object","required":["turns"],"description":"Canonical transcript document.","properties":{"turns":{"type":"array","items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","enum":["user","assistant"]},"text":{"type":"string"},"start_ms":{"type":"integer","minimum":0},"end_ms":{"type":"integer","minimum":0}},"additionalProperties":false}}},"additionalProperties":false}}},"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}}},"NumberComplianceCaseCreate":{"type":"object","additionalProperties":false,"required":["kind","country","number_type","capabilities","use_case"],"properties":{"kind":{"type":"string","enum":["new_number","port_in"]},"country":{"type":"string","enum":["US","CA","IN","GB","AU"]},"number_type":{"type":"string","description":"Requested market number type, such as local, mobile, or toll_free."},"capabilities":{"type":"array","uniqueItems":true,"items":{"type":"string"},"example":["voice","sms"]},"use_case":{"type":"string","description":"Customer-declared use case used to select requirements."}}},"NumberComplianceUploadGrantCreate":{"type":"object","additionalProperties":false,"required":["document_request_id","mime_type","size_bytes","sha256"],"properties":{"document_request_id":{"type":"string"},"mime_type":{"type":"string"},"size_bytes":{"type":"integer","minimum":1},"sha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}}},"NumberComplianceSignedGrant":{"type":"object","required":["signed_url","expires_at"],"properties":{"object_ref":{"type":"string"},"signed_url":{"type":"string","format":"uri","description":"Ephemeral transport credential. Never persist or log it."},"required_headers":{"type":"object","additionalProperties":{"type":"string"}},"expires_at":{"type":"integer","format":"int64"}}},"NumberComplianceUploadCreate":{"type":"object","additionalProperties":false,"required":["document_request_id","object_ref","file_name","mime_type","size_bytes","sha256"],"properties":{"document_request_id":{"type":"string"},"object_ref":{"type":"string","pattern":"^obj_[A-Za-z0-9_-]{20,}$","description":"Opaque object-storage reference. Never a document body or public URL."},"file_name":{"type":"string","maxLength":255},"generation":{"type":"string","pattern":"^[1-9][0-9]*$","description":"Optional immutable object generation returned by storage."},"mime_type":{"type":"string"},"size_bytes":{"type":"integer","minimum":1},"sha256":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"Lowercase SHA-256 content hash."}}},"NumberComplianceChecklist":{"type":"object","required":["allowed","blockers","missingDocumentRequestIds","lowConfidenceFields","unresolvedMatchRuleIds","missingAttestationIds"],"properties":{"allowed":{"type":"boolean"},"blockers":{"type":"array","items":{"type":"string","enum":["rules_unavailable","evidence_required","object_ownership_required","malware_scan_required","extraction_required","extraction_failed","confidence_review_required","mismatch_resolution_required","attestation_required","human_approval_required","network_capability_required","network_review_pending"]}},"missingDocumentRequestIds":{"type":"array","items":{"type":"string"}},"lowConfidenceFields":{"type":"array","items":{"type":"object","properties":{"extractionId":{"type":"string"},"field":{"type":"string"}}}},"unresolvedMatchRuleIds":{"type":"array","items":{"type":"string"}},"missingAttestationIds":{"type":"array","items":{"type":"string"}}}},"NumberComplianceNetworkOperation":{"type":"object","required":["id","status","network_status","reason_code","reason_message","timeline","created_at","updated_at"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["open","claimed","awaiting_second_approval","submitted_for_network_review","additional_information_required","in_review","approved","rejected","activated","escalated","cancelled","manual_reconciliation","failed"]},"network_status":{"type":"string"},"reason_code":{"type":["string","null"]},"reason_message":{"type":["string","null"]},"timeline":{"type":"array","items":{"type":"object","required":["id","action","at","message"],"properties":{"id":{"type":"string"},"action":{"type":"string"},"at":{"type":"integer","format":"int64"},"message":{"type":["string","null"]}}}},"created_at":{"type":"integer","format":"int64"},"updated_at":{"type":"integer","format":"int64"}}},"NumberComplianceCase":{"type":"object","required":["id","object","project_id","kind","country","state","rules_snapshot","document_requests","uploads","extractions","timeline","version"],"properties":{"id":{"type":"string","example":"ncc_..."},"object":{"type":"string","const":"telephony.compliance_case"},"project_id":{"type":"string"},"kind":{"type":"string","enum":["new_number","port_in"]},"country":{"type":"string","enum":["US","CA","IN","GB","AU"]},"number_type":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"use_case":{"type":"string"},"network_status":{"type":"string"},"state":{"type":"string","enum":["collecting_evidence","extracting","needs_review","ready_for_submission","submitted","cancelled"]},"continuation_reason":{"type":["string","null"]},"terminal_reason":{"type":["string","null"],"enum":["network_review_submitted","cancelled_by_customer",null]},"rules_snapshot":{"type":"object","description":"Immutable rules, source identifiers, thresholds, retry caps, parallelism, wall-clock, cost, and retention configuration frozen when the case started.","properties":{"available":{"type":"boolean"},"registry_version":{"type":"string"},"source_ids":{"type":"array","items":{"type":"string"}},"frozen_at":{"type":"integer"},"config":{"type":"object","additionalProperties":true}}},"document_requests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"document_type":{"type":"string"},"label":{"type":"string"},"required":{"type":"boolean"},"accepted_mime_types":{"type":"array","items":{"type":"string"}},"max_bytes":{"type":"integer"}}}},"attestation_requests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"required":{"type":"boolean"}}}},"uploads":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"document_request_id":{"type":"string"},"object_ref":{"type":"string","description":"Opaque object reference."},"generation":{"type":["string","null"]},"file_name":{"type":"string"},"mime_type":{"type":"string"},"size_bytes":{"type":"integer"},"sha256":{"type":"string"},"malware_status":{"type":"string","enum":["pending","clean","infected","scan_failed"]},"uploaded_at":{"type":"integer"},"retention_delete_at":{"type":"integer"}}}},"extractions":{"type":"array","description":"Terminal processing attempts. Extracted personal values are masked in API views.","items":{"type":"object","properties":{"id":{"type":"string"},"upload_id":{"type":"string"},"processing_profile":{"type":"string","const":"standard"},"status":{"type":"string","enum":["succeeded","failed","cancelled"]},"failure_reason":{"type":["string","null"]},"attempts":{"type":"integer"},"fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"value":{"type":"string","description":"Extracted value; personal values are masked."},"confidence":{"type":"number","minimum":0,"maximum":1},"provenance":{"type":"object"},"pii_class":{"type":"string"},"redacted":{"type":"boolean"}}}},"cost_micros":{"type":"integer"},"model_version":{"type":["string","null"]},"latency_ms":{"type":"integer","minimum":0},"started_at":{"type":"integer"},"completed_at":{"type":"integer"}}}},"corrections":{"type":"array","description":"Correction audit metadata. Corrected PII values are intentionally omitted from API views.","items":{"type":"object","additionalProperties":true}},"matches":{"type":"array","items":{"type":"object","additionalProperties":true}},"attestations":{"type":"array","items":{"type":"object","additionalProperties":true}},"timeline":{"type":"array","items":{"type":"object","additionalProperties":true}},"created_at":{"type":"integer"},"updated_at":{"type":"integer"},"retention_delete_at":{"type":"integer"},"version":{"type":"integer","minimum":1}}},"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":"Monthly line price in USD cents ($1 for US/Canada, $6 for India)."}}},"TelephonyProvisioningStatus":{"type":"object","required":["mode","capability","message"],"properties":{"mode":{"type":"string","enum":["automated","assisted"]},"capability":{"type":"string","example":"available"},"message":{"type":"string"}}},"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":"Whether PyAI recording is enabled for the number."},"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, Canada, or India number (E.164) from the available-numbers search."},"agent_id":{"type":"string","nullable":true,"description":"Optional agent to route inbound calls to."},"provisioning_mode":{"type":"string","enum":["automated","adopt_preowned"],"default":"automated","description":"Use `adopt_preowned` only for a number already ordered through assisted provisioning. It never buys a number."}}},"WhatsAppNumber":{"type":"object","properties":{"object":{"type":"string","example":"whatsapp.number"},"id":{"type":"string","example":"wan_..."},"waba_id":{"type":"string","description":"WhatsApp Business Account id."},"phone_number_id":{"type":"string","description":"Meta's id for the business number (what the webhook carries)."},"phone_number":{"type":"string","example":"+14155550123","description":"E.164."},"app_id":{"type":"string","nullable":true},"agent_id":{"type":"string","nullable":true,"description":"Agent that answers inbound WhatsApp calls."},"calling_enabled":{"type":"boolean"},"calling_settings":{"type":"object","additionalProperties":true,"description":"Last calling settings pushed to Meta."},"status":{"type":"string","enum":["active","released"]},"created_at":{"type":"integer","description":"Unix ms."},"updated_at":{"type":"integer","description":"Unix ms."},"released_at":{"type":"integer","nullable":true,"description":"Unix ms."}}},"WhatsAppRegisterNumberRequest":{"type":"object","required":["waba_id","phone_number_id","display_phone_number","access_token"],"properties":{"waba_id":{"type":"string"},"phone_number_id":{"type":"string"},"display_phone_number":{"type":"string","example":"+14155550123","description":"The number in E.164."},"access_token":{"type":"string","description":"Meta system-user token with `whatsapp_business_messaging`. Stored encrypted, never returned."},"app_id":{"type":"string","nullable":true,"description":"Meta app that owns the webhook subscription."},"agent_id":{"type":"string","nullable":true,"description":"Optional agent to route inbound calls to."}}},"WhatsAppAssignRequest":{"type":"object","properties":{"agent_id":{"type":"string","nullable":true,"description":"Agent to bind, or null to unassign."}}},"WhatsAppCallingSettingsRequest":{"type":"object","properties":{"enabled":{"type":"boolean","default":true,"description":"Meta `calling.status`."},"call_icon_visibility":{"type":"string","enum":["DEFAULT","DISABLE_ALL"],"default":"DEFAULT"},"callback_permission":{"type":"boolean","default":true,"description":"A user calling you grants a 7-day call-back permission automatically."}}},"WhatsAppPermissionRequest":{"type":"object","required":["number_id","to","text"],"properties":{"number_id":{"type":"string"},"to":{"type":"string","example":"+919876543210","description":"The user's WhatsApp number (E.164)."},"text":{"type":"string","maxLength":1024,"description":"Why you want to call, shown in the permission prompt."}}},"WhatsAppCallPermission":{"type":"object","properties":{"object":{"type":"string","example":"whatsapp.call_permission"},"number_id":{"type":"string"},"user":{"type":"string","example":"+919876543210"},"status":{"type":"string","enum":["temporary","permanent","rejected","none"]},"source":{"type":"string","nullable":true,"description":"user_action | automatic | api"},"expires_at":{"type":"integer","nullable":true,"description":"Unix ms (temporary only)."},"updated_at":{"type":"integer"}}},"WhatsAppCallCreateRequest":{"type":"object","required":["number_id","to"],"properties":{"number_id":{"type":"string"},"to":{"type":"string","example":"+919876543210","description":"The user's WhatsApp number (E.164)."},"agent_id":{"type":"string","nullable":true,"description":"Defaults to the number's agent."}}},"WhatsAppCall":{"type":"object","properties":{"object":{"type":"string","example":"whatsapp.call"},"id":{"type":"string","example":"wac_..."},"number_id":{"type":"string"},"wa_call_id":{"type":"string","nullable":true,"description":"Meta's call id (wacid...)."},"direction":{"type":"string","enum":["inbound","outbound"]},"user":{"type":"string","example":"+919876543210"},"agent_id":{"type":"string","nullable":true},"status":{"type":"string","enum":["ringing","accepted","connected","completed","failed","rejected","no_answer","terminated"]},"failure_reason":{"type":"string","nullable":true},"started_at":{"type":"integer","nullable":true,"description":"Unix ms."},"connected_at":{"type":"integer","nullable":true,"description":"Unix ms."},"ended_at":{"type":"integer","nullable":true,"description":"Unix ms."},"duration_seconds":{"type":"integer","nullable":true},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}}},"TelephonyAssignRequest":{"type":"object","properties":{"agent_id":{"type":"string","nullable":true,"description":"Agent to bind, or null to unassign."}}},"TelephonyCompliancePolicyInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","suspended"]},"allowed_countries":{"type":"array","items":{"type":"string","pattern":"^[A-Z]{2}$"}},"calling_window_start_hour":{"type":"integer","minimum":0,"maximum":23,"default":8},"calling_window_end_hour":{"type":"integer","minimum":1,"maximum":24,"default":21},"max_calls_per_24_hours":{"type":"integer","minimum":1,"maximum":10000,"default":100},"max_calls_per_destination_24_hours":{"type":"integer","minimum":1,"maximum":100,"default":3,"description":"Maximum calls to one destination in an exact rolling 24-hour window."},"max_attempts":{"type":"integer","minimum":1,"maximum":2,"default":2},"require_cnam_for_sales":{"type":"boolean","default":true}}},"TelephonyCompliancePolicy":{"allOf":[{"$ref":"#/components/schemas/TelephonyCompliancePolicyInput"},{"type":"object","properties":{"object":{"type":"string","example":"telephony.compliance_policy"},"version":{"type":["integer","null"]},"updated_at":{"type":["integer","null"],"format":"int64"},"guidance":{"type":"object","properties":{"consent":{"type":"string"},"quiet_hours":{"type":"string"},"dnc":{"type":"string"}}}}}]},"TelephonyCallCreateRequest":{"type":"object","required":["from_number_id","to","agent_id","use_case"],"properties":{"from_number_id":{"type":"string","description":"Active managed-number ID to present as caller ID."},"to":{"type":"string","example":"+14155550123","description":"Supported E.164 destination."},"agent_id":{"type":"string","description":"Omni agent that handles the connected call."},"use_case":{"type":"string","enum":["transactional","support","sales","collections","survey"],"description":"Declared calling purpose. Determines consent, identity, CNAM, and policy gates."},"routing_profile":{"type":"string","enum":["quality_first","balanced","cost_optimized"],"default":"balanced"}}},"TelephonyCall":{"type":"object","properties":{"object":{"type":"string","example":"telephony.call"},"id":{"type":"string","example":"tcall_..."},"from_number_id":{"type":"string"},"to":{"type":"string","example":"+14155550123"},"agent_id":{"type":"string"},"use_case":{"type":"string","enum":["transactional","support","sales","collections","survey"]},"routing_profile":{"type":"string","enum":["quality_first","balanced","cost_optimized"]},"status":{"type":"string","enum":["queued","dispatching","ringing","in_progress","completed","failed","canceled","dispatch_unknown"]},"attempt_count":{"type":"integer","minimum":0,"maximum":3},"max_attempts":{"type":"integer","minimum":1,"maximum":3},"failure_reason":{"type":["string","null"],"enum":["network_unavailable","congestion","timeout","no_answer","busy","rejected","invalid_number","identity_blocked","compliance_blocked","unknown",null]},"created_at":{"type":"integer","description":"Unix ms."},"updated_at":{"type":"integer","description":"Unix ms."},"answered_at":{"type":["integer","null"],"description":"Unix ms."},"ended_at":{"type":["integer","null"],"description":"Unix ms."},"artifacts":{"type":"object","description":"Post-call artifact and billing readiness. Included on the single-call detail response; pending until the media edge finalizes the call.","properties":{"status":{"type":"string","enum":["pending","ready","unavailable"]},"omni_call_id":{"type":"string"},"recording":{"type":"object","properties":{"status":{"type":"string"},"reason":{"type":["string","null"]},"resource":{"type":["string","null"]}}},"transcript":{"type":"object","properties":{"status":{"type":"string"},"resource":{"type":["string","null"]}}},"usage":{"type":"object","properties":{"status":{"type":"string","enum":["pending","recorded"]},"meter":{"type":"string","example":"telephony.minutes"},"units":{"type":"number"},"billable":{"type":"boolean"}}}}}}},"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_dorit_en_us"},"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":"Optional pre-generated audio preview (MP3). If absent, synthesize a short sample with this `voice_id`."},"accent":{"type":"string","description":"Normalized accent label used for matching."},"age_band":{"type":"string","description":"Normalized age band, e.g. 25-34."},"pitch_band":{"type":"string","description":"Normalized pitch band, e.g. low-medium."},"pace":{"type":"string","description":"Normalized speaking pace."},"use_cases":{"type":"array","items":{"type":"string"},"description":"Recommended Agent and content use cases."},"search_tags":{"type":"array","items":{"type":"string"},"description":"Search and auto-suggest tags."},"aliases":{"type":"array","items":{"type":"string"},"description":"Permanent convenience identifiers accepted on the voice's advertised surfaces. API responses report the canonical `voice_id`."},"available_on":{"type":"array","items":{"type":"string","enum":["speak","omni"]},"description":"Product surfaces on which this stock voice can be selected."},"synthesis_modes":{"type":"array","items":{"type":"string","enum":["streaming","async"]},"description":"Delivery modes advertised for this voice on `POST /v1/audio/speech`. Send `stream: false` for an async-only voice."},"tier":{"type":"string","enum":["standard","natural"],"description":"Customer-facing voice quality tier. The public catalog includes Standard and Natural voices; inspect `available_on` and `synthesis_modes` before use.","example":"standard"},"pricing":{"type":"object","required":["included_in_base_price","additional_price_usd_per_minute"],"description":"Voice-specific pricing on top of the selected product's base rate.","properties":{"included_in_base_price":{"type":"boolean","example":true},"additional_price_usd_per_minute":{"type":"number","minimum":0,"example":0}}},"source_dataset":{"type":"string","description":"Source dataset attribution."},"license":{"type":"string","description":"Source voice license identifier."}}},"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."},"role":{"type":["string","null"],"enum":["receptionist","support","sales","collections","ea","concierge","custom",null],"description":"Role archetype. Adds PyAI's role operating standard beneath your persona and derives the runtime mode (`receptionist`, `sales`, and `collections` enable their matching deterministic conversation guards; everything else runs the default mode). Your stored persona is unchanged and remains authoritative for identity and business policy. Null clears the role."},"greeting":{"type":["string","null"],"maxLength":1000,"description":"Greeting message, opening line spoken at turn 0 when a call connects (before the caller speaks). Stored on the agent profile; played automatically when connecting with `session_label={agent_id}`. May also be sent inline in the Omni `configure` frame."},"greeting_variants":{"type":["array","null"],"maxItems":15,"items":{"type":"string","maxLength":1000},"description":"Approved opening-line variants. PyAI selects one for each newly resolved call profile; null or empty falls back to greeting. Recording consent is fixed and never rotated."},"voice_id":{"type":["string","null"],"description":"A stock `voice_id`, one of its permanent `aliases` from `GET /v1/voices`, or a cloned voice id. Omni reports the canonical served stock id in `configured.voice_id`."},"voice_instruct":{"type":["string","null"],"maxLength":200,"description":"TTS delivery direction for this managed Agent on instruct-capable voice tiers. Omit or set null for PyAI's natural conversational pace; an inline Omni `configure.voice_instruct` wins for that session."},"brain_model":{"type":["string","null"],"description":"Per-agent model selection. Omit for the platform default."},"barge_sensitivity":{"type":["string","null"],"deprecated":true,"description":"Deprecated compatibility field. Stored values are not applied to Omni runtime."},"ack_mode":{"type":["string","null"],"deprecated":true,"description":"Deprecated compatibility field. Stored values are not applied to Omni runtime."},"idle_check_in":{"type":["string","null"],"enum":["auto","patient","off",null],"description":"How patient the agent is before checking in on a silent caller (\"Sorry, are you still there?\"). `auto` checks in after a few seconds of silence; `patient` waits far longer, for callers who routinely think, read, or look something up mid-call; `off` disables the check-in entirely, so the agent stays silent until the caller speaks. When unset, most roles render `auto`; support agents render `patient`. Independent of `ack_mode`. May also be sent inline in the Omni `configure` frame, which wins for that session."},"persona_perspective":{"type":["string","null"],"enum":["agent","caller",null],"description":"Which side of the call the persona is on. `agent` (default) means the persona is the business being called, so PyAI adds its conversation layer for handling a caller (capability honesty, handoffs, turn discipline). `caller` means the persona is the individual on the call instead, as in QA and simulation callers, mystery shopping, or training partners; PyAI drops that operator-voice layer so it cannot contradict an inverted persona. May also be sent inline in the Omni `configure` frame, which wins for that session."},"recordings_enabled":{"type":["boolean","null"],"description":"Enable stereo call recordings. Default false."},"consent_line":{"type":["string","null"],"minLength":10,"maxLength":500,"description":"Recording disclosure spoken before recording starts when `recordings_enabled` is true. Required for compliance when recordings are on and must contain a meaningful spoken phrase (at least 10 characters and 5 letters). Playback order: consent_line, then greeting, then conversation."},"language":{"type":["string","null"],"enum":["en","fr","es","de","hi",null],"description":"Requested language for this Agent's Omni sessions. `null` or absent means `en`. Availability is staged: as of 2026-08-13 public serving is `en`, `fr`, `es`, and `hi`; `de` falls back to English. Inspect the session's `configured.language_active` and `language_fallback` fields before assuming the requested language is active. See the Language support reference."},"metadata":{"type":["object","null"],"additionalProperties":{"type":"string"},"description":"Up to 16 key/value annotations (keys ≤64 chars, values ≤512 chars)."},"vocabulary":{"type":["array","null"],"maxItems":5,"items":{"type":"string","minLength":4,"maxLength":64},"description":"Optional custom vocabulary for this Agent's Omni speech recognition. A non-empty list is the opt-in. PyAI keeps at most five effective terms, with at most five words per term. It trims whitespace, deduplicates without regard to case while preserving the first spelling and order, and drops common-only phrases. Set `[]` or `null` to turn it off. The list is fixed when a session starts. Organization Hear vocabulary is never applied to Omni."},"keyterms":{"type":["array","null"],"items":{"type":"string"},"maxItems":100,"deprecated":true,"description":"Deprecated stored compatibility field. It does not affect speech recognition. Use `vocabulary`."},"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`). Omni `configure.tools[]` may list hosted catalog names or client-loop schemas; server webhooks must be registered here. An inline configure `endpoint` is not supported."},"continuity":{"type":["boolean","null"],"description":"Reuse a tiny caller card on the next call when PyAI can resolve the caller (phone number or a signed customer id). The card is advisory — the agent may recall a name or open thread, and must not refund, book, or transfer from it. Default false."}}},"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"]},"greeting_variants":{"type":"array","maxItems":15,"items":{"type":"string"}},"voice_id":{"type":["string","null"]},"voice_instruct":{"type":"string","description":"Effective TTS delivery direction for instruct-capable voice tiers. Renders PyAI's natural conversational pace when no override is stored."},"brain_model":{"type":"string","example":"default"},"barge_sensitivity":{"type":["string","null"],"deprecated":true},"ack_mode":{"type":["string","null"],"deprecated":true},"idle_check_in":{"type":"string","enum":["auto","patient","off"],"description":"Idle check-in patience. Renders `auto` when unset, except support agents render `patient`."},"persona_perspective":{"type":"string","enum":["agent","caller"],"description":"Which side of the call the persona is on. Renders the effective default (`agent`) when unset."},"mode":{"type":"string","enum":["default","receptionist","sales","collections"],"readOnly":true,"example":"default","description":"Read-only runtime mode derived from the agent's role (`default`, `receptionist`, `sales`, or `collections`). Set indirectly via the writable `role` field."},"role":{"type":["string","null"],"description":"Role archetype the agent runs as, or null. Writable on create and update."},"continuity":{"type":"boolean","description":"Whether this agent reuses a prior-call caller card when a caller key is available. Default false."},"recordings_enabled":{"type":"boolean"},"consent_line":{"type":["string","null"]},"language":{"type":"string","enum":["en","fr","es","de","hi"],"description":"Session language for this agent's calls. Renders the effective default (`en`) when unset."},"vocabulary":{"type":"array","maxItems":5,"items":{"type":"string","minLength":4,"maxLength":64},"description":"Sanitized Agent vocabulary. An empty list means speech-recognition biasing is off."},"keyterms":{"type":"array","items":{"type":"string"},"deprecated":true},"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"},"input_schema":{"type":"object","additionalProperties":true,"description":"Per-call model argument schema. Present on agent reads when the tool resolves."},"execution":{"type":"string","enum":["hosted","server","engine","client"],"description":"Resolved execution mode. Present on agent reads when the tool resolves."}}},"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 URL PyAI calls when this server tool runs. Public HTTPS only; private, loopback, and metadata addresses are rejected at registration."},"execution":{"type":"string","enum":["server","client"],"description":"How the tool runs. server = PyAI calls webhook_url (default when webhook_url is set; works on phone calls). client = your connected app executes it over the Omni socket. Register server tools here; do not put webhook_url on the Omni configure frame."},"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,"description":"Per-call budget (ms). Note the executor also enforces a platform hard ceiling (5000 ms by default); the effective budget is min(timeout_ms, ceiling). On timeout the model receives a synthetic soft `{ \"error\": \"tool timed out\" }` and the turn continues."}}},"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"]},"rotate_secret":{"type":"boolean","description":"When true, mint a NEW webhook signing secret and return it once as `hmac_secret`. Zero-drop rotation: deploy verification that accepts both the old and new secret, call this, then drop the old one."}}},"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},"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":100},"offset":{"type":"integer","minimum":0,"maximum":10000}}},"AgentList":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}},"has_more":{"type":"boolean","example":false}}}}}}