openapi: 3.1.0 info: title: VoiceBlender API description: | VoiceBlender bridges SIP and WebRTC voice calls with multi-party audio mixing, real-time speech-to-text, text-to-speech, AI agent integration, recording, and webhook-based event delivery. x-config-vars: - name: INSTANCE_ID default: (auto-generated UUID) description: Instance identifier included in all API responses and webhook events - name: HTTP_ADDR default: :8080 description: REST API listen address - name: ALLOWED_IPS default: (empty = allow all) description: Comma-separated allowlist of IPs and CIDR ranges (IPv4 and IPv6) gating every HTTP endpoint, including /v1/vsi, /v1/legs/websocket, /v1/legs/moq, /metrics, and pprof. Bare addresses become /32 (v4) or /128 (v6); malformed entries fail server startup. Only X-Forwarded-For is consulted as a proxy header (see TRUST_PROXY_HEADERS). - name: TRUST_PROXY_HEADERS default: "false" description: When true, the client IP used for the ALLOWED_IPS check is taken from the leftmost X-Forwarded-For entry (falling back to the socket peer if absent). Enable only behind a trusted reverse proxy that overwrites the header — otherwise it is client-spoofable. - name: SIP_BIND_IP default: 127.0.0.1 description: IPv4 address advertised in SDP, Contact, and Via headers (and used as the listen address when SIP_LISTEN_IP is empty) - name: SIP_LISTEN_IP default: (same as SIP_BIND_IP) description: UDP socket bind IP. Accepts 127.0.0.1, 0.0.0.0, ::, or any literal v4/v6 address - name: SIP_BIND_IPV6 default: (empty = v4-only) description: IPv6 address advertised in SDP/Contact/Via for IPv6 calls. Set this for IPv6-only or dual-stack deployments - name: SIP_LISTEN_IPV6 default: (same as SIP_BIND_IPV6) description: Optional separate IPv6 socket bind address (used when running with both 0.0.0.0 and a specific v6 literal) - name: SIP_EXTERNAL_IP default: "" description: Public IPv4 address for NAT/Docker deployments. When set, used in SIP Contact headers and SDP media (c=) lines instead of the bind IP. IPv6 has no equivalent — set SIP_BIND_IPV6 to the address you want advertised. - name: SIP_PORT default: "5060" description: SIP listen port - name: SIP_TLS_PORT default: (disabled) description: SIP-over-TLS listen port (typically 5061). When set, SIP_TLS_CERT and SIP_TLS_KEY must also be provided. Required for WhatsApp Business Calling integration. - name: SIP_TLS_CERT default: "" description: Path to PEM-encoded TLS certificate (e.g. fullchain.pem). Meta rejects self-signed certs — use a CA-signed cert matching a public FQDN. - name: SIP_TLS_KEY default: "" description: Path to PEM-encoded TLS private key (e.g. privkey.pem). - name: SIP_DEBUG default: "false" description: When true, log the full RFC 3261 wire form of every inbound and outbound SIP request and response. Very verbose — use only for troubleshooting. - name: SIP_DOMAIN default: (falls back to advertised IP) description: FQDN advertised in From, Contact and Via on all outbound SIP signalling (classic trunks and WhatsApp). Should match the SAN on SIP_TLS_CERT and any allowlist your carrier or Meta keeps. - name: SIP_HOST default: voiceblender description: SIP User-Agent name - name: SIP_CODECS default: PCMU,PCMA description: 'Comma-separated, preference-ordered list of codecs the SIP engine offers on outbound INVITEs and accepts on inbound INVITEs. Recognized names (case-insensitive): PCMU, PCMA, G722, opus, AMR-WB, AMR-NB (bare token AMR resolves to AMR-NB per RFC 4867 §8.1). Unknown names and duplicates are dropped silently.' - name: SIP_AUTO_RINGING default: "false" description: When true, the server sends 180 Ringing automatically after 100 Trying. Default sends only 100 Trying; the API caller drives ringing via /ring, /early-media, or /answer. - name: SIP_USE_SOURCE_SOCKET default: "false" description: When true, route SIP responses and in-dialog requests (BYE, re-INVITE, UPDATE, INFO, NOTIFY, REFER) back to the request's source UDP socket instead of the peer's Contact / Via sent-by. Enable when peers advertise unroutable addresses (e.g. private IPs in Contact from behind NAT). - name: SIP_REGISTRATION_DEFAULT_EXPIRES_SECONDS default: "3600" description: Expiry used when an inbound REGISTER carries no Expires value. - name: SIP_REGISTRATION_MAX_EXPIRES_SECONDS default: "7200" description: Upper clamp on the granted REGISTER expiry. Requests above this value are honored at this maximum. - name: SIP_REGISTRATION_SWEEP_INTERVAL_MS default: "1000" description: Sweeper period (ms) for evicting expired AOR bindings. - name: SIP_REGISTRATION_ALLOW_MULTIPLE_CONTACTS default: "true" description: When true, the same AOR may be bound from multiple Contacts simultaneously (and POST /v1/legs parallel-forks to every bound contact). When false, each REGISTER replaces any prior Contacts for the AOR. - name: ICE_SERVERS default: stun:stun.l.google.com:19302 description: STUN/TURN URLs for WebRTC ICE, comma-separated - name: WEBRTC_EXTERNAL_IPS default: (empty) description: Comma-separated public IPs advertised as host ICE candidates (pion SetNAT1To1IPs). Required when VB runs behind NAT/Docker so peers behind firewalls can reach it; supports IPv4 and IPv6 literals. The literal value "auto" triggers STUN-based public-IP discovery at startup using the configured ICE_SERVERS; failure is non-fatal. - name: RTP_PORT_MIN default: "10000" description: Minimum UDP port for RTP/RTCP media - name: RTP_PORT_MAX default: "20000" description: Maximum UDP port for RTP/RTCP media - name: DEFAULT_SAMPLE_RATE default: "16000" description: 'Default mixer sample rate (Hz) for new rooms when sample_rate is not specified. Allowed: 8000, 16000, 48000.' - name: SPEECH_DETECTION_ENABLED default: "false" description: Emit speaking.started / speaking.stopped events for every connected leg by default. Per-call speech_detection on POST /v1/legs or POST /v1/legs/{id}/answer overrides this. - name: VSI_EVENT_BUFFER_SIZE default: "256" description: 'Per-client buffer (in events) on the /v1/vsi WebSocket. When the client falls behind, new events are dropped and the next delivered event carries an events_dropped notification. Clamped to [16, 1000000]. Memory: ~1 KB × buffer per connection at the default.' - name: AMRWB_MODE default: "2" description: 'AMR-WB (G.722.2) encoder speech-mode ceiling 0..8: 0=6.60, 1=8.85, 2=12.65, 3=14.25, 4=15.85, 5=18.25, 6=19.85, 7=23.05, 8=23.85 kbit/s. The actual transmit mode is this ceiling clamped to the peer''s negotiated mode-set. Default 2 matches GSMA IR.92 / VoLTE.' - name: AMRWB_OCTET_ALIGNED default: "true" description: Offer octet-aligned AMR-WB framing (RFC 4867) in outbound SDP. When false, offers bandwidth-efficient framing. On answers, VoiceBlender always echoes the framing the peer negotiated. - name: AMRNB_MODE default: "7" description: 'AMR-NB (RFC 4867) encoder speech-mode ceiling 0..7: 0=4.75, 1=5.15, 2=5.90, 3=6.70, 4=7.40, 5=7.95, 6=10.2, 7=12.2 kbit/s. The actual transmit mode is this ceiling clamped to the peer''s negotiated mode-set. Default 7 is GSM-EFR-equivalent 12.2 kbit/s.' - name: AMRNB_OCTET_ALIGNED default: "true" description: Offer octet-aligned AMR-NB framing (RFC 4867) in outbound SDP. When false, offers bandwidth-efficient framing. On answers, VoiceBlender always echoes the framing the peer negotiated. - name: RECORDING_DIR default: /tmp/recordings description: Local directory for recording output files - name: LOG_LEVEL default: info description: 'Log verbosity: debug, info, warn, error. Transcript text, DTMF digits and event payloads are logged only at debug.' - name: WEBHOOK_URL default: "" description: Global webhook URL for event delivery (fallback when no per-leg or per-room webhook is set) - name: WEBHOOK_SECRET default: "" description: HMAC-SHA256 signing secret for the global webhook - name: ELEVENLABS_API_KEY default: "" description: API key for ElevenLabs TTS, STT, and Agent provider - name: VAPI_API_KEY default: "" description: API key for VAPI Agent provider - name: DEEPGRAM_API_KEY default: "" description: API key for Deepgram STT and TTS - name: AZURE_SPEECH_KEY default: "" description: Subscription key for Azure Cognitive Speech Services (TTS and STT) - name: AZURE_SPEECH_REGION default: eastus description: Azure region for Speech Services (e.g. eastus, westeurope) - name: S3_BUCKET default: "" description: S3 bucket name for recording uploads - name: S3_REGION default: us-east-1 description: AWS region for S3 - name: S3_ENDPOINT default: "" description: Custom S3-compatible endpoint (e.g. MinIO) - name: S3_PREFIX default: "" description: Key prefix applied to all S3 objects - name: S3_ALLOW_INSECURE_ENDPOINT default: "false" description: 'Allow a plaintext http:// S3_ENDPOINT on a non-local host, rather than refusing to ship recording audio in cleartext (startup: exit 1; per request: 400). Loopback, private and link-local addresses, single-label hostnames and .internal/.local names are exempt and need no opt-in.' - name: S3_PREFLIGHT_TIMEOUT default: 10s description: Budget for the HeadBucket probe run at startup. A bucket the store reports absent exits 1; any other probe failure (403 without s3:ListBucket, 5xx, unreachable) is warned about and startup continues. 0 disables the probe. - name: S3_REQUEST_PREFLIGHT_TIMEOUT default: 2s description: Budget for the HeadBucket probe run when a request supplies s3_bucket. A bucket the store reports absent returns 400; any other probe failure is warned about and the recording proceeds. Kept short because it runs inside record-start, which on VSI occupies the connection's command loop. 0 disables the probe. - name: GCS_BUCKET default: "" description: Google Cloud Storage bucket for recording uploads via the native GCS API (storage=gcs). Uses Application Default Credentials / Workload Identity — preferred over S3_ENDPOINT=https://storage.googleapis.com on GKE. - name: GCS_OBJECT_NAME_PREFIX default: "" description: Object name prefix applied to all GCS uploads (e.g. recordings or a bare workspace id). A trailing slash is added automatically when missing. - name: AWS_ACCESS_KEY_ID default: "" description: '[SDK-resolved, not read by VoiceBlender] AWS access key for S3 uploads and AWS Polly TTS. Consumed by the AWS SDK default credential chain alongside AWS_SECRET_ACCESS_KEY and the optional AWS_SESSION_TOKEN.' - name: AWS_SECRET_ACCESS_KEY default: "" description: '[SDK-resolved, not read by VoiceBlender] AWS secret key paired with AWS_ACCESS_KEY_ID.' - name: AWS_SESSION_TOKEN default: "" description: '[SDK-resolved, not read by VoiceBlender] Optional temporary-credential session token (STS / SSO) used together with AWS_ACCESS_KEY_ID/SECRET.' - name: AWS_PROFILE default: "" description: '[SDK-resolved, not read by VoiceBlender] Profile name in ~/.aws/credentials to use instead of static AWS_* env vars.' - name: AWS_REGION default: "" description: '[SDK-resolved, not read by VoiceBlender] AWS region used by S3 and Polly when S3_REGION is empty.' - name: GOOGLE_APPLICATION_CREDENTIALS default: "" description: '[SDK-resolved, not read by VoiceBlender] Path to a Google Cloud service-account JSON file used by Google Cloud TTS and by GCS recording uploads when no other credential source is available. Consumed by Google''s Application Default Credentials chain (env var → gcloud ADC → GCE/GKE metadata / Workload Identity).' - name: TTS_CACHE_ENABLED default: "false" description: Enable disk-backed TTS audio cache; cached audio persists across restarts - name: TTS_CACHE_DIR default: /tmp/tts_cache description: Directory for cached TTS audio files (used when TTS_CACHE_ENABLED=true) - name: TTS_CACHE_INCLUDE_API_KEY default: "false" description: Include API key in TTS cache key; set true if different keys map to different voice clones - name: SIP_JITTER_BUFFER_MS default: "0" description: SIP ingress jitter buffer target delay in ms (0 = disabled passthrough). Applies to every SIP leg. - name: SIP_JITTER_BUFFER_MAX_MS default: "300" description: Maximum depth of the SIP ingress jitter buffer in ms. Frames beyond this are dropped oldest-first to catch up after a stall. - name: SIP_SDP_STRICT_MLINE_ANSWER default: "false" description: Emit a port-0 placeholder for every offered m= section we do not accept, so answers carry the same m-line count and order as the offer (RFC 3264 §6). Gated separately from multi-stream because it changes the SDP single-stream calls emit whenever a peer offers a section we don't handle, such as video. - name: SIP_REFER_AUTO_DIAL default: "false" description: 'When true, accept incoming SIP REFER requests and automatically originate the transferred call. Default-deny: stays off unless the SIP edge is locked down (IP allow-lists, digest auth) because auto-dialing arbitrary Refer-To URIs is a classic toll-fraud vector. Outbound transfers initiated via the REST API are unaffected by this flag.' - name: SIP_TCP_ENABLED default: "false" description: 'Listen for SIP over TCP on SIP_PORT alongside the UDP listener. Recommended with SIPREC: a recording session''s INVITE carries the metadata document alongside the SDP and is larger than RFC 3261 section 18.1.1 allows over UDP.' - name: SIPREC_ENABLED default: "false" description: 'Accept inbound SIPREC recording sessions (RFC 7866), where an SBC or PBX forks a call''s media to this server. Off by default: when off, an INVITE carrying Require: siprec is rejected with 420 Bad Extension and one that only hints at SIPREC with 488.' - name: SIPREC_AUTO_ANSWER default: "true" description: Answer an inbound recording session immediately instead of parking it until POST /v1/legs/{id}/answer. A session recording client does not wait for an application decision, so leaving this on is usually correct; turn it off to gate sessions from a controller. - name: SIPREC_MAX_STREAMS default: "8" description: Maximum number of m=audio sections accepted on one recording session. A session offering more is rejected with 486, bounding the RTP ports and goroutines a single peer can claim. - name: SIPREC_METADATA_MAX_BYTES default: "65536" description: Maximum size of the rs-metadata XML document in a SIPREC INVITE. A larger document is rejected with 413 rather than parsed. - name: SIPREC_SRC_ENABLED default: "false" description: 'Allow POST /v1/rooms/{id}/siprec to originate outbound recording sessions, forking a room''s participants to an external session recording server. Off by default: it lets an API caller stream a room''s audio to an arbitrary SIP destination.' - name: SIPREC_AUTO_RECORD default: "false" description: Start multi-channel recording automatically when a SIPREC session is accepted, one channel per recorded participant. When false, recording is driven through the usual /v1/legs/{id}/record endpoint. - name: SIPREC_ROOM_MODE default: none description: Where a recording session's audio streams are mixed. "none" attaches nothing and leaves placement to the stream API; "per_session" creates a room named siprec- and attaches every stream, which is what makes live STT and agents apply to a recorded call; "fixed" attaches every session's streams into SIPREC_ROOM_ID. - name: SIPREC_ROOM_ID default: "" description: Room every recording session's streams join when SIPREC_ROOM_MODE=fixed. Ignored in the other modes. - name: MOQ_ENABLED default: "false" description: Enable the experimental MoQ (Media over QUIC) inbound leg endpoint at CONNECT /v1/legs/moq over WebTransport/HTTP/3. PoC quality, tracks IETF draft-11. When enabled, both MOQ_TLS_CERT_FILE and MOQ_TLS_KEY_FILE must be set. - name: MOQ_LISTEN_ADDR default: :8443 description: UDP address for the HTTP/3 listener that backs the MoQ leg. Independent of HTTP_ADDR — TCP/:8080 and UDP/:8443 can run side-by-side. - name: MOQ_TLS_CERT_FILE default: "" description: Path to the TLS certificate used by the HTTP/3 listener. Required when MOQ_ENABLED=true. - name: MOQ_TLS_KEY_FILE default: "" description: Path to the TLS private key used by the HTTP/3 listener. Required when MOQ_ENABLED=true. - name: MOQ_OPUS_BITRATE default: "24000" description: Target bitrate (bps) for the Opus encoder feeding the MoQ leg's mix track. Must be in 6000..510000. - name: LIVEKIT_ENABLED default: "false" description: Enable the livekit_room leg type at POST /v1/legs (type=livekit_room). Lets VoiceBlender join a LiveKit room as a participant and bridge audio between SIP and LiveKit. Speaks the LiveKit signaling protocol directly via livekit/protocol protobufs over the existing pion stack — no LiveKit SDK is used. - name: LIVEKIT_URL default: "" description: Default LiveKit server endpoint (wss://...). Required when LIVEKIT_ENABLED=true unless every request supplies livekit.url. Overridable per-request. - name: LIVEKIT_OPUS_BITRATE default: "24000" description: Target bitrate (bps) for the Opus encoder publishing audio into LiveKit. Must be in 6000..510000. Overridable per-request via livekit.opus_bitrate. - name: LIVEKIT_TOKEN_SIGNING_ENABLED default: "false" description: 'Opt-in: when true, callers may omit livekit.token and instead pass {room,identity,permissions}; VoiceBlender mints the JWT itself. Security caveat: enabling this stores the LiveKit API secret (a high-privilege credential) in VoiceBlender. Keep off in multi-tenant deployments.' - name: LIVEKIT_API_KEY default: "" description: LiveKit API key used to sign minted JWTs. Required only when LIVEKIT_TOKEN_SIGNING_ENABLED=true. - name: LIVEKIT_API_SECRET default: "" description: LiveKit API secret used to sign minted JWTs. Required only when LIVEKIT_TOKEN_SIGNING_ENABLED=true. Treat as a high-value secret; redact in logs. - name: LIVEKIT_DEFAULT_TOKEN_TTL default: 6h description: Default TTL applied to minted JWTs when the request omits livekit.token_ttl. Go duration string. LiveKit recommends ≤ 6 hours. version: 1.0.0 license: name: MIT servers: - url: http://localhost:8080/v1 description: Local development server tags: - name: Legs description: Voice call legs (SIP or WebRTC) - name: Rooms description: Multi-party audio conference rooms - name: WebRTC description: WebRTC peer connection establishment - name: Observability description: Metrics and health endpoints paths: /legs: post: operationId: createLeg summary: Originate an outbound leg description: 'Originate a new outbound leg. The `type` field selects the transport: `sip` originates a SIP INVITE; `whatsapp` originates a WhatsApp call through Meta; `websocket` dials a remote WebSocket endpoint (audio is PCM in either binary or `json_base64` framing, with bidirectional text and caller-supplied X-/P- headers).' tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateLegRequest' responses: '201': description: Leg created content: application/json: schema: $ref: '#/components/schemas/Leg' '400': description: Invalid JSON, bad URI/URL, unknown codec, or unsupported type content: application/json: schema: $ref: '#/components/schemas/Error' get: operationId: listLegs summary: List all active legs tags: - Legs responses: '200': description: Array of legs content: application/json: schema: type: array items: $ref: '#/components/schemas/Leg' /legs/websocket: get: operationId: wsLeg summary: Connect a WebSocket as a leg (HTTP upgrade) description: 'Upgrades the HTTP request to a WebSocket and creates a `websocket_in` leg. Query parameters: `sample_rate` (8000/16000/24000/48000; default 16000); `wire_format` (`binary` default, or `json_base64`); `sample_format` (`s16le` only in v1); `room_id` to auto-add the leg to a room; `app_id` for event filtering; `rtt=true` to enable the bidirectional text channel; `webhook_url`/`webhook_secret` for per-leg event routing. X-* and P-* request headers (plus Authorization) are captured into the leg''s `headers` map and surfaced on `leg.ringing` and in `LegView`. The leg goes straight to `connected` (no ringing/answer flow). Audio frames carry PCM16-LE mono at `sample_rate`; with `wire_format=binary` each WebSocket binary frame is exactly one 20ms PCM frame, with `json_base64` the same payload is wrapped as `{"type":"audio","audio":""}`. Text and control messages always use JSON text frames: `{"type":"text","text":...}`, `{"type":"ping","event_id":N}`/`{"type":"pong","event_id":N}`, and `{"type":"hangup"}` to terminate from the peer side.' tags: - Legs responses: '101': description: Switching Protocols — WebSocket upgrade succeeded '400': description: Invalid query parameters content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Room create failure content: application/json: schema: $ref: '#/components/schemas/Error' /legs/moq: post: operationId: moqLeg summary: Connect a MoQ (Media over QUIC) leg (WebTransport extended-CONNECT, experimental) description: |- **Actual HTTP method: `CONNECT`** (HTTP/3 extended-CONNECT for WebTransport). OpenAPI 3.1 does not define `connect` as a path-item method, so this operation is documented under `post` with an `x-actual-method: CONNECT` extension. Standard HTTP clients (e.g. `curl -X POST`) will receive `405 Method Not Allowed` — use a WebTransport-capable HTTP/3 client. **Experimental / PoC.** Upgrades an HTTP/3 extended-CONNECT request to a WebTransport session and creates an inbound MoQ leg. Reachable only over HTTP/3 on the MoQ listener (not on the regular HTTP/1.1 chi listener). Requires `MOQ_ENABLED=true` plus `MOQ_TLS_CERT_FILE` and `MOQ_TLS_KEY_FILE`. Speaks IETF draft-11 of moq-transport (via `mengelbart/moqtransport`); browser interop with draft-16 clients (moqtail, moq.dev) is not expected to work. Media is bidirectional within a single MoQ session per leg: the server publishes namespace `mix`/track `audio` (downlink, room mix) and subscribes to namespace `mic`/track `audio` (uplink, leg mic). Audio is Opus framed one frame per MoQ Object (LOC-style) at 48 kHz mono, 20 ms frames. Query parameters: `sample_rate` (only `48000` is accepted — the encoder/decoder are hard-wired to 48 kHz; default 48000); `room_id` to auto-add the leg to a room; `app_id` for event filtering; `webhook_url`/`webhook_secret` for per-leg event routing. X-* and P-* request headers (plus Authorization) are captured into the leg's `headers` map and surfaced on `LegView`. The leg goes straight to `connected` (no ringing/answer flow); no DTMF, no RTT, and event parity is limited to `leg.connected` / `leg.disconnected`. tags: - Legs responses: '200': description: WebTransport extended-CONNECT accepted; MoQ session established (no JSON body — the response is the upgraded WebTransport session) '400': description: Invalid query parameters or config content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Room create failure content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: MoQ endpoint is not enabled (`MOQ_ENABLED=false`) content: application/json: schema: $ref: '#/components/schemas/Error' x-actual-method: CONNECT /legs/{id}: parameters: - $ref: '#/components/parameters/LegId' get: operationId: getLeg summary: Get a single leg tags: - Legs responses: '200': description: Leg details content: application/json: schema: $ref: '#/components/schemas/Leg' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: deleteLeg summary: Hang up a leg (asynchronous) description: |- Validates the leg exists and queues a hangup. The HTTP call returns 202 as soon as the leg is found; the SIP work and cleanup run in the background, and the eventual disconnection is observed via the `leg.disconnected` event. Without a request body the legacy behavior is preserved: SIP BYE on connected legs (`cdr.reason: "api_hangup"`), or dialog cancel on unanswered inbound legs (`cdr.reason: "caller_cancel"`). With `{"reason": ""}` and an unanswered SIP inbound leg (state `ringing` or `early_media`), VoiceBlender sends a final non-2xx response instead of BYE/cancel: `busy`→486, `declined`/`rejected`→603, `unavailable`→480, `not_found`→404, `forbidden`→403, `server_error`→500. The reason value is passed through to `leg.disconnected`'s `cdr.reason`. For connected legs the request body is ignored. tags: - Legs requestBody: content: application/json: schema: $ref: '#/components/schemas/DeleteLegRequest' responses: '202': description: Hangup queued '400': description: Unknown reason value content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/answer: parameters: - $ref: '#/components/parameters/LegId' post: operationId: answerLeg summary: Answer a ringing or early-media inbound SIP leg (asynchronous) description: Signals the inbound-call goroutine to send 200 OK. The HTTP call returns 202 immediately; the actual SIP 200 OK is sent in the background, and the leg's transition is observed via `leg.connected`. Pre-condition failures (wrong state, unknown codec) still return 4xx synchronously. When the caller offers several m=audio sections they are all accepted; `streams` optionally routes each accepted stream beyond the primary into its own room, applied once the answer is negotiated. tags: - Legs requestBody: content: application/json: schema: $ref: '#/components/schemas/AnswerLegRequest' responses: '202': description: Answer queued '400': description: Not a SIP inbound leg, invalid body, or codec not in offer content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg is not in ringing or early_media state content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/ring: parameters: - $ref: '#/components/parameters/LegId' post: operationId: ringLeg summary: Send 180 Ringing on a ringing inbound SIP leg (asynchronous) description: 'Queues a SIP 180 Ringing provisional response with no SDP. Use when `SIP_AUTO_RINGING=false` (the default) and you want to indicate alerting before deciding to early-media or answer. Idempotent: each call emits another 180 — receivers tolerate re-sends. The HTTP call returns 202 as soon as the request is validated; SIP-level send failures surface as `leg.command_failed` with `command="ring"`.' tags: - Legs responses: '202': description: 180 Ringing queued '400': description: Not a SIP inbound leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg is not in ringing state content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/challenge: parameters: - $ref: '#/components/parameters/LegId' post: operationId: challengeLeg summary: Challenge a ringing inbound SIP leg with a 401 digest auth request description: Sends a SIP 401 Unauthorized carrying a `WWW-Authenticate` digest challenge on an unanswered inbound INVITE. The current leg is torn down (a `leg.disconnected` with `reason="challenged"` is published); the UAC's credentialed re-INVITE arrives as a new inbound call surfaced via `leg.ringing` with `authenticated=true` once VoiceBlender verifies the response against the supplied credential. Provide either `password` or `ha1`. An invalid retry is answered with 403 Forbidden and never surfaced. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ChallengeRequest' responses: '202': description: 401 challenge queued '400': description: Not a SIP inbound leg, or missing realm/credential content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg is not in ringing or early_media state content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/early-media: parameters: - $ref: '#/components/parameters/LegId' post: operationId: earlyMediaLeg summary: Enable early media on a ringing inbound SIP leg (asynchronous) description: Queues a SIP 183 Session Progress with SDP and the RTP/codec setup. The HTTP call returns 202 as soon as the request is validated; the leg transitions to `early_media` state asynchronously, observable via `leg.early_media`. Setup failures surface as `leg.command_failed` with `command="early_media"`. tags: - Legs requestBody: content: application/json: schema: $ref: '#/components/schemas/EarlyMediaLegRequest' responses: '202': description: Early media queued '400': description: Not a SIP inbound leg or codec not in offer content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg is not in ringing state content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/mute: parameters: - $ref: '#/components/parameters/LegId' post: operationId: muteLeg summary: Mute a leg description: A muted leg's audio is excluded from the room mix and speaking events are suppressed. Taps (recording/STT) still receive the muted leg's own audio. tags: - Legs responses: '200': description: Leg muted content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: unmuteLeg summary: Unmute a leg tags: - Legs responses: '200': description: Leg unmuted content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/hold: parameters: - $ref: '#/components/parameters/LegId' post: operationId: holdLeg summary: Put a SIP call on hold (asynchronous) description: Queues a re-INVITE with `sendonly` SDP direction. The HTTP call returns 202 as soon as the leg is validated; the re-INVITE is sent in the background and success surfaces as `leg.hold`. Failures surface as `leg.command_failed` with `command="hold"`. The RTP timeout is paused while held, and a 2-hour auto-hangup timer starts. tags: - Legs responses: '202': description: Hold queued '400': description: Not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Hold not supported for this leg type (e.g. WhatsApp) content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: unholdLeg summary: Resume a held SIP call (asynchronous) description: Queues a re-INVITE with `sendrecv` SDP direction. The HTTP call returns 202; success surfaces as `leg.unhold`, failures as `leg.command_failed` with `command="unhold"`. tags: - Legs responses: '202': description: Unhold queued '400': description: Not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Hold not supported for this leg type (e.g. WhatsApp) content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/transfer: parameters: - $ref: '#/components/parameters/LegId' post: operationId: transferLeg summary: Transfer a SIP leg via REFER (asynchronous) description: Asynchronously transfers a SIP leg. The HTTP call returns 202 as soon as the request is validated; the REFER is sent in the background and its outcome is surfaced through `leg.transfer_initiated` / `leg.transfer_progress` / `leg.transfer_completed` / `leg.transfer_failed` events. Blind transfer when `replaces_leg_id` is omitted; attended transfer when present (the named leg's dialog identity is embedded as a Replaces parameter per RFC 3891). On terminal 2xx the leg (and the replaces leg, if any) is hung up automatically. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TransferRequest' responses: '202': description: Transfer request accepted for processing '400': description: Missing or invalid target URI (including URIs without a host such as sip:) content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg not connected, not a SIP leg, or replaces_leg_id is invalid content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/transfer/accept: parameters: - $ref: '#/components/parameters/LegId' post: operationId: acceptTransfer summary: Accept a parked inbound REFER description: Accepts an inbound transfer surfaced via `leg.transfer_requested` (default app-driven model, `SIP_REFER_AUTO_DIAL=false`). Sends `202 Accepted` to the referrer and a `NOTIFY` sipfrag `100 Trying`, keeping the refer subscription open. `{id}` is the referrer leg (the leg that received the REFER). After accepting, re-bridge as needed and report progress via `.../transfer/progress` and the outcome via `.../transfer/complete`. tags: - Legs responses: '202': description: Accept queued (202 + NOTIFY 100 sent) '404': description: No pending transfer for the leg (unknown or already decided) content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/transfer/progress: parameters: - $ref: '#/components/parameters/LegId' post: operationId: transferProgress summary: Report interim transfer progress description: Sends an interim sipfrag `NOTIFY` (e.g. `180 Ringing`) on an accepted inbound transfer so the referrer's UA reflects real progress. The subscription stays active. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TransferProgressRequest' responses: '202': description: Progress NOTIFY queued '400': description: status_code out of range (must be 100-699) content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: No accepted transfer for the leg content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/transfer/complete: parameters: - $ref: '#/components/parameters/LegId' post: operationId: completeTransfer summary: Complete an accepted inbound transfer description: Terminates an accepted inbound transfer with a final sipfrag `NOTIFY` and emits `leg.transfer_completed` (on success) or `leg.transfer_failed`. `success:true` sends `200 OK`; otherwise `status_code`/`reason` (default 500) carry the failure. The referrer leg is left for the app to hang up. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TransferCompleteRequest' responses: '202': description: Terminal NOTIFY queued '404': description: No accepted transfer for the leg (accept it first) content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/transfer/decline: parameters: - $ref: '#/components/parameters/LegId' post: operationId: declineTransfer summary: Decline a parked inbound REFER description: Rejects a parked (not-yet-accepted) inbound transfer, replying to the referrer with a non-2xx (`603 Decline` by default; override via `code`/`reason`) and emitting `leg.transfer_failed`. tags: - Legs requestBody: content: application/json: schema: $ref: '#/components/schemas/TransferDeclineRequest' responses: '202': description: Decline queued '404': description: No pending transfer for the leg (unknown or already accepted) content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/dtmf: parameters: - $ref: '#/components/parameters/LegId' post: operationId: sendDTMF summary: Send DTMF digits on a leg tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DTMFRequest' responses: '200': description: Digits sent content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or empty digits content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: DTMF writer unavailable content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/dtmf/accept: parameters: - $ref: '#/components/parameters/LegId' post: operationId: acceptDTMFLeg summary: Enable DTMF reception on a leg description: Allow this leg to receive DTMF digits broadcast from other legs in the same room. This is the default state for new legs. tags: - Legs responses: '200': description: DTMF reception enabled content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/dtmf/reject: parameters: - $ref: '#/components/parameters/LegId' post: operationId: rejectDTMFLeg summary: Disable DTMF reception on a leg description: Block this leg from receiving DTMF digits broadcast from other legs in the same room. DTMF received from this leg's own far end is still emitted as a leg.dtmf event. tags: - Legs responses: '200': description: DTMF reception disabled content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/rtt: parameters: - $ref: '#/components/parameters/LegId' post: operationId: sendRTT summary: Send Real-Time Text (T.140) on a SIP leg description: Sends UTF-8 text on the leg's RTT (T.140 / RFC 4103) media stream. Requires that the SDP offer/answer agreed on an m=text section with the remote UA. Enable RTT on the server with RTT_ENABLED=true. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RTTRequest' responses: '200': description: Text sent content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or empty text content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: RTT was not negotiated for this leg content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Send failed content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/rtt/accept: parameters: - $ref: '#/components/parameters/LegId' post: operationId: acceptRTTLeg summary: Enable RTT reception on a leg description: Allow this leg to receive RTT text broadcast from other legs in the same room and to emit rtt.received events for incoming text. Default state for new legs. tags: - Legs responses: '200': description: RTT reception enabled content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/rtt/reject: parameters: - $ref: '#/components/parameters/LegId' post: operationId: rejectRTTLeg summary: Disable RTT reception on a leg description: Block this leg from receiving RTT text broadcast from other legs in the same room and suppress rtt.received events for this leg. tags: - Legs responses: '200': description: RTT reception disabled content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/play: parameters: - $ref: '#/components/parameters/LegId' post: operationId: playLeg summary: Start audio playback to a leg tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PlaybackRequest' responses: '200': description: Playback started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or volume out of range content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg has no audio writer content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/play/{playbackID}: parameters: - $ref: '#/components/parameters/LegId' - $ref: '#/components/parameters/PlaybackId' patch: operationId: volumePlayLeg summary: Change the volume of an active leg playback tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VolumeRequest' responses: '200': description: Volume updated content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or volume out of range content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Playback not found content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: stopPlayLeg summary: Stop audio playback on a leg tags: - Legs responses: '200': description: Playback stopped content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No playback in progress content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/tts: parameters: - $ref: '#/components/parameters/LegId' post: operationId: ttsLeg summary: Synthesize speech and play it on a leg description: Synthesizes the provided text using the configured TTS provider and plays the audio on the leg. When `TTS_CACHE_ENABLED=true`, identical requests (same text, voice, model, language, and prompt) are stored on disk in `TTS_CACHE_DIR` and persist across restarts, without calling the external provider. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TTSRequest' responses: '200': description: TTS playback started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON, missing text/voice, or volume out of range content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg has no audio writer content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No API key provided for the selected provider content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/record: parameters: - $ref: '#/components/parameters/LegId' post: operationId: recordLeg summary: Start recording a leg to a WAV file description: For SIP legs, recording is stereo (left=incoming, right=outgoing). For legs in a room, stereo at 16kHz (left=participant audio, right=mixed-minus-self). tags: - Legs requestBody: content: application/json: schema: $ref: '#/components/schemas/RecordingRequest' responses: '200': description: Recording started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid storage type, S3 not configured, or invalid S3 credentials content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg has no audio reader or room not found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Failed to create recording file content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: stopRecordLeg summary: Stop recording a leg tags: - Legs responses: '200': description: Recording stopped content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No recording in progress content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/record/pause: parameters: - $ref: '#/components/parameters/LegId' post: operationId: pauseRecordLeg summary: Pause a leg recording description: 'Replaces incoming audio with silence on the active recording until `/record/resume` is called. The WAV''s timeline is preserved (silent gap where audio was paused), so reviewers can see exactly when sensitive data was excluded. Idempotent: calling while already paused returns `status: already_paused`.' tags: - Legs responses: '200': description: Recording paused (or already paused) content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No recording in progress content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/record/resume: parameters: - $ref: '#/components/parameters/LegId' post: operationId: resumeRecordLeg summary: Resume a paused leg recording description: 'Resumes writing real audio after a prior `/record/pause`. Idempotent: calling while not paused returns `status: not_paused`.' tags: - Legs responses: '200': description: Recording resumed (or was not paused) content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No recording in progress content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/stt: parameters: - $ref: '#/components/parameters/LegId' post: operationId: sttLeg summary: Start real-time speech-to-text on a leg tags: - Legs requestBody: content: application/json: schema: $ref: '#/components/schemas/STTRequest' responses: '200': description: STT started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg not connected, STT already running, or no audio reader content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No ElevenLabs API key provided content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: stopSTTLeg summary: Stop speech-to-text on a leg tags: - Legs responses: '200': description: STT stopped content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No STT in progress content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/stt/finalize: parameters: - $ref: '#/components/parameters/LegId' post: operationId: finalizeSTTLeg summary: Flush the STT buffer on a leg without stopping STT description: Forces the provider to emit a final transcript for the audio buffered so far while the session keeps running, so a caller that knows the speaker has finished does not have to wait for the provider's own endpointing. Only Deepgram supports this; the Azure and ElevenLabs integrations answer 501. The flushed transcript arrives on the usual stt.text event with is_final true — a segment containing no speech produces no event at all, so do not block on one. tags: - Legs responses: '200': description: Final transcript requested content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No STT in progress content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: STT session not connected or the flush failed content: application/json: schema: $ref: '#/components/schemas/Error' '501': description: The active STT provider does not support finalize content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/agent/elevenlabs: parameters: - $ref: '#/components/parameters/LegId' post: operationId: agentLegElevenLabs summary: Attach an ElevenLabs ConvAI agent to a leg description: Bridges audio bidirectionally with an ElevenLabs conversational AI agent. Standalone legs use direct audio; legs in a room use mixer taps. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ElevenLabsAgentRequest' responses: '200': description: Agent started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or missing agent_id content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg not connected, agent already attached, or no audio reader/writer content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No ElevenLabs API key provided content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/agent/vapi: parameters: - $ref: '#/components/parameters/LegId' post: operationId: agentLegVAPI summary: Attach a VAPI agent to a leg description: Bridges audio bidirectionally with a VAPI conversational AI agent. Standalone legs use direct audio; legs in a room use mixer taps. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VAPIAgentRequest' responses: '200': description: Agent started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or missing assistant_id content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg not connected, agent already attached, or no audio reader/writer content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No VAPI API key provided content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/agent/pipecat: parameters: - $ref: '#/components/parameters/LegId' post: operationId: agentLegPipecat summary: Attach a Pipecat bot to a leg description: Bridges audio bidirectionally with a self-hosted Pipecat bot via WebSocket. Standalone legs use direct audio; legs in a room use mixer taps. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PipecatAgentRequest' responses: '200': description: Agent started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or missing websocket_url content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg not connected, agent already attached, or no audio reader/writer content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/agent/deepgram: parameters: - $ref: '#/components/parameters/LegId' post: operationId: agentLegDeepgram summary: Attach a Deepgram Voice Agent to a leg description: Bridges audio bidirectionally with a Deepgram Voice Agent. Standalone legs use direct audio; legs in a room use mixer taps. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DeepgramAgentRequest' responses: '200': description: Agent started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg not connected, agent already attached, or no audio reader/writer content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No Deepgram API key provided content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/agent/message: parameters: - $ref: '#/components/parameters/LegId' post: operationId: agentLegMessage summary: Inject a message into a running agent session on a leg description: Sends a context message or instruction to the running agent. Supported by Deepgram (InjectAgentMessage), Pipecat (TextFrame), and VAPI (control URL). Returns 501 for ElevenLabs. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AgentMessageRequest' responses: '200': description: Message sent content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or missing message content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: No agent attached to this leg content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Agent session not running content: application/json: schema: $ref: '#/components/schemas/Error' '501': description: Provider does not support message injection content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/agent: parameters: - $ref: '#/components/parameters/LegId' delete: operationId: stopAgentLeg summary: Detach the agent from a leg tags: - Legs responses: '200': description: Agent stopped content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No agent attached to this leg content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/amd: parameters: - $ref: '#/components/parameters/LegId' post: operationId: startAMDLeg summary: Start answering machine detection on a connected leg tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AMDParams' responses: '200': description: AMD started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid AMD params or not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg is not in connected state content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/ice-candidates: parameters: - $ref: '#/components/parameters/LegId' post: operationId: addICECandidate summary: Send a remote ICE candidate to a WebRTC leg (trickle ICE) tags: - WebRTC responses: '200': description: Candidate added content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or leg is not a WebRTC leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Failed to add ICE candidate content: application/json: schema: $ref: '#/components/schemas/Error' get: operationId: getICECandidates summary: Get server-side ICE candidates for a WebRTC leg (trickle ICE) tags: - WebRTC responses: '200': description: Buffered ICE candidates content: application/json: schema: $ref: '#/components/schemas/WebRTCCandidatesResult' '400': description: Leg is not a WebRTC leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /rooms: post: operationId: createRoom summary: Create a room tags: - Rooms requestBody: content: application/json: schema: $ref: '#/components/schemas/RoomCreateRequest' responses: '201': description: Room created content: application/json: schema: $ref: '#/components/schemas/Room' '409': description: Room ID already exists content: application/json: schema: $ref: '#/components/schemas/Error' get: operationId: listRooms summary: List all rooms with participants tags: - Rooms responses: '200': description: Array of rooms content: application/json: schema: type: array items: $ref: '#/components/schemas/Room' /rooms/{id}: parameters: - $ref: '#/components/parameters/RoomId' get: operationId: getRoom summary: Get a room with participants tags: - Rooms responses: '200': description: Room details content: application/json: schema: $ref: '#/components/schemas/Room' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: deleteRoom summary: Delete a room tags: - Rooms responses: '200': description: Room deleted content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/legs: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: addLegToRoom summary: Add or move a leg to a room description: Add a leg to a room (auto-creates room if it doesn't exist). If the leg is already in a different room, it is atomically moved to the target room. A ringing inbound SIP leg is automatically answered before being added — in this case the response status is `adding` and the actual room join happens asynchronously, observable via `leg.joined_room`. Auto-answer failures surface as `leg.command_failed` with `command="add_to_room"`. Only the leg's primary audio stream joins by default; `streams` additionally mixes named secondary streams of the same leg into this room. A stream currently mixed elsewhere is moved here. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AddLegRequest' responses: '200': description: Leg added or moved content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON, leg not found, or leg not connected content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/legs/{legID}: parameters: - $ref: '#/components/parameters/RoomId' - name: legID in: path required: true schema: type: string description: Leg ID delete: operationId: removeLegFromRoom summary: Remove a leg from a room tags: - Rooms responses: '200': description: Leg removed content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Room or leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/bridges: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: createRoomBridge summary: Bridge this room's mixer to another room's mixer description: 'Joins the room in the path with `room_id` so audio flows between their mixers. Both rooms must exist and share a sample rate. `direction` is relative to the room in the path: `bidirectional`, `send` (path room → other), `receive` (other → path room), or `none` (allocated but silent). Bridging rooms into a cycle with feedback-enabled directions causes audio feedback — use one-way directions to break loops.' tags: - Bridges requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateRoomBridgeRequest' responses: '201': description: Bridge created content: application/json: schema: $ref: '#/components/schemas/BridgeView' '400': description: Invalid JSON, self-bridge, sample-rate mismatch, or invalid direction content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Path room or room_id not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: A bridge between these rooms already exists content: application/json: schema: $ref: '#/components/schemas/Error' get: operationId: listRoomBridges summary: List bridges involving this room tags: - Bridges responses: '200': description: Array of bridges (direction relative to the path room) content: application/json: schema: type: array items: $ref: '#/components/schemas/BridgeView' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/bridges/{bridgeID}: parameters: - $ref: '#/components/parameters/RoomId' - name: bridgeID in: path required: true schema: type: string get: operationId: getRoomBridge summary: Get a bridge involving this room tags: - Bridges responses: '200': description: Bridge details (direction relative to the path room) content: application/json: schema: $ref: '#/components/schemas/BridgeView' '404': description: Bridge not found for this room content: application/json: schema: $ref: '#/components/schemas/Error' patch: operationId: updateRoomBridge summary: Change a bridge's audio flow direction description: Live-updates the direction (relative to the room in the path) without interrupting audio. tags: - Bridges requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateRoomBridgeRequest' responses: '200': description: Bridge updated content: application/json: schema: $ref: '#/components/schemas/BridgeView' '400': description: Invalid JSON or invalid/missing direction content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Bridge not found for this room content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: deleteRoomBridge summary: Tear down a bridge tags: - Bridges responses: '200': description: Bridge deleted content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Bridge not found for this room content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/routing: parameters: - $ref: '#/components/parameters/RoomId' get: operationId: getRoomRouting summary: Get the room's audio routing matrix description: Returns the per-listener-role source whitelist used by the room's audio mixer. A listener role absent from the matrix defaults to full mesh (hears every other leg). A role with an empty `[]` list is an isolated listener that hears nothing. tags: - Rooms responses: '200': description: Current matrix content: application/json: schema: $ref: '#/components/schemas/RoomRoutingView' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' put: operationId: setRoomRouting summary: Replace the room's audio routing matrix description: Atomically replaces the room's audio routing matrix and recomputes every leg's per-listener source whitelist in one mixer-mutex acquisition. The next mix tick (≤ 20 ms) reflects the new routing. Roles are operator-supplied strings (e.g. "customer", "agent", "supervisor"). A leg with no role defaults to full mesh. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RoomRoutingRequest' responses: '200': description: Updated matrix content: application/json: schema: $ref: '#/components/schemas/RoomRoutingView' '400': description: Invalid JSON content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' patch: operationId: updateRoomRouting summary: Replace selected rows of the room's audio routing matrix description: 'Replaces the listed listener-role rows. Pass `"sources": null` on an update to clear that row back to full mesh.' tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RoomRoutingUpdateRequest' responses: '200': description: Updated matrix content: application/json: schema: $ref: '#/components/schemas/RoomRoutingView' '400': description: Invalid JSON content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/siprec: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: startRoomSIPREC summary: Fork a room to an external SIPREC recording server description: Originates a SIPREC recording session (RFC 7866) to the given recording server, offering one `sendonly` `m=audio` section per room participant and carrying an RFC 7865 metadata document that names each party and binds it to a section's `a=label`. Each participant's own audio is forked to its own section — not the room mix. Returns the resulting `siprec_out` leg; delete that leg to end the session. Requires `SIPREC_SRC_ENABLED=true`. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/StartSIPRECRequest' responses: '201': description: Recording session established content: application/json: schema: $ref: '#/components/schemas/Leg' '400': description: Invalid JSON or srs_uri content: application/json: schema: $ref: '#/components/schemas/Error' '403': description: Outbound SIPREC is disabled content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Room has no participants, or more than SIPREC_MAX_STREAMS content: application/json: schema: $ref: '#/components/schemas/Error' '502': description: The recording server rejected the session content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/siprec: parameters: - $ref: '#/components/parameters/LegId' post: operationId: startLegSIPREC summary: Fork a single call to an external SIPREC recording server description: 'Originates a SIPREC recording session (RFC 7866) carrying one call as two `sendonly` sections: what the far end says, and what this server sends them. No room is involved. `leg_ids` is ignored here — the two sections are fixed. Returns the resulting `siprec_out` leg; delete it to end the session. Requires `SIPREC_SRC_ENABLED=true`.' tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/StartSIPRECRequest' responses: '201': description: Recording session established content: application/json: schema: $ref: '#/components/schemas/Leg' '400': description: Invalid JSON, invalid srs_uri, or not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '403': description: Outbound SIPREC is disabled content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: The leg is itself a recording session content: application/json: schema: $ref: '#/components/schemas/Error' '502': description: The recording server rejected the session content: application/json: schema: $ref: '#/components/schemas/Error' get: operationId: getSIPRECSession summary: Get a SIPREC recording session description: 'Returns the RFC 7865 recording metadata of an inbound SIPREC session (leg type `siprec_in`): every recorded participant, every negotiated media stream, and the binding between them. A stream''s `a=label` is what ties the m= section to a participant, so the `streams` entries carry both the leg stream ID and the participant identity. The raw metadata document is returned verbatim in `metadata`.' tags: - Legs responses: '200': description: Recording session state content: application/json: schema: $ref: '#/components/schemas/SIPRECSessionView' '400': description: Leg is not a SIPREC recording session content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg or recording session state not found content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/streams: parameters: - $ref: '#/components/parameters/LegId' get: operationId: listLegStreams summary: List a leg's audio streams description: Returns every negotiated m=audio section on the leg, in m-line order. A single-stream call has exactly one entry, the primary. tags: - Legs responses: '200': description: Audio streams in m-line order content: application/json: schema: type: array items: $ref: '#/components/schemas/LegStreamView' '400': description: Not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' post: operationId: addLegStream summary: Add an audio stream to a live call description: Negotiates an additional m=audio section with a re-INVITE (RFC 3264 §8.1). The new section is appended below the existing ones and binds its own RTP port. Use it to carry a second independent audio stream — a translated feed, for example — alongside the original. Set `content` to "alt" and `lang` to the feed's language so the peer can tell them apart. A peer that answers the new section with port 0 leaves the call untouched and this returns 409. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AddLegStreamRequest' responses: '201': description: Stream negotiated content: application/json: schema: $ref: '#/components/schemas/LegStreamView' '400': description: Invalid JSON or not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg or room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Leg has no negotiated media yet, or the peer rejected the stream content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/streams/{streamId}: parameters: - $ref: '#/components/parameters/LegId' - name: streamId in: path required: true schema: type: string get: operationId: getLegStream summary: Get one of a leg's audio streams tags: - Legs responses: '200': description: Audio stream content: application/json: schema: $ref: '#/components/schemas/LegStreamView' '400': description: Not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg or stream not found content: application/json: schema: $ref: '#/components/schemas/Error' patch: operationId: updateLegStream summary: Change an audio stream's routing role description: |- Updates the stream's role in place and, when the stream is mixed into a room, recomputes that room's matrix-derived allow-sets atomically (single mixer-mutex acquisition), so no audio bleeds through mid-change. Emits `leg.stream_role_changed` and `room.routing_changed` with `reason: leg_stream_role_changed`. Only the role is mutable here. The SDP-level attributes — direction, lang, content, label — are fixed when the stream is negotiated; change them by removing the stream and adding a new one. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateLegStreamRequest' responses: '200': description: Updated stream content: application/json: schema: $ref: '#/components/schemas/LegStreamView' '400': description: Invalid JSON, not a SIP leg, or the primary stream (its role follows the leg) content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg or stream not found content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: removeLegStream summary: Remove an audio stream from a live call description: Disables the stream with a re-INVITE carrying port 0 for its section (RFC 3264 §8.2) and releases its RTP port. The m-line slot survives as a tombstone — the m-line count never decreases for the life of a dialog — so a later added stream takes a new position. The primary stream carries the call and cannot be removed. tags: - Legs responses: '204': description: Stream removed '400': description: Not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg or stream not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Cannot remove the primary stream, or the re-INVITE failed content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/streams/{streamId}/room: parameters: - $ref: '#/components/parameters/LegId' - name: streamId in: path required: true schema: type: string post: operationId: attachLegStreamRoom summary: Mix an audio stream into a room description: Attaches one of the leg's secondary streams to a room, which may differ from the leg's own room — that is what lets an original audio stream and a translated one be mixed separately. A leg never hears its own other streams, whatever the routing matrix says. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AttachStreamRoomRequest' responses: '200': description: Updated stream content: application/json: schema: $ref: '#/components/schemas/LegStreamView' '400': description: Invalid JSON, missing room_id, not a SIP leg, or the primary stream content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg, stream, or room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Stream carries no audio in either direction content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: detachLegStreamRoom summary: Remove an audio stream from its room tags: - Legs responses: '200': description: Updated stream content: application/json: schema: $ref: '#/components/schemas/LegStreamView' '400': description: Not a SIP leg content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg or stream not found content: application/json: schema: $ref: '#/components/schemas/Error' /legs/{id}/role: parameters: - $ref: '#/components/parameters/LegId' patch: operationId: setLegRole summary: Change a leg's routing role description: Updates the leg's routing role and, if the leg is currently in a room, recomputes the room's matrix-derived allow-sets atomically (single mixer-mutex acquisition). The next mix tick (≤ 20 ms) reflects the change. tags: - Legs requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SetLegRoleRequest' responses: '200': description: Updated leg view content: application/json: schema: $ref: '#/components/schemas/Leg' '400': description: Invalid JSON content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/play: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: playRoom summary: Play audio to a room tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PlaybackRequest' responses: '200': description: Playback started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or volume out of range content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Room has no participants content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/play/{playbackID}: parameters: - $ref: '#/components/parameters/RoomId' - $ref: '#/components/parameters/PlaybackId' patch: operationId: volumePlayRoom summary: Change the volume of an active room playback tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VolumeRequest' responses: '200': description: Volume updated content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or volume out of range content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Playback not found content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: stopPlayRoom summary: Stop room playback tags: - Rooms responses: '200': description: Playback stopped content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No playback in progress content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/tts: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: ttsRoom summary: Synthesize speech and play it into a room description: Synthesizes the provided text using the configured TTS provider and plays the audio into the room. When `TTS_CACHE_ENABLED=true`, identical requests (same text, voice, model, language, and prompt) are stored on disk in `TTS_CACHE_DIR` and persist across restarts, without calling the external provider. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TTSRequest' responses: '200': description: TTS playback started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON, missing text/voice, or volume out of range content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Room has no participants content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No API key provided for the selected provider content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/record: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: recordRoom summary: Start recording the room mix to a WAV file description: Records the full room mix at 16kHz, 16-bit, mono. tags: - Rooms requestBody: content: application/json: schema: $ref: '#/components/schemas/RecordingRequest' responses: '200': description: Recording started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid storage type, S3 not configured, or invalid S3 credentials content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Room has no participants content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Failed to create recording file content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: stopRecordRoom summary: Stop room recording tags: - Rooms responses: '200': description: Recording stopped content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No recording in progress content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/record/pause: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: pauseRecordRoom summary: Pause a room recording description: 'Replaces the room mix with silence on the active recording until `/record/resume` is called. When multi-channel recording is active, every per-participant track is paused too (including tracks for participants who join while paused). Idempotent: returns `status: already_paused` when already paused.' tags: - Rooms responses: '200': description: Recording paused (or already paused) content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No recording in progress content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/record/resume: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: resumeRecordRoom summary: Resume a paused room recording description: Resumes writing real audio after a prior `/record/pause`. Resumes every per-participant track if multi-channel recording is active. Idempotent. tags: - Rooms responses: '200': description: Recording resumed (or was not paused) content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No recording in progress content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/stt: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: sttRoom summary: Start speech-to-text on all room participants tags: - Rooms requestBody: content: application/json: schema: $ref: '#/components/schemas/STTRequest' responses: '200': description: STT started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: STT already running or room has no participants content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No ElevenLabs API key provided content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: stopSTTRoom summary: Stop speech-to-text on a room tags: - Rooms responses: '200': description: STT stopped content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No STT in progress content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/agent/elevenlabs: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: agentRoomElevenLabs summary: Attach an ElevenLabs ConvAI agent to a room description: The agent joins as a virtual participant, hearing all participants (mixed-minus-self) and speaking to everyone. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ElevenLabsAgentRequest' responses: '200': description: Agent started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or missing agent_id content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Agent already attached to this room content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No ElevenLabs API key provided content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/agent/vapi: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: agentRoomVAPI summary: Attach a VAPI agent to a room description: The agent joins as a virtual participant, hearing all participants (mixed-minus-self) and speaking to everyone. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VAPIAgentRequest' responses: '200': description: Agent started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or missing assistant_id content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Agent already attached to this room content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No VAPI API key provided content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/agent/pipecat: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: agentRoomPipecat summary: Attach a Pipecat bot to a room description: The bot joins as a virtual participant via WebSocket, hearing all participants (mixed-minus-self) and speaking to everyone. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PipecatAgentRequest' responses: '200': description: Agent started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or missing websocket_url content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Agent already attached to this room content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/agent/deepgram: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: agentRoomDeepgram summary: Attach a Deepgram Voice Agent to a room description: The agent joins as a virtual participant, hearing all participants (mixed-minus-self) and speaking to everyone. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DeepgramAgentRequest' responses: '200': description: Agent started content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Agent already attached to this room content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: No Deepgram API key provided content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/agent/message: parameters: - $ref: '#/components/parameters/RoomId' post: operationId: agentRoomMessage summary: Inject a message into a running agent session on a room description: Sends a context message or instruction to the running agent. Supported by Deepgram (InjectAgentMessage), Pipecat (TextFrame), and VAPI (control URL). Returns 501 for ElevenLabs. tags: - Rooms requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AgentMessageRequest' responses: '200': description: Message sent content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Invalid JSON or missing message content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: No agent attached to this room content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: Agent session not running content: application/json: schema: $ref: '#/components/schemas/Error' '501': description: Provider does not support message injection content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/agent: parameters: - $ref: '#/components/parameters/RoomId' delete: operationId: stopAgentRoom summary: Detach the agent from a room tags: - Rooms responses: '200': description: Agent stopped content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '404': description: No agent attached to this room content: application/json: schema: $ref: '#/components/schemas/Error' /rooms/{id}/ws: parameters: - $ref: '#/components/parameters/RoomId' get: operationId: wsRoom summary: WebSocket audio stream for a room description: Upgrades to a WebSocket connection and joins the room as a bidirectional audio participant. The client sends and receives 16kHz 16-bit signed little-endian PCM audio (mono), base64-encoded in JSON text frames. Each audio frame is 640 bytes (20ms). tags: - Rooms responses: '101': description: WebSocket upgrade successful. Server sends a `connected` message followed by mixed-minus-self audio frames. '404': description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' /sip/registrations: get: operationId: listSIPRegistrations summary: List active SIP AOR registrations description: Returns every currently bound AOR contact, with the UDP/TCP/TLS socket on which the binding's REGISTER arrived. Use this list to know which AORs are dialable via the `to` field of POST /v1/legs. tags: - SIP Registrations responses: '200': description: List of bindings content: application/json: schema: $ref: '#/components/schemas/RegistrationsResponse' /sip/registrations/{aor}: parameters: - name: aor in: path required: true schema: type: string delete: operationId: deleteSIPRegistration summary: Force-unbind an AOR (or a single contact within it) description: The AOR must be URL-encoded in the path (for example, `sip:alice@vb.example` is encoded as `sip%3Aalice%40vb.example`). Without query parameters, every Contact under the AOR is removed; with `?contact=` only that single Contact is removed. tags: - SIP Registrations responses: '204': description: Binding(s) removed '400': description: Invalid AOR encoding content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: AOR (or specified contact) not found content: application/json: schema: $ref: '#/components/schemas/Error' /sip/registrations/attempts/{id}/challenge: parameters: - $ref: '#/components/parameters/LegId' post: operationId: challengeSIPRegistrationAttempt summary: Challenge a parked inbound REGISTER with a 401 digest auth request description: Targets an inbound REGISTER surfaced via the `sip.registration_attempt` event (by its `attempt_id`). Sends a SIP 401 with a `WWW-Authenticate` digest challenge; the UA's credentialed re-REGISTER is verified against the supplied credential and, on success, bound and answered with 200 OK. Provide either `password` or `ha1`. The attempt must still be parked (it auto-accepts after the consult timeout). tags: - SIP Registrations requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ChallengeRequest' responses: '202': description: 401 challenge queued '400': description: Missing realm/credential content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Registration attempt not found or already decided content: application/json: schema: $ref: '#/components/schemas/Error' /sip/registrations/attempts/{id}/accept: parameters: - $ref: '#/components/parameters/LegId' post: operationId: acceptSIPRegistrationAttempt summary: Accept a parked inbound REGISTER attempt description: Binds the AOR and replies 200 OK for an inbound REGISTER surfaced via the `sip.registration_attempt` event (by its `attempt_id`). Equivalent to letting the consult timeout elapse, but immediate. An optional `max_expires` caps the granted binding TTL. tags: - SIP Registrations requestBody: content: application/json: schema: $ref: '#/components/schemas/RegistrationAcceptRequest' responses: '202': description: Accept queued '400': description: Invalid max_expires content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Registration attempt not found or already decided content: application/json: schema: $ref: '#/components/schemas/Error' /sip/registrations/attempts/{id}/reject: parameters: - $ref: '#/components/parameters/LegId' post: operationId: rejectSIPRegistrationAttempt summary: Reject a parked inbound REGISTER attempt description: Replies with a non-2xx (403 Forbidden by default; override via `code`/`reason`) for an inbound REGISTER surfaced via the `sip.registration_attempt` event (by its `attempt_id`). tags: - SIP Registrations requestBody: content: application/json: schema: $ref: '#/components/schemas/RegistrationRejectRequest' responses: '202': description: Reject queued '404': description: Registration attempt not found or already decided content: application/json: schema: $ref: '#/components/schemas/Error' /sip/trunks: post: operationId: createSIPTrunk summary: Create an outbound SIP trunk (REGISTER or static peering) description: 'Creates a typed SIP trunk. For `type: "sip_register"`, VoiceBlender begins REGISTERing to the supplied registrar URI with digest auth, refreshes before expiry, and routes inbound INVITEs that arrive on that peer''s socket plus outbound INVITEs whose `from` matches the AOR through the trunk. For `type: "ip_ip"`, returns 501 (reserved, not yet implemented).' tags: - SIP Trunks requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateTrunkRequest' responses: '202': description: Trunk accepted; REGISTER runs asynchronously content: application/json: schema: $ref: '#/components/schemas/CreateTrunkResponse' '400': description: Invalid JSON, missing required field, or unknown type content: application/json: schema: $ref: '#/components/schemas/Error' '501': description: Trunk type reserved but not yet implemented (e.g. ip_ip) content: application/json: schema: $ref: '#/components/schemas/Error' get: operationId: listSIPTrunks summary: List configured SIP trunks tags: - SIP Trunks responses: '200': description: List of trunks content: application/json: schema: $ref: '#/components/schemas/TrunksListResponse' /sip/trunks/{id}: parameters: - $ref: '#/components/parameters/LegId' get: operationId: getSIPTrunk summary: Get a single SIP trunk tags: - SIP Trunks responses: '200': description: Trunk view content: application/json: schema: $ref: '#/components/schemas/TrunkView' '404': description: Trunk not found content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: deleteSIPTrunk summary: Unregister and remove a SIP trunk description: 'Returns 202 Accepted; the unregister (REGISTER with Expires: 0) and final removal run asynchronously.' tags: - SIP Trunks responses: '202': description: Trunk accepted for teardown '404': description: Trunk not found content: application/json: schema: $ref: '#/components/schemas/Error' /vsi: get: operationId: vsi summary: VoiceBlender Streaming Interface (VSI) description: Upgrades to a WebSocket connection and streams all events in real-time as JSON text frames. The JSON shape is identical to webhook payloads. The server sends a `{"type":"connected"}` message on connect, followed by events and periodic `{"type":"ping"}` keepalives. Clients may send `{"type":"pong"}` or `{"type":"stop"}` to close gracefully. Unknown message types receive an error response with the echoed `request_id` (reserved for future commands). tags: - Events responses: '101': description: WebSocket upgrade successful. Server sends events as JSON text frames. /webrtc/offer: post: operationId: webrtcOffer summary: Establish a WebRTC leg via SDP offer/answer tags: - WebRTC requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WebRTCOfferRequest' responses: '200': description: SDP answer with leg ID content: application/json: schema: $ref: '#/components/schemas/WebRTCOfferResult' '400': description: Invalid JSON or invalid SDP offer content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Peer connection, track creation, or answer generation failed content: application/json: schema: $ref: '#/components/schemas/Error' /metrics: get: operationId: getMetrics summary: Prometheus metrics description: | Returns Prometheus-format metrics (text/plain exposition format). Includes VoiceBlender-specific metrics and standard Go runtime metrics. tags: - Observability responses: '200': description: Prometheus text exposition format content: text/plain: schema: type: string /debug/pprof/: get: operationId: pprofIndex summary: pprof index description: | Index of available Go runtime profiles. Only available when built with `-tags pprof` (e.g. `go build -tags pprof ./...`). tags: - Observability responses: '200': description: HTML index page listing available profiles content: text/html: schema: type: string /debug/pprof/profile: get: operationId: pprofCPU summary: CPU profile description: | 30-second CPU profile (duration configurable via ?seconds= query param). Only available when built with `-tags pprof`. tags: - Observability parameters: - name: seconds in: query schema: type: integer default: 30 description: Profile duration in seconds responses: '200': description: pprof binary profile content: application/octet-stream: schema: type: string format: binary /debug/pprof/heap: get: operationId: pprofHeap summary: Heap memory profile description: | Heap memory snapshot. Only available when built with `-tags pprof`. tags: - Observability responses: '200': description: pprof binary profile content: application/octet-stream: schema: type: string format: binary /debug/pprof/goroutine: get: operationId: pprofGoroutine summary: Goroutine stack traces description: | All goroutine stack traces. Only available when built with `-tags pprof`. tags: - Observability responses: '200': description: pprof binary profile content: application/octet-stream: schema: type: string format: binary components: parameters: LegId: name: id in: path required: true schema: type: string description: Leg ID RoomId: name: id in: path required: true schema: type: string description: Room ID PlaybackId: name: playbackID in: path required: true schema: type: string description: Playback ID responses: LegNotFound: description: Leg not found content: application/json: schema: $ref: '#/components/schemas/Error' RoomNotFound: description: Room not found content: application/json: schema: $ref: '#/components/schemas/Error' schemas: Leg: type: object properties: instance_id: type: string description: Instance identifier id: type: string description: Unique leg identifier (UUID) type: type: string description: Leg type enum: - sip_inbound - sip_outbound - webrtc - whatsapp_in - whatsapp_out - websocket_in - websocket_out - moq_in - livekit_publish - livekit_participant state: type: string description: Leg state enum: - ringing - early_media - connected - held - hung_up room_id: type: string description: Room ID if the leg is in a room, empty otherwise muted: type: boolean description: Whether the leg is muted (cannot be heard by others) deaf: type: boolean description: Whether the leg is deaf (cannot hear others) accept_dtmf: type: boolean description: Whether the leg receives DTMF digits broadcast from other legs in the same room. Defaults to true. held: type: boolean description: Whether the call is on hold (SIP legs only) role: type: string description: Routing role used by the room's audio routing matrix (e.g. "customer", "agent", "supervisor"). Empty string means unroled (full mesh). app_id: type: string description: Application identifier for event stream filtering. sip_headers: type: object additionalProperties: type: string description: 'Deprecated: X-* headers from the inbound INVITE. Only present on sip_inbound legs. Use `headers` for new code; it carries the same map plus surfaces handshake headers for websocket legs.' headers: type: object additionalProperties: type: string description: Custom protocol headers exposed by the leg's transport — X-/P- headers from a SIP INVITE, the WebSocket upgrade request, or supplied at outbound dial time. required: - id - type - state - muted - deaf - accept_dtmf - held Room: type: object properties: instance_id: type: string description: Instance identifier id: type: string description: Room identifier app_id: type: string description: Application identifier for event stream filtering. sample_rate: type: integer description: Mixer sample rate in Hz (8000, 16000, or 48000). participants: type: array items: $ref: '#/components/schemas/Leg' description: Legs currently in this room required: - id - sample_rate - participants Error: type: object properties: instance_id: type: string description: Instance identifier error: type: string description: Error message required: - error StatusResponse: type: object properties: instance_id: type: string description: Instance identifier status: type: string required: - status AddLegRequest: type: object properties: leg_id: type: string description: ID of the leg to add mute: type: boolean description: If set, apply this mute state to the leg atomically before it joins the mixer (no race where un-muted audio enters the mix). Omit to leave current state untouched (useful when moving between rooms). deaf: type: boolean description: If set, apply this deaf state to the leg atomically before it joins the mixer. Omit to leave current state untouched. accept_dtmf: type: boolean description: If set, control whether this leg receives DTMF digits broadcast from other legs in the same room. Omit to leave current state untouched (default for new legs is true). role: type: string description: If set, apply this routing role to the leg atomically before it joins the mixer. The room's routing matrix (see PUT /v1/rooms/{id}/routing) decides which other legs this leg hears and is heard by based on roles. Pass "" to clear the role (full mesh). Omit to leave the current role untouched. streams: type: array items: $ref: '#/components/schemas/AddRoomStream' description: Additional audio streams of the leg to mix into this room, each with its own routing role. Omit to add only the leg's primary stream. A stream already mixed elsewhere is moved here. required: - leg_id AddLegStreamRequest: type: object properties: direction: type: string description: Media direction for the new stream, from this server's point of view. Defaults to sendrecv. enum: - sendrecv - sendonly - recvonly - inactive default: sendrecv lang: type: string description: BCP 47 language tag advertised as a=lang (RFC 8866), e.g. "es-ES" for a Spanish translation feed. content: type: string description: Value advertised as a=content (RFC 4796). Use "main" for original audio and "alt" for an alternative feed such as a translation. enum: - main - alt - speaker - slides - sl label: type: string description: Value advertised as a=label (RFC 4574), for correlating the stream with external metadata. room_id: type: string description: If set, attach the new stream to this room once it is negotiated. role: type: string description: Routing role to apply when room_id is set. AgentMessageRequest: type: object properties: message: type: string description: Context or instruction to inject into the running agent session required: - message AnswerLegRequest: type: object properties: speech_detection: type: boolean description: If true, emit speaking.started and speaking.stopped events for this leg. If false, suppress them. Omit to use the server default (SPEECH_DETECTION_ENABLED env var, default false). codec: type: string description: Explicit codec for the answer SDP. Must appear in the remote offer's offered_codecs list. Omit to use the server's default preference order. enum: - PCMU - PCMA - G722 - opus - AMR-WB - AMR-NB streams: type: array items: $ref: '#/components/schemas/AnswerLegStream' description: 'Rooms for the caller''s additional audio streams, applied once the answer is negotiated. Positional: entry i addresses the i-th accepted stream beyond the primary, in m-line order — the caller''s offer decides how many exist, so an entry with no matching stream is ignored. Use POST /v1/legs/{id}/streams/{streamId}/room to re-route a stream later.' AttachStreamRoomRequest: type: object properties: room_id: type: string description: Room to mix this stream into. May differ from the leg's own room. role: type: string description: Routing role for the stream inside that room. The room's routing matrix decides who hears it. required: - room_id ChallengeRequest: type: object properties: realm: type: string username: type: string password: type: string ha1: type: string algorithm: type: string qop: type: array items: type: string max_expires: type: integer required: - realm CreateLegRequest: type: object properties: type: type: string description: Leg type enum: - sip - whatsapp - websocket - livekit_room to: type: string description: Destination. For sip legs, a SIP URI (e.g. "sip:alice@example.com"). For whatsapp legs, an E.164 phone number (with or without '+'). uri: type: string description: Deprecated alias for `to` (sip legs only). Prefer `to`. from: type: string description: Caller ID. A bare user-part (e.g. "+15551234567", "alice") sets the user of the SIP From header. A full SIP URI (e.g. "sip:alice@pbx.example.com") sets both the user and the host; otherwise the host comes from the matched trunk's AOR realm, falling back to SIP_DOMAIN. privacy: type: string description: SIP Privacy header value (e.g. "id", "none") ring_timeout: type: integer description: Seconds to wait for answer; 0 = no timeout default: 0 max_duration: type: integer description: Maximum call duration in seconds after connect. Automatically hung up when reached. 0 or omitted = no limit. default: 0 codecs: type: array items: type: string enum: - PCMU - PCMA - G722 - opus - AMR-WB - AMR-NB description: Codec preference order (sip legs only) headers: type: object additionalProperties: type: string description: Custom headers to include in the outbound INVITE (sip/whatsapp) or the WebSocket upgrade request (websocket) room_id: type: string description: Room ID to auto-add the leg to once media is ready (early_media or connected). If the room does not exist, it is automatically created. auth: description: Digest auth credentials. Required for whatsapp legs (Meta-issued password; username defaults to `from` with '+' stripped). Optional for sip legs (sipgo retries on 401/407 challenge). nullable: true allOf: - $ref: '#/components/schemas/SIPAuth' webhook_url: type: string description: Route all events for this leg exclusively to this URL instead of global webhooks. format: uri webhook_secret: type: string description: HMAC-SHA256 signing secret for the per-leg webhook. amd: description: Enable Answering Machine Detection on outbound calls. Include the object (even empty) to enable with defaults; omit to disable. nullable: true allOf: - $ref: '#/components/schemas/AMDParams' accept_dtmf: type: boolean description: If false, this leg will not receive DTMF digits broadcast from other legs in the same room. Defaults to true. default: true app_id: type: string description: Application identifier. Carried through to all events for this leg. Use to filter the WebSocket event stream by app. speech_detection: type: boolean description: If true, emit speaking.started and speaking.stopped events for this leg. If false, suppress them. Omit to use the server default (SPEECH_DETECTION_ENABLED env var, default false). rtt: type: boolean description: 'For sip legs: offer Real-Time Text (ITU-T T.140 over RTP per RFC 4103) alongside audio. For websocket legs: enable the bidirectional text-message channel. Default: false.' default: false streams: type: array items: $ref: '#/components/schemas/CreateLegStream' description: SIP outbound only. Extra m=audio sections to offer alongside the call's primary bidirectional audio, so a multi-stream call is established by the first INVITE instead of a follow-up re-INVITE. Each entry binds its own RTP port and may be mixed into its own room. To add a stream to a call that is already up, use POST /v1/legs/{id}/streams instead. url: type: string description: WebSocket target URL (ws:// or wss://) for outbound websocket legs. Required when type=websocket. format: uri sample_rate: type: integer description: PCM sample rate for websocket legs. The room's mixer automatically resamples between this and the room rate. enum: - "8000" - "16000" - "24000" - "48000" default: 16000 wire_format: type: string description: Audio framing for websocket legs. `binary` ships raw PCM as WebSocket binary frames; `json_base64` wraps PCM as `{"type":"audio","audio":""}` text frames (browser-friendly). enum: - binary - json_base64 default: binary sample_format: type: string description: On-the-wire PCM sample encoding for websocket legs. v1 only supports `s16le`. enum: - s16le default: s16le livekit: description: LiveKit room join parameters (only used when type=livekit_room). nullable: true allOf: - $ref: '#/components/schemas/LiveKitParams' required: - type CreateRoomBridgeRequest: type: object properties: id: type: string description: Custom bridge ID (auto-generated UUID if omitted) room_id: type: string description: The other room to join. Must use the same sample rate as the room in the path. direction: type: string description: 'Audio flow relative to the room in the path: bidirectional (both hear each other), send (path room → other only), receive (other → path room only), none (allocated but silent). Default: bidirectional.' enum: - bidirectional - send - receive - none default: bidirectional required: - room_id RoomCreateRequest: type: object properties: id: type: string description: Custom room ID (auto-generated UUID if omitted) webhook_url: type: string description: Route all events for this room exclusively to this URL instead of global webhooks. format: uri webhook_secret: type: string description: HMAC-SHA256 signing secret for the per-room webhook. app_id: type: string description: Application identifier. Carried through to all events for this room. Use to filter the WebSocket event stream by app. sample_rate: type: integer description: 'Mixer sample rate in Hz. Allowed values: 8000, 16000, 48000. Default: 16000.' enum: - "8000" - "16000" - "48000" default: 16000 required: - id CreateTrunkRequest: type: object properties: type: type: string description: Trunk type discriminator. Only `sip_register` is implemented today; `ip_ip` is reserved and returns 501. enum: - sip_register - ip_ip app_id: type: string description: Application identifier carried through to every event emitted by this trunk. sip_register: description: Required when type == "sip_register". Configures the outbound REGISTER (registrar URI, AOR, digest credentials, expiry). nullable: true allOf: - $ref: '#/components/schemas/SIPRegisterTrunkSpec' ip_ip: description: Reserved for static-IP peering (no REGISTER). Not yet implemented; supplying this returns 501. nullable: true allOf: - $ref: '#/components/schemas/IPIPTrunkSpec' required: - type DTMFRequest: type: object properties: digits: type: string description: 'DTMF digits to send (0-9, *, #)' required: - digits DeepgramAgentRequest: type: object properties: settings: type: object additionalProperties: type: string description: Full Deepgram agent settings object (agent.listen, agent.think, agent.speak, etc.). When omitted, sensible defaults are used (nova-3 STT, gpt-4o-mini LLM, aura-2-asteria-en TTS). greeting: type: string description: Agent greeting message language: type: string description: Language code (e.g. "en", "es") api_key: type: string description: API key override (falls back to DEEPGRAM_API_KEY env var) DeleteLegRequest: type: object properties: reason: type: string description: 'Disconnect reason. Only honored for unanswered SIP inbound legs (state `ringing` or `early_media`); on connected legs the body is ignored and the leg is hung up with the legacy `api_hangup` reason. The value flows through to `leg.disconnected`''s `cdr.reason` and selects the SIP final response: `busy`→486, `declined`/`rejected`→603, `unavailable`→480, `not_found`→404, `forbidden`→403, `server_error`→500.' enum: - busy - declined - rejected - unavailable - not_found - forbidden - server_error EarlyMediaLegRequest: type: object properties: codec: type: string description: Explicit codec for the 183 Session Progress SDP. Must appear in the remote offer's offered_codecs list. Omit to use the server's default preference order. enum: - PCMU - PCMA - G722 - opus - AMR-WB - AMR-NB ElevenLabsAgentRequest: type: object properties: agent_id: type: string description: ElevenLabs agent ID first_message: type: string description: Override the agent's first message language: type: string description: Language code (e.g. "en", "es") dynamic_variables: type: object additionalProperties: type: string description: Key-value pairs passed to the agent as dynamic variables api_key: type: string description: API key override (falls back to ELEVENLABS_API_KEY env var) required: - agent_id PipecatAgentRequest: type: object properties: websocket_url: type: string description: WebSocket URL of the Pipecat bot (e.g. ws://my-bot:8765) format: uri required: - websocket_url PlaybackRequest: type: object properties: url: type: string description: URL of the audio file (mutually exclusive with tone) format: uri tone: type: string description: 'Built-in telephone tone name. Format: {country}_{type} or bare {type} (defaults to US). Types: ringback, busy, dial, congestion. Countries: us, gb, de, fr, au, jp, it, in, br, pl, ru. Examples: us_ringback, gb_busy, dial.' mime_type: type: string description: MIME type (e.g. audio/wav). Required when using url. repeat: type: integer description: Number of times to repeat playback (url only) default: 0 volume: type: integer description: Volume adjustment in dB (-8 to 8) default: 0 minimum: -8 maximum: 8 required: - url - tone - mime_type - repeat - volume oneOf: - required: - url - mime_type - required: - tone RTTRequest: type: object properties: text: type: string description: UTF-8 text to send. May be one or more characters and may include T.140 control codes (e.g. backspace U+0008, CR/LF). required: - text RecordingRequest: type: object properties: storage: type: string description: '"file" (default) — local disk, "s3" — upload to S3 after recording stops, "gcs" — upload to Google Cloud Storage via the native GCS API (Application Default Credentials / Workload Identity)' enum: - file - s3 - gcs multi_channel: type: boolean description: When true, record each participant to a separate mono WAV file in addition to the full mix. Only applies to room recordings. default: false s3_bucket: type: string description: S3 bucket name. Overrides S3_BUCKET env var. Required if env var is not set. s3_region: type: string description: AWS region. Overrides S3_REGION env var. Default us-east-1. s3_endpoint: type: string description: Custom S3 endpoint (MinIO, etc.). Overrides S3_ENDPOINT env var. s3_prefix: type: string description: Key prefix (e.g. recordings/). Overrides S3_PREFIX env var. s3_access_key: type: string description: AWS access key ID. Overrides default credential chain. s3_secret_key: type: string description: AWS secret access key. Must be set together with s3_access_key. gcs_bucket: type: string description: GCS bucket name. Overrides GCS_BUCKET env var. Required if env var is not set when storage=gcs. gcs_object_name_prefix: type: string description: Object name prefix (e.g. recordings or recordings/). Overrides GCS_OBJECT_NAME_PREFIX env var. A trailing slash is added automatically when missing. filename: type: string description: Optional output basename for the WAV file. A .wav suffix is added when missing. Must be a single path segment (no directories). Dots inside the name are preserved (only a trailing .wav is treated as the extension). Rejected with 409 if the file already exists or another recording is using the same name. When omitted, a timestamped name is generated. required: - storage - multi_channel - s3_bucket - s3_region - s3_endpoint - s3_prefix - s3_access_key - s3_secret_key - gcs_bucket - gcs_object_name_prefix - filename RegistrationAcceptRequest: type: object properties: max_expires: type: integer RegistrationRejectRequest: type: object properties: code: type: integer reason: type: string RoomRoutingRequest: type: object properties: matrix: type: object additionalProperties: type: array items: type: string description: Listener-role → list of allowed source roles. Omitted listener roles default to full mesh. Empty list = hears nothing. required: - matrix RoomRoutingUpdateRequest: type: object properties: updates: type: array items: $ref: '#/components/schemas/RoutingRowUpdate' description: Per-listener-role row replacements applied as a single atomic update. required: - updates STTRequest: type: object properties: language: type: string description: Language code (e.g. "en", "es") partial: type: boolean description: Emit partial (non-final) transcripts default: false provider: type: string description: 'STT provider: "elevenlabs" (default) or "deepgram"' enum: - elevenlabs - deepgram api_key: type: string description: API key override (falls back to ELEVENLABS_API_KEY or DEEPGRAM_API_KEY env var depending on provider) required: - language - partial SetLegRoleRequest: type: object properties: role: type: string description: New routing role for the leg. The room's routing matrix decides which other legs this leg hears and is heard by based on roles. Pass an empty string to clear the role (full mesh). required: - role StartSIPRECRequest: type: object properties: srs_uri: type: string description: SIP URI of the session recording server, e.g. "sip:srs@recorder.example.com:5060". A recording session carries the metadata document alongside the SDP and exceeds the UDP message limit, so the target should accept TCP. leg_ids: type: array items: type: string description: Which participants to record. Each entry is either a leg ID (that leg's own audio) or "#" for one of a leg's secondary audio streams mixed into the room. Empty or absent records every participant. An entry that is not in the room is a 404. session_id: type: string description: Communication session identifier put in the recording metadata. Defaults to the room ID. app_id: type: string description: Application identifier tagged onto the resulting leg and its events. auth_username: type: string description: SIP digest username, when the recording server challenges the INVITE. auth_password: type: string description: SIP digest password, when the recording server challenges the INVITE. headers: type: object additionalProperties: type: string description: 'Extra SIP headers to include in the INVITE. Require: siprec is always sent.' required: - srs_uri TTSRequest: type: object properties: text: type: string description: Text to synthesize voice: type: string description: 'Provider-specific voice identifier. ElevenLabs: voice name or ID. AWS Polly: voice ID (e.g. Joanna, Matthew). Google Cloud: voice name — either full format (e.g. en-US-Neural2-F) or short name for Gemini models (e.g. Achernar, Kore). Deepgram: model name (e.g. aura-2-asteria-en).' model_id: type: string description: 'Provider-specific model/engine. ElevenLabs: model ID. AWS Polly: engine (standard, neural, long-form, generative; default neural). Google Cloud: model name (e.g. gemini-2.5-pro-tts, chirp3-hd).' language: type: string description: Language code (e.g. "en-US", "pl-pl"). Required for Google Gemini TTS voices that use short names (e.g. Achernar). Auto-extracted from full voice names like en-US-Neural2-F. prompt: type: string description: Style/tone instruction for promptable voice models (Google Gemini TTS only). E.g. "Read aloud in a warm, welcoming tone." volume: type: integer description: Volume adjustment in dB (-8 to 8) default: 0 minimum: -8 maximum: 8 provider: type: string description: 'TTS provider: "elevenlabs" (default), "aws", "google", or "deepgram"' enum: - elevenlabs - aws - google - deepgram api_key: type: string description: 'ElevenLabs: API key override (falls back to ELEVENLABS_API_KEY env var). AWS: optional ACCESS_KEY:SECRET_KEY override (falls back to default AWS credential chain). Google Cloud: optional API key override (falls back to Application Default Credentials). Deepgram: API key override (falls back to DEEPGRAM_API_KEY env var).' required: - text - voice - model_id - volume TransferCompleteRequest: type: object properties: success: type: boolean status_code: type: integer reason: type: string required: - success TransferDeclineRequest: type: object properties: code: type: integer reason: type: string TransferProgressRequest: type: object properties: status_code: type: integer reason: type: string required: - status_code TransferRequest: type: object properties: target: type: string description: SIP URI to transfer the call to (e.g. "sip:bob@example.com"). replaces_leg_id: type: string description: ID of an existing connected SIP leg whose dialog should be replaced (attended transfer). Omit for blind transfer. required: - target UpdateLegStreamRequest: type: object properties: role: type: string description: New routing role for this stream inside its room. The room's routing matrix decides who hears it. Pass an empty string to clear the role (full mesh). Omit to leave it untouched. Applied atomically — the room's allow-sets are recomputed in a single mixer-mutex acquisition, so no audio bleeds through mid-change. UpdateRoomBridgeRequest: type: object properties: direction: type: string description: 'New audio flow relative to the room in the path: bidirectional, send, receive, or none.' enum: - bidirectional - send - receive - none required: - direction VAPIAgentRequest: type: object properties: assistant_id: type: string description: VAPI assistant ID first_message: type: string description: Override the agent's first message variable_values: type: object additionalProperties: type: string description: Key-value pairs passed as VAPI variable values (assistantOverrides.variableValues) api_key: type: string description: API key override (falls back to VAPI_API_KEY env var) required: - assistant_id VolumeRequest: type: object properties: volume: type: integer description: Volume adjustment (-8 to 8, ~3dB per step, 0 = unchanged) minimum: -8 maximum: 8 required: - volume WebRTCOfferRequest: type: object properties: sdp: type: string description: SDP offer from the browser app_id: type: string description: Application identifier. Carried through to all events emitted for this leg, and matched against the VSI `app_id` filter. required: - sdp AMDParams: type: object properties: initial_silence_timeout: type: integer description: Max milliseconds of silence before declaring no_speech default: 2500 greeting_duration: type: integer description: Speech duration threshold (ms) above which answerer is classified as machine default: 1500 after_greeting_silence: type: integer description: Silence duration (ms) after initial speech to declare human default: 800 total_analysis_time: type: integer description: Max analysis window in milliseconds. A threshold longer than this window suppresses that verdict; a window shorter than all of initial_silence_timeout, greeting_duration and after_greeting_silence is rejected, since the call could only end not_sure. default: 5000 minimum_word_length: type: integer description: Minimum speech burst duration (ms) to count as a word default: 100 beep_timeout: type: integer description: Max time (ms) to wait for the voicemail beep after machine detection. 0 or omitted = disabled. default: 0 AddRoomStream: type: object properties: stream_id: type: string description: Stream identifier from GET /v1/legs/{id}/streams. The primary stream is not addressable here — it joins with the leg itself. role: type: string description: Routing role for this stream inside the room. required: - stream_id AnswerLegStream: type: object properties: room_id: type: string description: Room to mix this stream into once the answer is negotiated. May differ from the room the leg itself joins. role: type: string description: Routing role for this stream inside its room. BridgeView: type: object properties: id: type: string description: Bridge identifier room_id: type: string description: The peer room joined to the room in the path direction: type: string description: 'Audio flow relative to the room in the path: bidirectional, send, receive, or none.' enum: - bidirectional - send - receive - none sample_rate: type: integer description: Shared mixer sample rate in Hz (both rooms must match). required: - id - room_id - direction - sample_rate CreateLegStream: type: object properties: direction: type: string description: Media direction for this stream, from this server's point of view. Defaults to sendrecv. enum: - sendrecv - sendonly - recvonly - inactive default: sendrecv lang: type: string description: BCP 47 language tag advertised as a=lang (RFC 8866), e.g. "es-ES" for a Spanish translation feed. content: type: string description: Value advertised as a=content (RFC 4796). Use "alt" for an alternative feed such as a translation. enum: - main - alt - speaker - slides - sl label: type: string description: Value advertised as a=label (RFC 4574), for correlating the stream with external metadata. room_id: type: string description: Room to mix this stream into once the call connects. May differ from the leg's own room_id, which governs the primary stream. role: type: string description: Routing role for this stream inside its room. CreateTrunkResponse: type: object properties: id: type: string type: type: string status: type: string required: - id - type - status IPIPTrunkSpec: type: object properties: peer_uri: type: string description: Static peer SIP URI for IP-IP peering. Reserved; not yet implemented. IPIPTrunkView: type: object properties: peer_uri: type: string LegStreamView: type: object properties: id: type: string description: Stream identifier, stable for the life of the dialog. The primary stream is always "0". mid: type: string description: The stream's SDP a=mid token (RFC 5888), used to correlate it across offer/answer. index: type: integer description: Position of the stream's m= line in the SDP. Fixed for the life of the dialog (RFC 3264 §8). primary: type: boolean description: True for the call's main bidirectional audio stream, which cannot be removed or attached to a room independently. state: type: string description: Negotiation state. enum: - pending - active - removed direction: type: string description: Negotiated media direction from this server's point of view. enum: - sendrecv - sendonly - recvonly - inactive desired_direction: type: string description: Direction requested by the application. Survives hold/unhold, unlike the negotiated direction. enum: - sendrecv - sendonly - recvonly - inactive codec: type: string description: Codec negotiated for this stream. Streams on one leg may use different codecs. sample_rate: type: integer description: Native sample rate of the stream's codec, in Hz. local_port: type: integer description: Local RTP port. Each stream binds its own port; a shared transport is undefined without BUNDLE (RFC 9143). remote_addr: type: string description: Remote RTP address media is currently sent to. label: type: string description: The stream's a=label value (RFC 4574), for correlating it with external metadata. content: type: string description: The stream's a=content value (RFC 4796), e.g. "main" for original audio and "alt" for a translated feed. lang: type: string description: 'The stream''s a=lang value (RFC 8866): a BCP 47 language tag such as "en" or "es-ES".' room_id: type: string description: Room this stream's audio is mixed into. A secondary stream may sit in a different room than its leg. role: type: string description: Routing role of this stream within its room. Streams carry their own role, independent of their leg's. required: - id - index - primary - state - direction LiveKitParams: type: object properties: url: type: string description: LiveKit server endpoint (wss://...). Overrides LIVEKIT_URL. format: uri token: type: string description: Pre-signed LiveKit JWT. Mutually exclusive with `room`/`identity` (mint mode); if both are present the token wins. room: type: string description: LiveKit room name. Required when minting (i.e. `token` is empty AND LIVEKIT_TOKEN_SIGNING_ENABLED=true). identity: type: string description: LiveKit participant identity. Required when minting. participant_name: type: string description: Display name for the participant; surfaces in LK Room UIs. permissions: description: LiveKit grant flags. Nil pointers default to publish=true, subscribe=true, data=false, admin=false. nullable: true allOf: - $ref: '#/components/schemas/LiveKitPermissions' token_ttl: type: string description: Go duration string (e.g. "30m", "6h"). Used only when minting. Defaults to LIVEKIT_DEFAULT_TOKEN_TTL (6h). opus_bitrate: type: integer description: Override LIVEKIT_OPUS_BITRATE for this leg. 6000..510000. minimum: 6000 maximum: 510000 LiveKitPermissions: type: object properties: can_publish: type: boolean description: Allow publishing tracks. Default true. can_subscribe: type: boolean description: Allow subscribing to remote tracks. Default true. can_publish_data: type: boolean description: Allow publishing data channel messages. Default false (audio bridge does not use data). room_admin: type: boolean description: Grant admin actions on the room (e.g., server-side MuteTrack of remote participants). Default false. RegistrationView: type: object properties: aor: type: string contact: type: string socket: type: string transport: type: string user_agent: type: string call_id: type: string app_id: type: string created_at: type: string last_refresh: type: string expires_at: type: string granted_expires_seconds: type: integer required: - aor - contact - socket - transport - created_at - last_refresh - expires_at - granted_expires_seconds RegistrationsResponse: type: object properties: bindings: type: array items: $ref: '#/components/schemas/RegistrationView' required: - bindings RoomRoutingView: type: object properties: matrix: type: object additionalProperties: type: array items: type: string description: Listener-role → list of allowed source roles. Roles absent from the matrix default to full mesh. required: - matrix RoutingRowUpdate: type: object properties: listener_role: type: string description: The role whose row is being replaced. sources: type: array items: type: string description: New list of allowed source roles for this listener role. Pass null to clear the row (full mesh). required: - listener_role - sources SIPAuth: type: object properties: username: type: string description: Digest auth username. Optional for whatsapp legs (defaults to `from` with '+' stripped, per Meta's spec). password: type: string description: Digest auth password. required: - password SIPRECParticipantView: type: object properties: participant_id: type: string description: The participant_id attribute from the recording metadata document. aor: type: string description: The participant's address of record, e.g. "sip:alice@example.com". name: type: string description: The participant's display name, when the metadata carries one. required: - participant_id SIPRECSessionView: type: object properties: leg_id: type: string description: Leg carrying the recording session. session_id: type: string description: Communication session being recorded, from the metadata's sessionrecordingassoc. data_mode: type: string description: Data mode of the most recently applied metadata document (RFC 7865 §6.1). enum: - complete - partial room_id: type: string description: Room this session's streams were attached to, when SIPREC_ROOM_MODE placed them in one. participants: type: array items: $ref: '#/components/schemas/SIPRECParticipantView' description: Every party currently recorded by this session. streams: type: array items: $ref: '#/components/schemas/SIPRECStreamView' description: Every negotiated media stream, joined to the participant it carries. metadata: type: string description: The raw rs-metadata XML document as most recently received. required: - leg_id - participants - streams SIPRECStreamView: type: object properties: leg_stream_id: type: string description: Identifier of the leg stream carrying this recorded media, as used by /v1/legs/{id}/streams. mid: type: string description: The stream's SDP a=mid token (RFC 5888). label: type: string description: The stream's a=label value (RFC 4574). This is the key that binds the m= section to the recording metadata. direction: type: string description: 'Negotiated media direction. Always recvonly or inactive: a recording server never transmits.' enum: - recvonly - inactive codec: type: string description: Codec negotiated for this stream. room_id: type: string description: Room this stream's audio is mixed into, when it has been attached to one. role: type: string description: Routing role of this stream within its room. Defaults to the participant's identity. participant_id: type: string description: Participant whose audio arrives on this stream, from the metadata's participantstreamassoc send binding. participant_aor: type: string description: Address of record of the participant sending on this stream. participant_name: type: string description: Display name of the participant sending on this stream. required: - leg_stream_id SIPRegisterTrunkSpec: type: object properties: registrar_uri: type: string description: Upstream registrar SIP URI (e.g. "sip:pbx.example.com:5060" or "sips:pbx.example.com:5061;transport=tls"). aor: type: string description: Address-of-record this trunk REGISTERs (e.g. "sip:alice@pbx.example.com"). Becomes the From URI on outbound REGISTER, and the From / P-Asserted-Identity host on outbound INVITEs placed `from` this AOR. username: type: string description: Digest auth username. Defaults to the AOR user-part when empty. password: type: string description: Digest auth password. Required. Never returned in any response. contact_user: type: string description: Override the user-part of the Contact header sent in REGISTER. Defaults to the AOR user-part. expires_seconds: type: integer description: 'Requested registration lifetime in seconds. Clamped to [SIP_OUTBOUND_REGISTRATION_MIN_EXPIRES_SECONDS, SIP_OUTBOUND_REGISTRATION_MAX_EXPIRES_SECONDS]. Default: SIP_OUTBOUND_REGISTRATION_DEFAULT_EXPIRES_SECONDS (3600).' required: - registrar_uri - aor - password SIPRegisterTrunkView: type: object properties: registrar_uri: type: string aor: type: string username: type: string contact_uri: type: string requested_expires_seconds: type: integer granted_expires_seconds: type: integer last_registered_at: type: string next_refresh_at: type: string call_id: type: string cseq: type: integer source_address: type: string required: - registrar_uri - aor - requested_expires_seconds TrunkView: type: object properties: id: type: string type: type: string app_id: type: string status: type: string last_error: type: string created_at: type: string sip_register: nullable: true allOf: - $ref: '#/components/schemas/SIPRegisterTrunkView' ip_ip: nullable: true allOf: - $ref: '#/components/schemas/IPIPTrunkView' required: - id - type - status - created_at TrunksListResponse: type: object properties: trunks: type: array items: $ref: '#/components/schemas/TrunkView' required: - trunks WebRTCCandidatesResult: type: object properties: candidates: type: array items: $ref: '#/components/schemas/ICECandidateInit' done: type: boolean required: - candidates - done WebRTCOfferResult: type: object properties: leg_id: type: string sdp: type: string required: - leg_id - sdp WebhookEvent: type: object description: Event envelope delivered via HTTP POST to registered webhook URLs. Event-specific fields are flattened into the top-level object (no "data" wrapper). Includes X-Signature-256 header when a secret is configured, and an X-Event-Id header equal to the event_id field. properties: type: $ref: '#/components/schemas/WebhookEventType' timestamp: type: string format: date-time event_id: type: string format: uuid description: Stable per-event idempotency key; identical across delivery retries and across all subscribers of the event. instance_id: type: string description: Instance identifier required: - type - timestamp WebhookEventType: type: string enum: - leg.ringing - leg.early_media - leg.connected - leg.disconnected - leg.joined_room - leg.left_room - leg.muted - leg.unmuted - leg.deaf - leg.undeaf - leg.hold - leg.unhold - leg.command_failed - leg.stream_added - leg.stream_removed - leg.stream_rejected - leg.stream_failed - leg.stream_room_changed - leg.stream_role_changed - siprec.session_started - siprec.session_ended - siprec.metadata_updated - siprec.participant_joined - siprec.participant_left - dtmf.received - rtt.received - speaking.started - speaking.stopped - playback.started - playback.finished - playback.error - tts.started - tts.finished - tts.error - recording.started - recording.finished - recording.paused - recording.resumed - leg.transfer_initiated - leg.transfer_requested - leg.transfer_progress - leg.transfer_completed - leg.transfer_failed - room.created - room.deleted - room.bridged - room.bridge_updated - room.unbridged - room.routing_changed - leg.role_changed - stt.text - agent.connected - agent.disconnected - agent.user_transcript - agent.agent_response - amd.result - amd.beep - sip.registration_attempt - sip.registration_active - sip.registration_expired - sip.outbound_registration_active - sip.outbound_registration_failed - sip.outbound_registration_expired ICECandidateInit: type: object properties: candidate: type: string description: ICE candidate string sdpMid: type: string description: Media stream identification tag sdpMLineIndex: type: integer description: Index of the media description required: - candidate x-webhooks: leg.ringing: post: summary: SIP call ringing (inbound or outbound) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string leg_type: type: string description: Leg type (e.g. sip_inbound, sip_outbound) uri: type: string description: Dialed SIP URI (outbound only) from: type: string description: Caller URI (inbound) or From header value (outbound, if set) to: type: string description: Callee URI (inbound only) sip_headers: type: object additionalProperties: type: string description: X-* custom SIP headers, if present offered_codecs: type: array items: $ref: '#/components/schemas/OfferedCodec' trunk_id: type: string source_address: type: string authenticated: type: boolean auth_username: type: string leg.early_media: post: summary: Outbound leg received 183 Session Progress with SDP; media pipeline active requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string leg_type: type: string description: Leg type (e.g. sip_outbound) leg.connected: post: summary: Leg answered/connected requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string leg_type: type: string description: Leg type (e.g. sip_inbound, sip_outbound, webrtc) leg.disconnected: post: summary: Leg hung up (CDR-style nested structure) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string cdr: type: object properties: reason: type: string description: 'Disconnect reason. Common SIP failures are mapped to named reasons; unmapped 4xx/5xx/6xx codes appear as sip_{code}. This is an open set — treat an unrecognized value as a new reason rather than an error. Known values: api_hangup, room_deleted, remote_bye, caller_cancel, max_duration, session_expired, rtp_timeout, busy, declined, rejected, unavailable, not_found, forbidden, server_error, ring_timeout, unauthorized, timeout, cancelled, not_acceptable, service_unavailable, invite_failed, connect_failed, challenged, transfer_completed, transfer_originate_failed, transfer_connect_failed, bad_answer, siprec_answer_failed, mixer_panic, hangup, peer_slow, connection_reset, ws_error, ws_dial_failed, moq_error, ice_failure, ice_failed, ice_disconnected, livekit_client_initiated, livekit_duplicate_identity, livekit_server_shutdown, livekit_kicked, livekit_room_deleted, livekit_state_mismatch, livekit_join_failure, livekit_migration, livekit_signal_close, livekit_room_closed, livekit_user_unavailable, livekit_user_rejected, livekit_token_expired, livekit_media_failure, livekit_disconnected, livekit_client_closed, livekit_signal_closed, livekit_signal_error, livekit_pc_setup_failed, livekit_add_track_failed, livekit_publisher_failed, livekit_subscriber_failed, livekit_signal_loop_exit, livekit_set_remote_desc_failed, livekit_create_answer_failed, livekit_set_local_desc_failed, livekit_signal_send_failed, livekit_set_publisher_remote_failed, livekit_participant_left.' duration_total: type: number description: Seconds from leg creation to disconnect duration_answered: type: number description: Seconds from answer to disconnect (0 if never answered) required: - reason - duration_total - duration_answered quality: description: RTP quality metrics. Omitted for WebRTC legs or unanswered legs with no media. type: object nullable: true properties: mos_score: type: number description: Mean Opinion Score (1.0–5.0) estimated via simplified E-model (ITU-T G.107) from packet loss and jitter rtp_packets_received: type: integer description: Total inbound RTP audio packets received rtp_packets_lost: type: integer description: Estimated lost packets based on sequence number gaps rtp_jitter_ms: type: number description: Inter-arrival jitter in milliseconds (RFC 3550 §A.8) required: - mos_score - rtp_packets_received - rtp_packets_lost - rtp_jitter_ms leg.joined_room: post: summary: Leg added to a room requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string leg.left_room: post: summary: Leg removed from a room requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string leg.muted: post: summary: Leg muted requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string leg.unmuted: post: summary: Leg unmuted requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string leg.deaf: post: summary: Leg deafened (stops receiving room audio) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string leg.undeaf: post: summary: Leg undeafened (resumes receiving room audio) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string leg.hold: post: summary: Leg put on hold (local or remote) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string leg_type: type: string description: 'Hold direction: "local" (we put them on hold) or "remote" (they put us on hold)' leg.unhold: post: summary: Leg taken off hold (local or remote) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string leg_type: type: string description: 'Hold direction: "local" or "remote"' leg.command_failed: post: summary: An asynchronous leg command (202 Accepted) failed during execution requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string command: type: string error: type: string leg.stream_added: post: summary: An additional m=audio stream was negotiated on a live dialog requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string stream_id: type: string mid: type: string direction: type: string lang: type: string room_id: type: string role: type: string reason: type: string leg.stream_removed: post: summary: An audio stream was disabled with a port-0 re-INVITE; its m-line slot survives as a tombstone requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string stream_id: type: string mid: type: string direction: type: string lang: type: string room_id: type: string role: type: string reason: type: string leg.stream_rejected: post: summary: The peer refused an additional audio stream, or it could not be negotiated; the call is unaffected requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string stream_id: type: string mid: type: string direction: type: string lang: type: string room_id: type: string role: type: string reason: type: string leg.stream_failed: post: summary: An audio stream's media loop failed and the stream was torn down; the call continues on its remaining streams requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string stream_id: type: string mid: type: string direction: type: string lang: type: string room_id: type: string role: type: string reason: type: string leg.stream_room_changed: post: summary: An audio stream was attached to or detached from a room (an empty room_id means detached) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string stream_id: type: string mid: type: string direction: type: string lang: type: string room_id: type: string role: type: string reason: type: string leg.stream_role_changed: post: summary: An audio stream's routing role changed; the room's allow-sets were recomputed atomically requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string stream_id: type: string mid: type: string direction: type: string lang: type: string room_id: type: string role: type: string reason: type: string siprec.session_started: post: summary: An inbound SIPREC recording session was accepted; carries the participants and the stream-to-participant bindings requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string session_id: type: string data_mode: type: string participants: type: array items: $ref: '#/components/schemas/ParticipantInfo' streams: type: array items: $ref: '#/components/schemas/SIPRECStream' siprec.session_ended: post: summary: A SIPREC recording session ended requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string session_id: type: string reason: type: string siprec.metadata_updated: post: summary: A SIPREC recording session's metadata document was updated on a re-INVITE requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string session_id: type: string data_mode: type: string participants_joined: type: array items: type: string participants_left: type: array items: type: string streams_added: type: array items: type: string streams_removed: type: array items: type: string streams: type: array items: $ref: '#/components/schemas/SIPRECStream' siprec.participant_joined: post: summary: A party joined the call being recorded by a SIPREC session requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string session_id: type: string label: type: string leg_stream_id: type: string participant_id: type: string participant_aor: type: string participant_name: type: string siprec.participant_left: post: summary: A party left the call being recorded by a SIPREC session requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string app_id: type: string session_id: type: string label: type: string leg_stream_id: type: string participant_id: type: string participant_aor: type: string participant_name: type: string dtmf.received: post: summary: DTMF digit received requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string digit: type: string description: DTMF digit received seq: type: integer rtt.received: post: summary: Real-Time Text (T.140 / RFC 4103) chunk received from the remote requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string text: type: string description: UTF-8 text chunk received from the remote seq: type: integer description: Per-leg monotonic sequence (independent of RTP sequence numbers) loss_marker: type: boolean description: True when a U+FFFD has been prepended to indicate text was lost beyond what RFC 2198 redundancy could recover speaking.started: post: summary: Participant started speaking requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier (present only when the leg is in a room) app_id: type: string speaking.stopped: post: summary: Participant stopped speaking requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier (present only when the leg is in a room) app_id: type: string playback.started: post: summary: Playback began requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string playback_id: type: string description: Playback identifier playback.finished: post: summary: Playback ended requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string playback_id: type: string description: Playback identifier reason: type: string description: 'Why playback ended: ''completed'' (reached the end of the audio) or ''stopped'' (did not reach the end, for any reason)' played_ms: type: integer description: Milliseconds of audio actually played, accumulated across repeat iterations playback.error: post: summary: Playback failed requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string playback_id: type: string description: Playback identifier error: type: string description: Error message tts.started: post: summary: TTS synthesis began playing requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string tts_id: type: string description: TTS playback identifier tts.finished: post: summary: TTS synthesis finished playing requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string tts_id: type: string description: TTS playback identifier reason: type: string description: 'Why the utterance ended: ''completed'' (reached the end of the audio) or ''stopped'' (did not reach the end, for any reason)' played_ms: type: integer description: Milliseconds of audio actually played tts.error: post: summary: TTS synthesis or playback failed requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string tts_id: type: string description: TTS playback identifier error: type: string description: Error message category: type: string description: 'Failure category: permanent_auth, permanent_input, rate_limited, service_unavailable, retryable, canceled or unknown for a synthesis failure, playback for a failure while streaming the audio. New values may be added; treat an unrecognised value as unknown' recording.started: post: summary: Recording began requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string file: type: string description: Recording file path or S3 URI — does not exist yet; the path only appears when the recording stops recording.finished: post: summary: Recording ended requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string file: type: string description: Recording file path or S3 URI multi_channel_file: type: string channels: type: object additionalProperties: $ref: '#/components/schemas/ChannelInfo' omitted_legs: type: array items: type: string description: Participants whose audio is missing from multi_channel_file because their capture failed. Absent when the recording is complete recording.paused: post: summary: Recording paused (audio replaced with silence) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string file: type: string recording.resumed: post: summary: Recording resumed from a paused state requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string file: type: string leg.transfer_initiated: post: summary: We sent a SIP REFER (transfer initiated by the operator) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string kind: type: string description: 'Transfer kind: "blind" or "attended"' target: type: string description: SIP URI to which the leg is being transferred replaces_leg_id: type: string description: Leg whose dialog is replaced (attended transfer only) leg.transfer_requested: post: summary: We received a SIP REFER from the peer; decide via accept_transfer/decline_transfer (unless SIP_REFER_AUTO_DIAL=true) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string kind: type: string description: 'Transfer kind: "blind" or "attended"' target: type: string description: SIP URI requested by the peer replaces_call_id: type: string description: Call-ID present in the Refer-To Replaces parameter (attended only) declined: type: boolean description: Vestigial (always false); retained for wire compatibility. The outcome now flows via leg.transfer_completed / leg.transfer_failed after the app decides leg.transfer_progress: post: summary: Transfer progress reported via NOTIFY sipfrag requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string status_code: type: integer description: Provisional SIP status from the NOTIFY sipfrag reason: type: string description: Reason phrase leg.transfer_completed: post: summary: Transfer reached terminal 2xx requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string status_code: type: integer description: Final 2xx SIP status from the NOTIFY sipfrag reason: type: string description: Reason phrase leg.transfer_failed: post: summary: Transfer failed (REFER rejected, sipfrag non-2xx, or local error) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string status_code: type: integer description: Final non-2xx SIP status (when applicable) reason: type: string description: Reason phrase error: type: string description: Local error message (when no SIP status applies) room.created: post: summary: Room created requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: room_id: type: string description: Room identifier app_id: type: string room.deleted: post: summary: Room deleted requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: room_id: type: string description: Room identifier app_id: type: string room.bridged: post: summary: Two rooms' mixers were joined requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: bridge_id: type: string description: Bridge identifier room_a_id: type: string description: First bridged room room_b_id: type: string description: Second bridged room app_id: type: string direction: type: string description: 'Canonical direction relative to room_a_id: "bidirectional", "a_to_b", "b_to_a", or "none"' room.bridge_updated: post: summary: A bridge's audio flow direction changed requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: bridge_id: type: string description: Bridge identifier room_a_id: type: string description: First bridged room room_b_id: type: string description: Second bridged room app_id: type: string direction: type: string description: New canonical direction relative to room_a_id room.unbridged: post: summary: A bridge was torn down requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: bridge_id: type: string description: Bridge identifier room_a_id: type: string description: First bridged room room_b_id: type: string description: Second bridged room app_id: type: string reason: type: string description: Empty for an explicit delete, or "room_deleted" when a bridged room was deleted room.routing_changed: post: summary: The room's audio routing matrix changed requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: room_id: type: string app_id: type: string matrix: type: object additionalProperties: type: array items: type: string reason: type: string leg.role_changed: post: summary: A leg's routing role changed requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string room_id: type: string app_id: type: string old_role: type: string new_role: type: string stt.text: post: summary: Speech-to-text transcript requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string text: type: string description: Transcribed text is_final: type: boolean description: Whether this is a final or partial transcript agent.connected: post: summary: Agent connected to provider requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string conversation_id: type: string description: Provider-assigned conversation identifier agent.disconnected: post: summary: Agent session ended requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string agent.user_transcript: post: summary: User speech transcribed by agent requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string text: type: string description: User speech text agent.agent_response: post: summary: Agent generated a response requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier room_id: type: string description: Room identifier app_id: type: string text: type: string description: Agent response text amd.result: post: summary: Answering machine detection completed requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string result: type: string description: 'Detection result: human, machine, no_speech, or not_sure' initial_silence_ms: type: integer description: Milliseconds of silence before first speech greeting_duration_ms: type: integer description: Milliseconds of speech in the greeting total_analysis_ms: type: integer description: Total milliseconds of analysis before determination amd.beep: post: summary: Voicemail beep tone detected after machine classification requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: leg_id: type: string description: Leg identifier app_id: type: string beep_ms: type: integer description: Milliseconds from machine detection to beep tone detection sip.registration_attempt: post: summary: Inbound REGISTER surfaced for a challenge/accept/reject decision (auto-accepts on consult timeout) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: app_id: type: string attempt_id: type: string aor: type: string contact: type: string source_address: type: string transport: type: string user_agent: type: string call_id: type: string has_authorization: type: boolean sip.registration_active: post: summary: SIP AOR registration created or refreshed (one event per Contact) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: app_id: type: string aor: type: string description: Canonical Address of Record (e.g. sip:alice@vb.example) contact: type: string description: Contact URI registered by the UA socket: type: string description: Transport-layer socket (ip:port) the REGISTER arrived on transport: type: string description: 'Transport: udp | tcp | tls' user_agent: type: string description: User-Agent header from the REGISTER, if present call_id: type: string description: Call-ID of the most recent REGISTER granted_expires_seconds: type: integer description: Expiry granted to the binding (clamped to SIP_REGISTRATION_MAX_EXPIRES_SECONDS) expires_at: type: string description: Absolute expiry time (RFC 3339) sip.registration_expired: post: summary: SIP AOR registration removed (TTL, explicit unregister, force-delete, or single-binding replacement) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: app_id: type: string aor: type: string description: Canonical Address of Record contact: type: string description: Contact URI that was unbound socket: type: string description: Transport-layer socket that held the binding reason: type: string description: 'Why the binding was removed: ttl, unregistered, forced, or replaced' sip.outbound_registration_active: post: summary: Outbound SIP trunk REGISTER accepted (initial or refresh) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: app_id: type: string trunk_id: type: string aor: type: string registrar: type: string contact: type: string granted_expires_seconds: type: integer expires_at: type: string call_id: type: string source_address: type: string sip.outbound_registration_failed: post: summary: Outbound SIP trunk REGISTER failed (transport error, non-2xx response, or digest auth rejected) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: app_id: type: string trunk_id: type: string aor: type: string registrar: type: string status_code: type: integer reason: type: string error: type: string sip.outbound_registration_expired: post: summary: Outbound SIP trunk removed (DELETE, shutdown, or refresh failed past granted lifetime) requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/WebhookEvent' - properties: app_id: type: string trunk_id: type: string aor: type: string registrar: type: string reason: type: string