> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.getdial.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.getdial.ai/_mcp/server.

# Self-hosted LLM protocol

> The WebSocket protocol your server speaks when driving a Dial call in Self-Hosted mode — connection, signing, message types, and resumability.

When [Self-Hosted mode](/documentation/platform/self-hosted) 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](/api-reference/self-hosted-audio-protocol/overview).)

These pages are the complete contract — you can build a conformant server from them alone. The
[SDKs](/documentation/sdks/overview) 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:

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

`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](#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:

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

`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:**

#### [call\_connected](/api-reference/self-hosted-protocol/call-connected)

Sent on each connect (incl. reconnect) — direction, numbers, instruction, language.

#### [transcript\_update](/api-reference/self-hosted-protocol/transcript-update)

The latest full transcript snapshot. Informational.

#### [response\_required](/api-reference/self-hosted-protocol/response-required)

Dial wants the agent to speak — reply with `response` frames.

#### [reminder\_required](/api-reference/self-hosted-protocol/reminder-required)

The user has been silent — optionally nudge.

**From your server → Dial:**

#### [response](/api-reference/self-hosted-protocol/response)

The agent's spoken reply to a request, streamable.

#### [interrupt](/api-reference/self-hosted-protocol/interrupt)

Make the agent speak immediately, out of turn.

**Both ways:**

#### [ping\_pong](/api-reference/self-hosted-protocol/ping-pong)

Keepalive — Dial pings \~every 2s; echo it back.

## Resumability

The connection path is keyed by the stable `call_id`. The
[`ping_pong`](/api-reference/self-hosted-protocol/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`](/api-reference/self-hosted-protocol/call-connected). So you can
restore session state keyed by that id and continue. Dial replays nothing on reconnect, but every
[`response_required`](/api-reference/self-hosted-protocol/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` + `content` only, no word‑level timestamps.
* **Call metadata / runtime configuration** — beyond
  [`call_connected`](/api-reference/self-hosted-protocol/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`.