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: 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_PORT default: "5060" description: SIP listen port - name: SIP_HOST default: voiceblender description: SIP User-Agent name - name: ICE_SERVERS default: stun:stun.l.google.com:19302 description: STUN/TURN URLs for WebRTC ICE, comma-separated - 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' - 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: 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: 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.' 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 SIP call 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 SIP URI, 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/{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 tags: - Legs responses: '200': description: Leg hung up content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '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 tags: - Legs requestBody: content: application/json: schema: $ref: '#/components/schemas/AnswerLegRequest' responses: '200': description: Answer initiated content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '400': description: Not a SIP inbound leg or invalid body 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 description: Sends SIP 183 Session Progress with SDP and sets up the media pipeline. The leg transitions to `early_media` state, allowing audio playback and room participation before the call is answered. tags: - Legs responses: '200': description: Early media enabled content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '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' '500': description: Media setup failed 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 description: Sends a re-INVITE with `sendonly` SDP direction. The RTP timeout is paused while held, and a 2-hour auto-hangup timer starts. tags: - Legs responses: '200': description: Call held content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '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: Leg is not in connected state, or already held content: application/json: schema: $ref: '#/components/schemas/Error' delete: operationId: unholdLeg summary: Resume a held SIP call description: Sends a re-INVITE with `sendrecv` SDP direction. tags: - Legs responses: '200': description: Call resumed content: application/json: schema: $ref: '#/components/schemas/StatusResponse' '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: Leg is not held 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}/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}/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}/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/StatusResponse' '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. 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}/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' /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/StatusResponse' '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 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) app_id: type: string description: Application identifier for event stream filtering. sip_headers: type: object additionalProperties: type: string description: X-* headers from the inbound INVITE. Only present on sip_inbound legs. 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). 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). CreateLegRequest: type: object properties: type: type: string description: Leg type enum: - sip - whatsapp 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 — sets the user part of the SIP From header (e.g. "+15551234567", "alice") 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 description: Codec preference order headers: type: object additionalProperties: type: string description: Custom SIP headers to include in the outbound INVITE (e.g. X-Correlation-ID) 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). required: - type 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 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) 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 RecordingRequest: type: object properties: storage: type: string description: '"file" (default) — local disk, "s3" — upload to S3 after recording stops' enum: - file - s3 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. required: - storage - multi_channel - s3_bucket - s3_region - s3_endpoint - s3_prefix - s3_access_key - s3_secret_key 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 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 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 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 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 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 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 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. properties: type: $ref: '#/components/schemas/WebhookEventType' timestamp: type: string format: date-time 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 - dtmf.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 - stt.text - agent.connected - agent.disconnected - agent.user_transcript - agent.agent_response - amd.result - amd.beep 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 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}. enum: - api_hangup - remote_bye - caller_cancel - ring_timeout - max_duration - busy - unavailable - not_found - forbidden - unauthorized - timeout - cancelled - not_acceptable - service_unavailable - declined - rtp_timeout - session_expired - invite_failed - connect_failed - ice_failure 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"' 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 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 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 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 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 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' 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 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: True when the REFER was declined (e.g. SIP_REFER_AUTO_DIAL=false) 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 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