Legs

Last updated August 14, 2026

Voice call legs (SIP or WebRTC)

# GET /legs

List all active legs

Responses

200 Array of legs
# POST /legs

Originate an outbound leg

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).

Request Body

FieldTypeDescription
typeenumrequiredLeg type
Values: sip, whatsapp, websocket, livekit_room
tostringoptionalDestination. For sip legs, a SIP URI (e.g. "sip:[email protected]"). For whatsapp legs, an E.164 phone number (with or without '+').
uristringoptionalDeprecated alias for `to` (sip legs only). Prefer `to`.
fromstringoptionalCaller ID. A bare user-part (e.g. "+15551234567", "alice") sets the user of the SIP From header. A full SIP URI (e.g. "sip:[email protected]") sets both the user and the host; otherwise the host comes from the matched trunk's AOR realm, falling back to SIP_DOMAIN.
privacystringoptionalSIP Privacy header value (e.g. "id", "none")
ring_timeoutintegeroptionalSeconds to wait for answer; 0 = no timeout 0
max_durationintegeroptionalMaximum call duration in seconds after connect. Automatically hung up when reached. 0 or omitted = no limit. 0
codecsarray[enum]optionalCodec preference order (sip legs only)
headersobjectoptionalCustom headers to include in the outbound INVITE (sip/whatsapp) or the WebSocket upgrade request (websocket)
room_idstringoptionalRoom 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.
authanyoptionalDigest 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).
webhook_urlstring(uri)optionalRoute all events for this leg exclusively to this URL instead of global webhooks.
webhook_secretstringoptionalHMAC-SHA256 signing secret for the per-leg webhook.
amdanyoptionalEnable Answering Machine Detection on outbound calls. Include the object (even empty) to enable with defaults; omit to disable.
accept_dtmfbooleanoptionalIf false, this leg will not receive DTMF digits broadcast from other legs in the same room. Defaults to true. true
app_idstringoptionalApplication identifier. Carried through to all events for this leg. Use to filter the WebSocket event stream by app.
speech_detectionbooleanoptionalIf 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).
rttbooleanoptionalFor 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. false
streamsarray[object]optionalSIP outbound only. Extra m=audio sections to offer alongside the call's primary bidirectional audio, so a multi-stream call is established by the first INVITE instead of a follow-up re-INVITE. Each entry binds its own RTP port and may be mixed into its own room. To add a stream to a call that is already up, use POST /v1/legs/{id}/streams instead.
urlstring(uri)optionalWebSocket target URL (ws:// or wss://) for outbound websocket legs. Required when type=websocket.
sample_rateenumoptionalPCM sample rate for websocket legs. The room's mixer automatically resamples between this and the room rate. 16000
Values: 8000, 16000, 24000, 48000
wire_formatenumoptionalAudio framing for websocket legs. `binary` ships raw PCM as WebSocket binary frames; `json_base64` wraps PCM as `{"type":"audio","audio":"<base64>"}` text frames (browser-friendly). "binary"
Values: binary, json_base64
sample_formatenumoptionalOn-the-wire PCM sample encoding for websocket legs. v1 only supports `s16le`. "s16le"
Values: s16le
livekitanyoptionalLiveKit room join parameters (only used when type=livekit_room).

Responses

201 Leg created
FieldTypeDescription
instance_idstringoptionalInstance identifier
idstringrequiredUnique leg identifier (UUID)
typeenumrequiredLeg type
Values: sip_inbound, sip_outbound, webrtc, whatsapp_in, whatsapp_out, websocket_in, websocket_out, moq_in, livekit_publish, livekit_participant
stateenumrequiredLeg state
Values: ringing, early_media, connected, held, hung_up
room_idstringoptionalRoom ID if the leg is in a room, empty otherwise
mutedbooleanrequiredWhether the leg is muted (cannot be heard by others)
deafbooleanrequiredWhether the leg is deaf (cannot hear others)
accept_dtmfbooleanrequiredWhether the leg receives DTMF digits broadcast from other legs in the same room. Defaults to true.
heldbooleanrequiredWhether the call is on hold (SIP legs only)
rolestringoptionalRouting role used by the room's audio routing matrix (e.g. "customer", "agent", "supervisor"). Empty string means unroled (full mesh).
app_idstringoptionalApplication identifier for event stream filtering.
sip_headersobjectoptionalDeprecated: 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.
headersobjectoptionalCustom 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.
400 Invalid JSON, bad URI/URL, unknown codec, or unsupported type
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# GET /legs/websocket

Connect a WebSocket as a leg (HTTP upgrade)

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":"<base64>"}. 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.

Responses

101 Switching Protocols — WebSocket upgrade succeeded
400 Invalid query parameters
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
500 Room create failure
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# CONNECT /legs/moq

Connect a MoQ (Media over QUIC) leg (WebTransport extended-CONNECT, experimental)

**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.

Responses

200 WebTransport extended-CONNECT accepted; MoQ session established (no JSON body — the response is the upgraded WebTransport session)
400 Invalid query parameters or config
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
500 Room create failure
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
503 MoQ endpoint is not enabled (`MOQ_ENABLED=false`)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# GET /legs/{id}

Get a single leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Leg details
FieldTypeDescription
instance_idstringoptionalInstance identifier
idstringrequiredUnique leg identifier (UUID)
typeenumrequiredLeg type
Values: sip_inbound, sip_outbound, webrtc, whatsapp_in, whatsapp_out, websocket_in, websocket_out, moq_in, livekit_publish, livekit_participant
stateenumrequiredLeg state
Values: ringing, early_media, connected, held, hung_up
room_idstringoptionalRoom ID if the leg is in a room, empty otherwise
mutedbooleanrequiredWhether the leg is muted (cannot be heard by others)
deafbooleanrequiredWhether the leg is deaf (cannot hear others)
accept_dtmfbooleanrequiredWhether the leg receives DTMF digits broadcast from other legs in the same room. Defaults to true.
heldbooleanrequiredWhether the call is on hold (SIP legs only)
rolestringoptionalRouting role used by the room's audio routing matrix (e.g. "customer", "agent", "supervisor"). Empty string means unroled (full mesh).
app_idstringoptionalApplication identifier for event stream filtering.
sip_headersobjectoptionalDeprecated: 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.
headersobjectoptionalCustom 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.
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}

Hang up a leg (asynchronous)

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": "<value>"} 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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
reasonenumoptionalDisconnect 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.
Values: busy, declined, rejected, unavailable, not_found, forbidden, server_error

Responses

202 Hangup queued
400 Unknown reason value
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/answer

Answer a ringing or early-media inbound SIP leg (asynchronous)

Signals the inbound-call goroutine to send 200 OK. The HTTP call returns 202 immediately; the actual SIP 200 OK is sent in the background, and the leg's transition is observed via leg.connected. Pre-condition failures (wrong state, unknown codec) still return 4xx synchronously. When the caller offers several m=audio sections they are all accepted; streams optionally routes each accepted stream beyond the primary into its own room, applied once the answer is negotiated.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
speech_detectionbooleanoptionalIf 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).
codecenumoptionalExplicit codec for the answer SDP. Must appear in the remote offer's offered_codecs list. Omit to use the server's default preference order.
Values: PCMU, PCMA, G722, opus, AMR-WB, AMR-NB
streamsarray[object]optionalRooms for the caller's additional audio streams, applied once the answer is negotiated. Positional: entry i addresses the i-th accepted stream beyond the primary, in m-line order — the caller's offer decides how many exist, so an entry with no matching stream is ignored. Use POST /v1/legs/{id}/streams/{streamId}/room to re-route a stream later.

Responses

202 Answer queued
400 Not a SIP inbound leg, invalid body, or codec not in offer
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg is not in ringing or early_media state
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/ring

Send 180 Ringing on a ringing inbound SIP leg (asynchronous)

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".

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

202 180 Ringing queued
400 Not a SIP inbound leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg is not in ringing state
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/challenge

Challenge a ringing inbound SIP leg with a 401 digest auth request

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
realmstringrequired
usernamestringoptional
passwordstringoptional
ha1stringoptional
algorithmstringoptional
qoparray[string]optional
max_expiresintegeroptional

Responses

202 401 challenge queued
400 Not a SIP inbound leg, or missing realm/credential
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg is not in ringing or early_media state
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/early-media

Enable early media on a ringing inbound SIP leg (asynchronous)

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".

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
codecenumoptionalExplicit 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.
Values: PCMU, PCMA, G722, opus, AMR-WB, AMR-NB

Responses

202 Early media queued
400 Not a SIP inbound leg or codec not in offer
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg is not in ringing state
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/mute

Mute a leg

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Leg muted
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/mute

Unmute a leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Leg unmuted
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/hold

Put a SIP call on hold (asynchronous)

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

202 Hold queued
400 Not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Hold not supported for this leg type (e.g. WhatsApp)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/hold

Resume a held SIP call (asynchronous)

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".

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

202 Unhold queued
400 Not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Hold not supported for this leg type (e.g. WhatsApp)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/transfer

Transfer a SIP leg via REFER (asynchronous)

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
targetstringrequiredSIP URI to transfer the call to (e.g. "sip:[email protected]").
replaces_leg_idstringoptionalID of an existing connected SIP leg whose dialog should be replaced (attended transfer). Omit for blind transfer.

Responses

202 Transfer request accepted for processing
400 Missing or invalid target URI (including URIs without a host such as sip:)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg not connected, not a SIP leg, or replaces_leg_id is invalid
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/transfer/accept

Accept a parked inbound REFER

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

202 Accept queued (202 + NOTIFY 100 sent)
404 No pending transfer for the leg (unknown or already decided)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/transfer/progress

Report interim transfer progress

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
status_codeintegerrequired
reasonstringoptional

Responses

202 Progress NOTIFY queued
400 status_code out of range (must be 100-699)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 No accepted transfer for the leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/transfer/complete

Complete an accepted inbound transfer

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
successbooleanrequired
status_codeintegeroptional
reasonstringoptional

Responses

202 Terminal NOTIFY queued
404 No accepted transfer for the leg (accept it first)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/transfer/decline

Decline a parked inbound REFER

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
codeintegeroptional
reasonstringoptional

Responses

202 Decline queued
404 No pending transfer for the leg (unknown or already accepted)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/dtmf

Send DTMF digits on a leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
digitsstringrequiredDTMF digits to send (0-9, *, #)

Responses

200 Digits sent
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON or empty digits
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
500 DTMF writer unavailable
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/dtmf/accept

Enable DTMF reception on a leg

Allow this leg to receive DTMF digits broadcast from other legs in the same room. This is the default state for new legs.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 DTMF reception enabled
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/dtmf/reject

Disable DTMF reception on a leg

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 DTMF reception disabled
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/rtt

Send Real-Time Text (T.140) on a SIP leg

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
textstringrequiredUTF-8 text to send. May be one or more characters and may include T.140 control codes (e.g. backspace U+0008, CR/LF).

Responses

200 Text sent
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON or empty text
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 RTT was not negotiated for this leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
500 Send failed
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/rtt/accept

Enable RTT reception on a leg

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 RTT reception enabled
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/rtt/reject

Disable RTT reception on a leg

Block this leg from receiving RTT text broadcast from other legs in the same room and suppress rtt.received events for this leg.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 RTT reception disabled
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/play

Start audio playback to a leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
urlstring(uri)requiredURL of the audio file (mutually exclusive with tone)
tonestringrequiredBuilt-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_typestringrequiredMIME type (e.g. audio/wav). Required when using url.
repeatintegerrequiredNumber of times to repeat playback (url only) 0
volumeintegerrequiredVolume adjustment in dB (-8 to 8) 0

Responses

200 Playback started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON or volume out of range
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg has no audio writer
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# PATCH /legs/{id}/play/{playbackID}

Change the volume of an active leg playback

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
playbackIDpathstringrequiredPlayback ID

Request Body

FieldTypeDescription
volumeintegerrequiredVolume adjustment (-8 to 8, ~3dB per step, 0 = unchanged)

Responses

200 Volume updated
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON or volume out of range
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Playback not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/play/{playbackID}

Stop audio playback on a leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
playbackIDpathstringrequiredPlayback ID

Responses

200 Playback stopped
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No playback in progress
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/tts

Synthesize speech and play it on a leg

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
textstringrequiredText to synthesize
voicestringrequiredProvider-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_idstringrequiredProvider-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).
languagestringoptionalLanguage 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.
promptstringoptionalStyle/tone instruction for promptable voice models (Google Gemini TTS only). E.g. "Read aloud in a warm, welcoming tone."
volumeintegerrequiredVolume adjustment in dB (-8 to 8) 0
providerenumoptionalTTS provider: "elevenlabs" (default), "aws", "google", or "deepgram"
Values: elevenlabs, aws, google, deepgram
api_keystringoptionalElevenLabs: 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).

Responses

200 TTS playback started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON, missing text/voice, or volume out of range
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg has no audio writer
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
503 No API key provided for the selected provider
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/tts/preflight

Synthesize speech and hold it for a later commit

Stages a speculative reply. The audio is synthesized and buffered in memory but not played, so that committing it starts playback with no synthesis delay. Intended for the turn-taking loop of a voice agent: start a draft reply on the eager_end_of_turn stt.turn event, then commit it on end_of_turn or discard it on turn_resumed. A tts.staged event reports when the audio is ready; synthesis failures are reported on tts.error. Staged utterances are dropped after TTS_PREFLIGHT_TTL or when the leg ends, and at most TTS_PREFLIGHT_MAX_PER_LEG may be staged on one leg at a time. Preflight is leg-scoped; use POST /rooms/{id}/tts for room announcements.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
textstringrequiredText to synthesize
voicestringrequiredProvider-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_idstringrequiredProvider-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).
languagestringoptionalLanguage 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.
promptstringoptionalStyle/tone instruction for promptable voice models (Google Gemini TTS only). E.g. "Read aloud in a warm, welcoming tone."
volumeintegerrequiredVolume adjustment in dB (-8 to 8) 0
providerenumoptionalTTS provider: "elevenlabs" (default), "aws", "google", or "deepgram"
Values: elevenlabs, aws, google, deepgram
api_keystringoptionalElevenLabs: 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).

Responses

200 TTS staged
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON, missing text/voice, or volume out of range
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg has no audio writer, or too many staged utterances on this leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
503 No API key provided for the selected provider
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/tts/{ttsID}/commit

Play a staged TTS utterance

Starts playback of an utterance staged by the preflight endpoint. Returns immediately, before synthesis has necessarily finished; failure is reported asynchronously on tts.error, exactly as for POST /legs/{id}/tts. Once committed, stop playback with DELETE /legs/{id}/play/{playbackID} using the same tts_id.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
ttsIDpathstringrequired

Responses

200 TTS committed; playback starts as soon as the audio is ready
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No staged TTS with that id
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Staged TTS already committed
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/tts/{ttsID}

Drop a staged TTS utterance without playing it

Discards an utterance staged by the preflight endpoint, aborting synthesis if it is still in flight. Already-committed utterances are stopped with DELETE /legs/{id}/play/{playbackID} instead.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
ttsIDpathstringrequired

Responses

200 Staged TTS discarded
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No staged TTS with that id
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Staged TTS already committed
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/record

Start recording a leg to a WAV file

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).

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
storageenumrequired"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)
Values: file, s3, gcs
multi_channelbooleanrequiredWhen true, record each participant to a separate mono WAV file in addition to the full mix. Only applies to room recordings. false
s3_bucketstringrequiredS3 bucket name. Overrides S3_BUCKET env var. Required if env var is not set.
s3_regionstringrequiredAWS region. Overrides S3_REGION env var. Default us-east-1.
s3_endpointstringrequiredCustom S3 endpoint (MinIO, etc.). Overrides S3_ENDPOINT env var.
s3_prefixstringrequiredKey prefix (e.g. recordings/). Overrides S3_PREFIX env var.
s3_access_keystringrequiredAWS access key ID. Overrides default credential chain.
s3_secret_keystringrequiredAWS secret access key. Must be set together with s3_access_key.
gcs_bucketstringrequiredGCS bucket name. Overrides GCS_BUCKET env var. Required if env var is not set when storage=gcs.
gcs_object_name_prefixstringrequiredObject name prefix (e.g. recordings or recordings/). Overrides GCS_OBJECT_NAME_PREFIX env var. A trailing slash is added automatically when missing.
filenamestringrequiredOptional 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.

Responses

200 Recording started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid storage type, S3 not configured, or invalid S3 credentials
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg has no audio reader or room not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
500 Failed to create recording file
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/record

Stop recording a leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Recording stopped
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No recording in progress
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/record/pause

Pause a leg recording

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Recording paused (or already paused)
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No recording in progress
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/record/resume

Resume a paused leg recording

Resumes writing real audio after a prior /record/pause. Idempotent: calling while not paused returns status: not_paused.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Recording resumed (or was not paused)
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No recording in progress
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/stt

Start real-time speech-to-text on a leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
languagestringrequiredLanguage code (e.g. "en", "es")
partialbooleanrequiredEmit partial (non-final) transcripts false
providerenumoptionalSTT provider: "elevenlabs" (default), "deepgram" (/v1/listen), "deepgram_flux" (/v2/listen, conversational turn detection) or "azure"
Values: elevenlabs, deepgram, deepgram_flux, azure
api_keystringoptionalAPI key override (falls back to ELEVENLABS_API_KEY, DEEPGRAM_API_KEY or AZURE_SPEECH_KEY env var depending on provider)
modelstringoptionalProvider-specific model. Deepgram: default "nova-3". Deepgram Flux: "flux-general-en" (default) or "flux-general-multi".
keytermsarray[string]optionalTerms to boost recognition of (Deepgram and Deepgram Flux).
endpointingintegeroptionalDeepgram only: milliseconds of silence before a segment is finalized. 0 disables endpointing.
utterance_end_msintegeroptionalDeepgram only: milliseconds of silence after which an stt.turn event with event=utterance_end is emitted. Deepgram requires interim results for this, which are requested automatically and still suppressed unless partial is true.
eager_eot_thresholdnumberoptionalDeepgram Flux only: end-of-turn confidence that fires an eager_end_of_turn stt.turn event, enabling speculative generation. Must be between 0.3 and 0.9. When unset, no eager_end_of_turn or turn_resumed events are emitted at all.
eot_thresholdnumberoptionalDeepgram Flux only: end-of-turn confidence required to close a turn. Deepgram default 0.7.
eot_timeout_msintegeroptionalDeepgram Flux only: milliseconds of silence after which a turn is closed regardless of confidence. Deepgram default 5000.
language_hintsarray[string]optionalDeepgram Flux only: candidate language codes for the "flux-general-multi" model.

Responses

200 STT started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg not connected, STT already running, or no audio reader
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
503 No ElevenLabs API key provided
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/stt

Stop speech-to-text on a leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 STT stopped
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No STT in progress
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/stt/finalize

Flush the STT buffer on a leg without stopping STT

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 the deepgram provider supports this; deepgram_flux, azure and elevenlabs answer 501 — /v2/listen has no flush message, and Flux reports turn ends itself on stt.turn. 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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Final transcript requested
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No STT in progress
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 STT session not connected or the flush failed
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
501 The active STT provider does not support finalize
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/agent/elevenlabs

Attach an ElevenLabs ConvAI agent to a leg

Bridges audio bidirectionally with an ElevenLabs conversational AI agent. Standalone legs use direct audio; legs in a room use mixer taps.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
agent_idstringrequiredElevenLabs agent ID
first_messagestringoptionalOverride the agent's first message
languagestringoptionalLanguage code (e.g. "en", "es")
dynamic_variablesobjectoptionalKey-value pairs passed to the agent as dynamic variables
api_keystringoptionalAPI key override (falls back to ELEVENLABS_API_KEY env var)

Responses

200 Agent started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON or missing agent_id
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg not connected, agent already attached, or no audio reader/writer
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
503 No ElevenLabs API key provided
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/agent/vapi

Attach a VAPI agent to a leg

Bridges audio bidirectionally with a VAPI conversational AI agent. Standalone legs use direct audio; legs in a room use mixer taps.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
assistant_idstringrequiredVAPI assistant ID
first_messagestringoptionalOverride the agent's first message
variable_valuesobjectoptionalKey-value pairs passed as VAPI variable values (assistantOverrides.variableValues)
api_keystringoptionalAPI key override (falls back to VAPI_API_KEY env var)

Responses

200 Agent started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON or missing assistant_id
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg not connected, agent already attached, or no audio reader/writer
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
503 No VAPI API key provided
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/agent/pipecat

Attach a Pipecat bot to a leg

Bridges audio bidirectionally with a self-hosted Pipecat bot via WebSocket. Standalone legs use direct audio; legs in a room use mixer taps.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
websocket_urlstring(uri)requiredWebSocket URL of the Pipecat bot (e.g. ws://my-bot:8765)

Responses

200 Agent started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON or missing websocket_url
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg not connected, agent already attached, or no audio reader/writer
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/agent/deepgram

Attach a Deepgram Voice Agent to a leg

Bridges audio bidirectionally with a Deepgram Voice Agent. Standalone legs use direct audio; legs in a room use mixer taps.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
settingsobjectoptionalFull 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).
greetingstringoptionalAgent greeting message
languagestringoptionalLanguage code (e.g. "en", "es")
api_keystringoptionalAPI key override (falls back to DEEPGRAM_API_KEY env var)

Responses

200 Agent started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg not connected, agent already attached, or no audio reader/writer
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
503 No Deepgram API key provided
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/agent/message

Inject a message into a running agent session on a leg

Sends a context message or instruction to the running agent. Supported by Deepgram (InjectAgentMessage), Pipecat (TextFrame), and VAPI (control URL). Returns 501 for ElevenLabs.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
messagestringrequiredContext or instruction to inject into the running agent session

Responses

200 Message sent
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid JSON or missing message
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 No agent attached to this leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Agent session not running
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
501 Provider does not support message injection
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/agent

Detach the agent from a leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Agent stopped
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
404 No agent attached to this leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/amd

Start answering machine detection on a connected leg

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
initial_silence_timeoutintegeroptionalMax milliseconds of silence before declaring no_speech 2500
greeting_durationintegeroptionalSpeech duration threshold (ms) above which answerer is classified as machine 1500
after_greeting_silenceintegeroptionalSilence duration (ms) after initial speech to declare human 800
total_analysis_timeintegeroptionalMax 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. 5000
minimum_word_lengthintegeroptionalMinimum speech burst duration (ms) to count as a word 100
beep_timeoutintegeroptionalMax time (ms) to wait for the voicemail beep after machine detection. 0 or omitted = disabled. 0

Responses

200 AMD started
FieldTypeDescription
instance_idstringoptionalInstance identifier
statusstringrequired
400 Invalid AMD params or not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg is not in connected state
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# GET /legs/{id}/siprec

Get a SIPREC recording session

Returns the RFC 7865 recording metadata of an inbound SIPREC session (leg type siprec_in): every recorded participant, every negotiated media stream, and the binding between them. A stream's a=label is what ties the m= section to a participant, so the streams entries carry both the leg stream ID and the participant identity. The raw metadata document is returned verbatim in metadata.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Recording session state
FieldTypeDescription
leg_idstringrequiredLeg carrying the recording session.
session_idstringoptionalCommunication session being recorded, from the metadata's sessionrecordingassoc.
data_modeenumoptionalData mode of the most recently applied metadata document (RFC 7865 §6.1).
Values: complete, partial
room_idstringoptionalRoom this session's streams were attached to, when SIPREC_ROOM_MODE placed them in one.
participantsarray[object]requiredEvery party currently recorded by this session.
streamsarray[object]requiredEvery negotiated media stream, joined to the participant it carries.
metadatastringoptionalThe raw rs-metadata XML document as most recently received.
400 Leg is not a SIPREC recording session
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg or recording session state not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/siprec

Fork a single call to an external SIPREC recording server

Originates a SIPREC recording session (RFC 7866) carrying one call as two sendonly sections: what the far end says, and what this server sends them. No room is involved. leg_ids is ignored here — the two sections are fixed. Returns the resulting siprec_out leg; delete it to end the session. Requires SIPREC_SRC_ENABLED=true.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
srs_uristringrequiredSIP URI of the session recording server, e.g. "sip:[email protected]:5060". A recording session carries the metadata document alongside the SDP and exceeds the UDP message limit, so the target should accept TCP.
leg_idsarray[string]optionalWhich participants to record. Each entry is either a leg ID (that leg's own audio) or "<legID>#<streamID>" for one of a leg's secondary audio streams mixed into the room. Empty or absent records every participant. An entry that is not in the room is a 404.
session_idstringoptionalCommunication session identifier put in the recording metadata. Defaults to the room ID.
app_idstringoptionalApplication identifier tagged onto the resulting leg and its events.
auth_usernamestringoptionalSIP digest username, when the recording server challenges the INVITE.
auth_passwordstringoptionalSIP digest password, when the recording server challenges the INVITE.
headersobjectoptionalExtra SIP headers to include in the INVITE. Require: siprec is always sent.

Responses

201 Recording session established
FieldTypeDescription
instance_idstringoptionalInstance identifier
idstringrequiredUnique leg identifier (UUID)
typeenumrequiredLeg type
Values: sip_inbound, sip_outbound, webrtc, whatsapp_in, whatsapp_out, websocket_in, websocket_out, moq_in, livekit_publish, livekit_participant
stateenumrequiredLeg state
Values: ringing, early_media, connected, held, hung_up
room_idstringoptionalRoom ID if the leg is in a room, empty otherwise
mutedbooleanrequiredWhether the leg is muted (cannot be heard by others)
deafbooleanrequiredWhether the leg is deaf (cannot hear others)
accept_dtmfbooleanrequiredWhether the leg receives DTMF digits broadcast from other legs in the same room. Defaults to true.
heldbooleanrequiredWhether the call is on hold (SIP legs only)
rolestringoptionalRouting role used by the room's audio routing matrix (e.g. "customer", "agent", "supervisor"). Empty string means unroled (full mesh).
app_idstringoptionalApplication identifier for event stream filtering.
sip_headersobjectoptionalDeprecated: 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.
headersobjectoptionalCustom 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.
400 Invalid JSON, invalid srs_uri, or not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
403 Outbound SIPREC is disabled
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 The leg is itself a recording session
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
502 The recording server rejected the session
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# GET /legs/{id}/streams

List a leg's audio streams

Returns every negotiated m=audio section on the leg, in m-line order. A single-stream call has exactly one entry, the primary.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Responses

200 Audio streams in m-line order
400 Not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/streams

Add an audio stream to a live call

Negotiates an additional m=audio section with a re-INVITE (RFC 3264 §8.1). The new section is appended below the existing ones and binds its own RTP port. Use it to carry a second independent audio stream — a translated feed, for example — alongside the original. Set content to "alt" and lang to the feed's language so the peer can tell them apart. A peer that answers the new section with port 0 leaves the call untouched and this returns 409.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
directionenumoptionalMedia direction for the new stream, from this server's point of view. Defaults to sendrecv. "sendrecv"
Values: sendrecv, sendonly, recvonly, inactive
langstringoptionalBCP 47 language tag advertised as a=lang (RFC 8866), e.g. "es-ES" for a Spanish translation feed.
contentenumoptionalValue advertised as a=content (RFC 4796). Use "main" for original audio and "alt" for an alternative feed such as a translation.
Values: main, alt, speaker, slides, sl
labelstringoptionalValue advertised as a=label (RFC 4574), for correlating the stream with external metadata.
room_idstringoptionalIf set, attach the new stream to this room once it is negotiated.
rolestringoptionalRouting role to apply when room_id is set.

Responses

201 Stream negotiated
FieldTypeDescription
idstringrequiredStream identifier, stable for the life of the dialog. The primary stream is always "0".
midstringoptionalThe stream's SDP a=mid token (RFC 5888), used to correlate it across offer/answer.
indexintegerrequiredPosition of the stream's m= line in the SDP. Fixed for the life of the dialog (RFC 3264 §8).
primarybooleanrequiredTrue for the call's main bidirectional audio stream, which cannot be removed or attached to a room independently.
stateenumrequiredNegotiation state.
Values: pending, active, removed
directionenumrequiredNegotiated media direction from this server's point of view.
Values: sendrecv, sendonly, recvonly, inactive
desired_directionenumoptionalDirection requested by the application. Survives hold/unhold, unlike the negotiated direction.
Values: sendrecv, sendonly, recvonly, inactive
codecstringoptionalCodec negotiated for this stream. Streams on one leg may use different codecs.
sample_rateintegeroptionalNative sample rate of the stream's codec, in Hz.
local_portintegeroptionalLocal RTP port. Each stream binds its own port; a shared transport is undefined without BUNDLE (RFC 9143).
remote_addrstringoptionalRemote RTP address media is currently sent to.
labelstringoptionalThe stream's a=label value (RFC 4574), for correlating it with external metadata.
contentstringoptionalThe stream's a=content value (RFC 4796), e.g. "main" for original audio and "alt" for a translated feed.
langstringoptionalThe stream's a=lang value (RFC 8866): a BCP 47 language tag such as "en" or "es-ES".
room_idstringoptionalRoom this stream's audio is mixed into. A secondary stream may sit in a different room than its leg.
rolestringoptionalRouting role of this stream within its room. Streams carry their own role, independent of their leg's.
400 Invalid JSON or not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg or room not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Leg has no negotiated media yet, or the peer rejected the stream
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# GET /legs/{id}/streams/{streamId}

Get one of a leg's audio streams

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
streamIdpathstringrequired

Responses

200 Audio stream
FieldTypeDescription
idstringrequiredStream identifier, stable for the life of the dialog. The primary stream is always "0".
midstringoptionalThe stream's SDP a=mid token (RFC 5888), used to correlate it across offer/answer.
indexintegerrequiredPosition of the stream's m= line in the SDP. Fixed for the life of the dialog (RFC 3264 §8).
primarybooleanrequiredTrue for the call's main bidirectional audio stream, which cannot be removed or attached to a room independently.
stateenumrequiredNegotiation state.
Values: pending, active, removed
directionenumrequiredNegotiated media direction from this server's point of view.
Values: sendrecv, sendonly, recvonly, inactive
desired_directionenumoptionalDirection requested by the application. Survives hold/unhold, unlike the negotiated direction.
Values: sendrecv, sendonly, recvonly, inactive
codecstringoptionalCodec negotiated for this stream. Streams on one leg may use different codecs.
sample_rateintegeroptionalNative sample rate of the stream's codec, in Hz.
local_portintegeroptionalLocal RTP port. Each stream binds its own port; a shared transport is undefined without BUNDLE (RFC 9143).
remote_addrstringoptionalRemote RTP address media is currently sent to.
labelstringoptionalThe stream's a=label value (RFC 4574), for correlating it with external metadata.
contentstringoptionalThe stream's a=content value (RFC 4796), e.g. "main" for original audio and "alt" for a translated feed.
langstringoptionalThe stream's a=lang value (RFC 8866): a BCP 47 language tag such as "en" or "es-ES".
room_idstringoptionalRoom this stream's audio is mixed into. A secondary stream may sit in a different room than its leg.
rolestringoptionalRouting role of this stream within its room. Streams carry their own role, independent of their leg's.
400 Not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg or stream not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# PATCH /legs/{id}/streams/{streamId}

Change an audio stream's routing role

Updates the stream's role in place and, when the stream is mixed into a room, recomputes that room's matrix-derived allow-sets atomically (single mixer-mutex acquisition), so no audio bleeds through mid-change. Emits leg.stream_role_changed and room.routing_changed with reason: leg_stream_role_changed.

Only the role is mutable here. The SDP-level attributes — direction, lang, content, label — are fixed when the stream is negotiated; change them by removing the stream and adding a new one.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
streamIdpathstringrequired

Request Body

FieldTypeDescription
rolestringoptionalNew routing role for this stream inside its room. The room's routing matrix decides who hears it. Pass an empty string to clear the role (full mesh). Omit to leave it untouched. Applied atomically — the room's allow-sets are recomputed in a single mixer-mutex acquisition, so no audio bleeds through mid-change.

Responses

200 Updated stream
FieldTypeDescription
idstringrequiredStream identifier, stable for the life of the dialog. The primary stream is always "0".
midstringoptionalThe stream's SDP a=mid token (RFC 5888), used to correlate it across offer/answer.
indexintegerrequiredPosition of the stream's m= line in the SDP. Fixed for the life of the dialog (RFC 3264 §8).
primarybooleanrequiredTrue for the call's main bidirectional audio stream, which cannot be removed or attached to a room independently.
stateenumrequiredNegotiation state.
Values: pending, active, removed
directionenumrequiredNegotiated media direction from this server's point of view.
Values: sendrecv, sendonly, recvonly, inactive
desired_directionenumoptionalDirection requested by the application. Survives hold/unhold, unlike the negotiated direction.
Values: sendrecv, sendonly, recvonly, inactive
codecstringoptionalCodec negotiated for this stream. Streams on one leg may use different codecs.
sample_rateintegeroptionalNative sample rate of the stream's codec, in Hz.
local_portintegeroptionalLocal RTP port. Each stream binds its own port; a shared transport is undefined without BUNDLE (RFC 9143).
remote_addrstringoptionalRemote RTP address media is currently sent to.
labelstringoptionalThe stream's a=label value (RFC 4574), for correlating it with external metadata.
contentstringoptionalThe stream's a=content value (RFC 4796), e.g. "main" for original audio and "alt" for a translated feed.
langstringoptionalThe stream's a=lang value (RFC 8866): a BCP 47 language tag such as "en" or "es-ES".
room_idstringoptionalRoom this stream's audio is mixed into. A secondary stream may sit in a different room than its leg.
rolestringoptionalRouting role of this stream within its room. Streams carry their own role, independent of their leg's.
400 Invalid JSON, not a SIP leg, or the primary stream (its role follows the leg)
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg or stream not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/streams/{streamId}

Remove an audio stream from a live call

Disables the stream with a re-INVITE carrying port 0 for its section (RFC 3264 §8.2) and releases its RTP port. The m-line slot survives as a tombstone — the m-line count never decreases for the life of a dialog — so a later added stream takes a new position. The primary stream carries the call and cannot be removed.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
streamIdpathstringrequired

Responses

204 Stream removed
400 Not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg or stream not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Cannot remove the primary stream, or the re-INVITE failed
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# POST /legs/{id}/streams/{streamId}/room

Mix an audio stream into a room

Attaches one of the leg's secondary streams to a room, which may differ from the leg's own room — that is what lets an original audio stream and a translated one be mixed separately. A leg never hears its own other streams, whatever the routing matrix says.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
streamIdpathstringrequired

Request Body

FieldTypeDescription
room_idstringrequiredRoom to mix this stream into. May differ from the leg's own room.
rolestringoptionalRouting role for the stream inside that room. The room's routing matrix decides who hears it.

Responses

200 Updated stream
FieldTypeDescription
idstringrequiredStream identifier, stable for the life of the dialog. The primary stream is always "0".
midstringoptionalThe stream's SDP a=mid token (RFC 5888), used to correlate it across offer/answer.
indexintegerrequiredPosition of the stream's m= line in the SDP. Fixed for the life of the dialog (RFC 3264 §8).
primarybooleanrequiredTrue for the call's main bidirectional audio stream, which cannot be removed or attached to a room independently.
stateenumrequiredNegotiation state.
Values: pending, active, removed
directionenumrequiredNegotiated media direction from this server's point of view.
Values: sendrecv, sendonly, recvonly, inactive
desired_directionenumoptionalDirection requested by the application. Survives hold/unhold, unlike the negotiated direction.
Values: sendrecv, sendonly, recvonly, inactive
codecstringoptionalCodec negotiated for this stream. Streams on one leg may use different codecs.
sample_rateintegeroptionalNative sample rate of the stream's codec, in Hz.
local_portintegeroptionalLocal RTP port. Each stream binds its own port; a shared transport is undefined without BUNDLE (RFC 9143).
remote_addrstringoptionalRemote RTP address media is currently sent to.
labelstringoptionalThe stream's a=label value (RFC 4574), for correlating it with external metadata.
contentstringoptionalThe stream's a=content value (RFC 4796), e.g. "main" for original audio and "alt" for a translated feed.
langstringoptionalThe stream's a=lang value (RFC 8866): a BCP 47 language tag such as "en" or "es-ES".
room_idstringoptionalRoom this stream's audio is mixed into. A secondary stream may sit in a different room than its leg.
rolestringoptionalRouting role of this stream within its room. Streams carry their own role, independent of their leg's.
400 Invalid JSON, missing room_id, not a SIP leg, or the primary stream
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg, stream, or room not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
409 Stream carries no audio in either direction
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# DELETE /legs/{id}/streams/{streamId}/room

Remove an audio stream from its room

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID
streamIdpathstringrequired

Responses

200 Updated stream
FieldTypeDescription
idstringrequiredStream identifier, stable for the life of the dialog. The primary stream is always "0".
midstringoptionalThe stream's SDP a=mid token (RFC 5888), used to correlate it across offer/answer.
indexintegerrequiredPosition of the stream's m= line in the SDP. Fixed for the life of the dialog (RFC 3264 §8).
primarybooleanrequiredTrue for the call's main bidirectional audio stream, which cannot be removed or attached to a room independently.
stateenumrequiredNegotiation state.
Values: pending, active, removed
directionenumrequiredNegotiated media direction from this server's point of view.
Values: sendrecv, sendonly, recvonly, inactive
desired_directionenumoptionalDirection requested by the application. Survives hold/unhold, unlike the negotiated direction.
Values: sendrecv, sendonly, recvonly, inactive
codecstringoptionalCodec negotiated for this stream. Streams on one leg may use different codecs.
sample_rateintegeroptionalNative sample rate of the stream's codec, in Hz.
local_portintegeroptionalLocal RTP port. Each stream binds its own port; a shared transport is undefined without BUNDLE (RFC 9143).
remote_addrstringoptionalRemote RTP address media is currently sent to.
labelstringoptionalThe stream's a=label value (RFC 4574), for correlating it with external metadata.
contentstringoptionalThe stream's a=content value (RFC 4796), e.g. "main" for original audio and "alt" for a translated feed.
langstringoptionalThe stream's a=lang value (RFC 8866): a BCP 47 language tag such as "en" or "es-ES".
room_idstringoptionalRoom this stream's audio is mixed into. A secondary stream may sit in a different room than its leg.
rolestringoptionalRouting role of this stream within its room. Streams carry their own role, independent of their leg's.
400 Not a SIP leg
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg or stream not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
# PATCH /legs/{id}/role

Change a leg's routing role

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.

Parameters

NameInTypeDescription
idpathstringrequiredLeg ID

Request Body

FieldTypeDescription
rolestringrequiredNew 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).

Responses

200 Updated leg view
FieldTypeDescription
instance_idstringoptionalInstance identifier
idstringrequiredUnique leg identifier (UUID)
typeenumrequiredLeg type
Values: sip_inbound, sip_outbound, webrtc, whatsapp_in, whatsapp_out, websocket_in, websocket_out, moq_in, livekit_publish, livekit_participant
stateenumrequiredLeg state
Values: ringing, early_media, connected, held, hung_up
room_idstringoptionalRoom ID if the leg is in a room, empty otherwise
mutedbooleanrequiredWhether the leg is muted (cannot be heard by others)
deafbooleanrequiredWhether the leg is deaf (cannot hear others)
accept_dtmfbooleanrequiredWhether the leg receives DTMF digits broadcast from other legs in the same room. Defaults to true.
heldbooleanrequiredWhether the call is on hold (SIP legs only)
rolestringoptionalRouting role used by the room's audio routing matrix (e.g. "customer", "agent", "supervisor"). Empty string means unroled (full mesh).
app_idstringoptionalApplication identifier for event stream filtering.
sip_headersobjectoptionalDeprecated: 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.
headersobjectoptionalCustom 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.
400 Invalid JSON
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message
404 Leg not found
FieldTypeDescription
instance_idstringoptionalInstance identifier
errorstringrequiredError message