Self-hosted audio protocol
Self-hosted audio protocol
In Self-Hosted audio mode (config.type: "audio" on the
Self-Hosted configuration), Dial connects the raw call
audio to a WebSocket server you host. Where the LLM protocol
sends you transcripts and asks for text turns — with Dial running speech-to-text and text-to-speech —
the audio protocol hands you the sound itself, both directions. Your server brings the entire voice
stack: a speech-to-speech model (OpenAI Realtime, Gemini Live, …) or your own STT → LLM → TTS chain.
These pages are the complete contract — you can build a conformant server from them alone. The SDKs ship a ready-made server helper (signature verification, frame codec, keepalive) so you start from handlers, not from a WebSocket loop.
Connection
Dial opens one WebSocket per call to your configured URL with the call id appended to the path:
call_id is Dial’s own call identifier — stable for the life of the call, and the only call
identifier you ever receive. If the connection drops mid-call, Dial reconnects to the same
path and sends a fresh call_connected
with reconnect: true (see Reconnection).
All frames are JSON text, discriminated by a type field, snake_case throughout. Audio rides
inside media frames as base64.
Authentication
Identical to the LLM protocol: every connection carries
where hex_signature is HMAC-SHA256(secret, "<t>.<call_id>") as a hex digest, with the signing
secret from your Self-Hosted configuration. Verify on connect with a constant-time comparison and
reject stale timestamps. The SDK helpers do this for you.
Audio formats
The audio encoding in each direction is set on your
Self-Hosted configuration and echoed in every
call_connected frame — there is no
in-band renegotiation. Both directions support the same five formats:
All audio is mono. Dial sends caller audio in your inbound format in ~20 ms frames; you send agent audio in your outbound format, any chunking (Dial buffers and paces playback).
Message types
From Dial → your server:
From your server → Dial:
Both ways: ping_pong — the keepalive:
Dial pings every ~2 s; echo it back. Miss enough pongs and Dial treats the link as dead and
reconnects.
Reconnection
If the socket drops mid-call, the call itself stays up: Dial discards caller audio while
disconnected (never replays it — the conversation is live), and re-dials your endpoint with
backoff for up to ~15 seconds. The new connection starts with
call_connected carrying
reconnect: true — restore your per-call state keyed by call_id and resume streaming. If
reconnection fails past the budget, the call ends and your Call record reflects it. After you send
end_call, reconnect attempts for that
call are refused.