Self-hosted audio protocol

View as Markdown

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:

wss://<your-host>/<your-path>/<call_id>

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

X-Dial-Signature: t=<unix_seconds>,v1=<hex_signature>

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:

FormatEncodingSample rateNotes
mulaw_8000G.711 μ-law, 8-bit8 kHzDefault. Native telephony; zero-transcode into most realtime voice APIs.
alaw_8000G.711 A-law, 8-bit8 kHz
l16_8000Linear PCM, 16-bit signed little-endian8 kHz
l16_16000Linear PCM, 16-bit signed little-endian16 kHze.g. Gemini Live input.
l16_24000Linear PCM, 16-bit signed little-endian24 kHze.g. OpenAI Realtime / Gemini Live PCM output.

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.