Skip to content

Wire Protocol

The bridge terminates two protocols: the StandIn media bridge’s worker protocol on one side, and the ElevenLabs Agent WebSocket on the other. This page documents both. The contract is identical to the Node.js sibling - the two implementations are interchangeable.

The StandIn media bridge opens one WebSocket per call to {path}/{callId} - the call id is the last path segment of the URL. The upgrade carries two headers:

HeaderValue
X-StandIn-TimestampUnix epoch milliseconds
X-StandIn-SignatureHMAC-SHA256(secret, "{timestampMs}.{callId}"), lowercase hex

The legacy header names X-OpenClawTeamsBridge-Timestamp / -Signature are still accepted; StandIn sends both pairs during the transition.

Verification (401 on failure): the timestamp must be within the freshness window (HMAC_FRESHNESS_MS, default 60 s), the signature must match (constant-time compare), and the (callId, ts, sig) tuple must be single-use (a captured handshake cannot be replayed within the window). The bridge fails closed if the shared secret is unset. The call id is also cross-checked against the session.start body.

GuardValue
Max concurrent connections64 (MAX_CONNECTIONS)
Per-IP cap= total cap (MAX_CONNECTIONS_PER_IP)
Max inbound frame2 MB
Outbound backpressure cap1 MB each way (drops realtime frames above it)
Pre-start timeout10 s (PRE_START_TIMEOUT_MS) - drops a socket that never sends session.start (only a real session.start clears it)
Worker idle timeout90 s (WORKER_IDLE_TIMEOUT_MS) - dead-peer detection: ends the call after 3 missed 30 s heartbeats, freeing the call id and the ElevenLabs conversation. A 30 s WS-level heartbeat catches dead peers earlier.
Duplicate call idrejected with 409 - no second billed conversation for one call

Audio on both sides is base64 PCM 16 kHz, 16-bit, mono.

MessageFieldsBridge action
session.startcallId, threadId, caller{aadId?, displayName?, tenantId?}, recordingStatus?, direction?Open the ElevenLabs conversation; send conversation_initiation_client_data with caller name/tenant/direction. All caller fields are nullable and are defaulted, never sent as null.
audio.frameseq, timestampMs, payloadBase64, speakerName?Forward payload verbatim as user_audio_chunk. In group calls, a changed speakerName becomes a rate-limited contextual update.
video.framesource (camera/screenshare), ts, width, height, mime, dataBase64, participantId?, participantName?Buffer the latest frame per source, in memory, for the on-demand look tool. Unknown sources are ignored.
participantscountcontextual_update (“N humans on the call, stay quiet unless addressed”).
dtmfdigitcontextual_update (“the caller pressed {digit}”).
pingtsReply pong with the same ts.
recording.statusstatusGate what may be persisted (transcripts, path-1 uploads).
assistant.saytextGovernor goodbye: speak it, then StandIn tears the call down.
session.endreasonClose the ElevenLabs socket, tear down.
MessageFieldsMeaning
audio.frameseq, timestampMs, payloadBase64Agent audio for the Teams side.
assistant.cancelturnIdBarge-in: flush queued playback on the Teams side.
expressionemotionAvatar emotion cue (from the agent’s express tool).
display.imagedataBase64, mime, mode?, caption?, …Show an image on the bot’s video tile (from show_image).
pongtsReply to a worker ping.
session.endreasonAsk StandIn to tear the call down (governor, agent end_call, or fatal error).
ElevenLabs messageDirectionBridge behavior
conversation_initiation_client_databridge → ELSent once at call start: dynamic_variables, optional first_message override, user_id, branch_id, environment.
conversation_initiation_metadataEL → bridgeCaptures conversation_id; validates agent_output_audio_format / user_input_audio_format are pcm_16000 (else the call ends).
user_audio_chunkbridge → ELCaller audio, verbatim.
audio (audio_event.audio_base_64, event_id)EL → bridgeAgent audio → audio.frame. Dropped if event_id is at or below the last interruption (ghost drop).
interruption (event_id)EL → bridgeEmit assistant.cancel and set the ghost-drop floor to that event_id.
ping (event_id)EL → bridgeReply pong with the event_id (keeps the socket alive).
contextual_updatebridge → ELNon-interrupting context (participants, dtmf, speaker).
user_messagebridge → ELThe goodbye fallback when no deterministic TTS voice is set.
client_tool_call / client_tool_resultbothAgent tools: look, show_image, express, end_call. See Vision and Tools.

The signed URL for a private agent is minted per call at session.start (never cached at boot) and refreshed on failure.