Self-Hosted

View as Markdown

Self-Hosted mode lets you bring your own agent infrastructure. Instead of Dial’s managed agent driving a call, Dial connects to a WebSocket server you host. It comes in two variants — one is active at a time, for all of your account’s calls:

  • LLM ("type": "llm") — Dial still handles the hard parts of voice: telephony, speech‑to‑text, text‑to‑speech, turn‑taking, and interruptions. Your server only answers one question, live: given the conversation so far, what should the agent say next? It speaks the Self-hosted LLM protocol — transcripts in, text turns out.
  • Audio ("type": "audio") — Dial pipes the raw call audio to your server, full duplex, over the Self-hosted audio protocol. You bring the entire voice stack: a speech-to-speech model (OpenAI Realtime, Gemini Live, …) or your own STT → LLM → TTS chain. Audio formats are configurable per direction (G.711 or linear PCM at 8–24 kHz).

Either way, no voice-infrastructure details leak through — your server sees clean Dial-native protocols and Dial call ids only.

How it works

  1. You host a WebSocket endpoint and configure Self-Hosted with its wss:// URL and variant.
  2. For each call, Dial opens a socket to wss://<your-url>/<call_id>, signed so you can verify it’s from Dial.
  3. LLM: Dial streams the live transcript and asks for the agent’s turns; your server streams back the words to speak. Audio: Dial streams the caller’s audio; your server streams back the agent’s audio.

While Self-Hosted is on, it drives all of your account’s calls — inbound and outbound.

Enable it

Set it up from the dashboard Self-Hosted page, or via the API. LLM and audio each have their own independent config — configure either without affecting the other.

Every change is a single POST with one action: save (persist a mode’s config; mints that mode’s signing secret on the first URL), activate (save the config, then enable + route calls through that mode — enable or switch), or disable.

$# save the LLM mode's config
>curl -X POST https://api.getdial.ai/v1/self-hosted \
> -H "Authorization: Bearer $DIAL_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "action": "save", "config": { "type": "llm", "wsUrl": "wss://my-agent.example.com/dial" } }'
>
># …or the audio mode's config
$curl -X POST https://api.getdial.ai/v1/self-hosted \
> -H "Authorization: Bearer $DIAL_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "action": "save", "config": { "type": "audio", "wsUrl": "wss://my-voice.example.com/dial",
> "audioInboundFormat": "mulaw_8000", "audioOutboundFormat": "mulaw_8000" } }'

save never changes which mode is active or whether Self-Hosted is on — it just persists that mode’s config. Each mode has its own wsUrl and signing secret.

Copy that mode’s signing secret — from the dashboard, or GET /api/v1/self-hosted/secret?mode=llm — and wire up your server’s signature verification with it (each mode has its own secret). Then activate: the body is exactly a save (the mode’s config), and it also enables Self-Hosted and routes all your calls (inbound and outbound) at that mode’s server immediately — so make sure it’s ready:

$curl -X POST https://api.getdial.ai/v1/self-hosted \
> -H "Authorization: Bearer $DIAL_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "action": "activate", "config": { "type": "llm", "wsUrl": "wss://my-agent.example.com/dial" } }'

To switch modes, activate the other one — its config comes along in the same body (affects new calls only). To turn Self-Hosted off, disable:

$curl -X POST https://api.getdial.ai/v1/self-hosted \
> -H "Authorization: Bearer $DIAL_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "action": "activate", "config": { "type": "audio", "wsUrl": "wss://my-voice.example.com/dial",
> "audioInboundFormat": "mulaw_8000", "audioOutboundFormat": "mulaw_8000" } }'
$
$curl -X POST https://api.getdial.ai/v1/self-hosted \
> -H "Authorization: Bearer $DIAL_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "action": "disable" }'

Verify every connection

Each connection carries an X-Dial-Signature header (HMAC‑SHA256 over <timestamp>.<call_id> with your signing secret) — the same scheme in both variants. Your server should verify it and reject stale or mismatched signatures. The SDKs ship a helper for exactly this, plus the full protocols as ready-made schemas and server helpers — start there rather than hand‑rolling the message types.

Reference servers

Don’t start from scratch. The OpenAI WebSocket server in the Dial playbooks repo is a minimal, runnable LLM-variant server. It verifies the X-Dial-Signature, drives each turn with the OpenAI SDK, echoes the ping_pong keepalive, and focuses on transcript interrupts — cancelling the in‑flight reply when the caller speaks again so the agent never talks over them. Run it, expose it with ngrok, and point Self-Hosted at the tunnel’s wss:// URL:

$git clone https://github.com/GetDial-AI/playbooks
$cd playbooks/self-hosted/openai-node
$npm install
$cp .env.example .env # OPENAI_API_KEY + your Dial signing secret
$npm start # listens on :8080

While Self-Hosted is on, managed features do not apply: built‑in agent tools and any Context MCP connections are inert, because the agent driving the call is yours. In the audio variant this extends to everything voice: transcripts, recordings, and call summaries are not produced — Dial never hears the conversation. Turn Self-Hosted off to return to the managed agent.

Next steps

  • Self-hosted LLM protocol — the LLM-variant WebSocket contract: connection, signing, messages, streaming, interrupts, and resumability.
  • Self-hosted audio protocol — the audio-variant contract: formats, media frames, barge-in, checkpoints, and reconnection.
  • SDKs — protocol schemas, server helpers, and the signature helper for Node and Python.