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_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: 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. 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"`. 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' /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. required: - leg_id 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 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 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 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 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 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 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 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 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 - 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, 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 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