Self-hosted LLM protocol
Self-hosted LLM protocol
When Self-Hosted mode is on in its LLM variant
(config.type: "llm"), Dial drives every call by opening a WebSocket to a server you host and
exchanging the messages documented in this section. Your server is the brain of the call: Dial
streams it the live transcript and asks for the agent’s next turn; your server streams back what
the agent should say. (For the raw-audio variant, see the
Self-hosted audio protocol.)
These pages are the complete contract — you can build a conformant server from them alone. The SDKs ship this protocol as ready-made schemas (zod for Node, pydantic for Python) plus a signature‑verification helper, so you don’t have to hand‑write the message types.
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. It is stable for the life of the call, so you can
key per‑call state on the path and resume cleanly if the socket reconnects (see
Resumability). It is the only call identifier you ever receive — Dial’s underlying
voice infrastructure is never exposed.
All frames are JSON text, discriminated by a type field.
Authentication
Every connection carries a signature header so you can confirm it is genuinely from Dial:
hex_signature is HMAC-SHA256(secret, "<t>.<call_id>") as a hex digest, where secret is the
signing secret Dial generates when you first set your Self-Hosted URL (copy it from the dashboard or
GET /api/v1/self-hosted/secret). Verify it on connect and reject stale
timestamps (e.g. older than a few minutes) and any mismatch — use a constant‑time comparison. The
SDK helpers do this for you: verifyDialSignature(secret, header, callId) (Node) /
verify_dial_signature(secret, header, call_id) (Python).
Message types
Every frame is a JSON object with a type field that identifies it. Field names use snake_case
(e.g. call_id, response_id, content_complete).
From Dial → your server:
response frames.From your server → Dial:
Both ways:
Resumability
The connection path is keyed by the stable call_id. The
ping_pong keepalive lets either side detect a
dropped link quickly; when that happens Dial reconnects to the same <your-url>/<call_id> and
sends a fresh call_connected. So you can
restore session state keyed by that id and continue. Dial replays nothing on reconnect, but every
response_required carries the full
transcript, so the latest one is enough to resume the conversation.
Not in this protocol (today)
To keep the surface small, the current protocol intentionally omits:
- Tools / function calling — the self‑hosted agent runs without tools. Use your own logic inside your server.
- Per‑word timing — transcripts carry
role+contentonly, no word‑level timestamps. - Call metadata / runtime configuration — beyond
call_connected, there is no metadata channel.
Transport reconnection is handled by Dial — on a dropped link it reconnects and re-sends
call_connected, so the only keepalive you implement is echoing ping_pong.