Dialt
Get API key

Developer guide

WebSocket API

The advanced path for applications that need direct protocol control. Your client owns audio conversion, pacing, playback, echo cancellation and reconnects.

← Choose another integration

Use the generated realtime reference for a message index or download the AsyncAPI contract.

WebSocket API

Connect to wss://dialt.com/ws. Trusted non-browser clients may authenticate with Authorization: Bearer ck_…; all clients may include api_key in their first JSON frame.

Start frame

{
  "type": "start",
  "session_id": "your-unique-session-id",
  "api_key": "ck_or_scoped_key",
  "audio": { "sr": 16000 },
  "mode": { "kind": "dialt", "web_search": false, "end_call": false },
  "client": {
    "user": "optional-stable-user-id",
    "timezone": "Europe/Dublin",
    "capabilities": []
  }
}
FieldRequiredDescription
typeYesMust be start.
session_idYesYour 1–64 character correlation ID; must match a scoped key.
api_keyIf no auth headerPersistent or scoped credential.
audio.srVoice onlyInput sample rate. Use 16000. Omit the entire audio object for text.
modeYesConversation configuration.
clientNoUser metadata and implemented protocol capabilities.

For text mode, set mode.modality to "text" and omit audio. Text mode uses the WebSocket transport only. The model, instructions, tools, greeting, history and event lifecycle are unchanged; only the media pipeline is absent.

Audio frames

DirectionEncodingRecommended frame size
Client → DialtBinary PCM16 little-endian, mono, 16 kHz20–100 ms, paced in real time
Dialt → clientBinary PCM16 little-endian, mono, 16 kHz by default; negotiable via start.audio.output_encoding / output_srPlay in arrival order

The ready frame states the negotiated downlink format explicitly in its audio field (e.g. {"output_encoding": "pcm16", "output_sr": 16000}). Assert on it rather than assuming an encoding. Note the browser and Python SDKs convert downlink audio and hand you Float32 samples; the wire itself carries PCM16 unless you opt into pcm_f32le.

JSON and binary frames share the same socket. A normal reply is turn, zero or more binary audio frames, utterance, then done. User transcripts arrive as asr.

The agent can end a session only when the start frame set mode.end_call: true. That declares the managed tool end_call(farewell), listed in tools_config like web_search. When the agent calls it, the turn speaks only the farewell (utterance, then done), followed by session_end_requested carrying the same farewell. Headless relays should stop automatic turn injection when that advisory frame arrives. The server then waits a short grace period (3 s by default) in which user speech cancels the end, and otherwise closes with code 1000 and reason ended by model. Without the flag the agent cannot end the session: the host ends it with the wrap_up control frame or by closing. The name end_call is reserved while enabled, like web_search; a client tool by that name is then rejected with invalid_tools.

Client control frames

FramePurpose
{"type":"input_text","text":"Hello"}Commit one user turn in text mode.
{"type":"reset"}Clear conversation context.
{"type":"set_voice","voice":"key"}Change voice for the next reply.
{"type":"tool_result","id":"…","content":{…}}Resolve a tool call.
{"type":"tool_deferred","id":"…","handle":"…"}Detach an eligible call from its voice turn.
{"type":"tool_progress","id":"…","note":"…"}Report non-final tool progress.
{"type":"tool_cancel","id":"…"}Cancel tool work.
{"type":"client_event","event":"playback_stopped",…}Report actual speaker playback after an interruption.
{"type":"client_event","event":"playback_report",…}Per-reply playback health: underruns, starved_ms, max_gap_ms.

Playback obligation

Clients that play assistant audio must track queued and discarded audio. After interrupted, report how much audio was not heard:

{
  "type": "client_event",
  "event": "playback_stopped",
  "remaining_ms": 150,
  "discarded_ms": 420,
  "barge_seq": 3
}

client.audio_frontend is optional diagnostic metadata describing the active microphone/AEC path; use unknown until it is known.

Echo barge_seq from the interruption. Dialt may respond with a corrected utterance carrying the same sequence; replace the earlier assistant transcript for that interruption. The browser SDK handles this playback protocol automatically. Python and raw WebSocket clients must implement it when they perform real playback.

Client tools

Declare tools in mode.tools. Tools are available in Dialt mode and may be combined with web_search. The model chooses whether to answer directly, search, or call one or more client tools.

"tools": [{
  "name": "lookup_order",
  "description": "Look up an order by its customer-visible ID.",
  "parameters": {
    "type": "object",
    "properties": { "order_id": { "type": "string" } },
    "required": ["order_id"]
  },
  "read_only": true,
  "requires_permission": false,
  "expected_duration": "instant",
  "status_label": "order lookup",
  "timeout": 30,
  "deferred": true,
  "deferred_timeout": 7200,
  "notify_on_complete": true
}]
FieldDefaultContract
nameRequiredUnique, non-empty tool name.
description""Tells the model when and how to use the tool.
parametersEmpty object schemaJSON Schema for args.
read_onlyfalsetrue lets Dialt start the call early for speed. Use it only for harmless, cheap lookups. Leave it false for anything that changes data, sends something, spends meaningful money or accesses sensitive data.
requires_permissionfalseHold the exact call until the caller clearly approves it. Dialt asks naturally, verifies the reply and executes nothing when approval or scope is unclear.
expected_durationLearnedWhat the caller should hear. "instant": a fast lookup (order status, account balance); the caller hears the answer directly. "seconds": anything that takes more than about a second (take a payment, cancel a booking); the assistant acknowledges first, then answers when the result arrives. "long": agent or batch jobs. Omit it and Dialt learns from observed results: a new tool is acknowledged first and holding for the answer is earned by fast results. A wrong "instant" is corrected the same way.
status_labelTool nameShort caller-safe label Dialt may use when the caller asks about pending work. Do not put arguments, internal identifiers or secrets here.
timeout30 secondsResult deadline; minimum 1 second, maximum 600 seconds.
deferredfalseAllow the host to release the current voice turn while keeping this call registered as a background job.
deferred_timeout7200 secondsJob lifetime after an accepted defer, up to 24 hours and no shorter than timeout.
notify_on_completetrueQueue one proactive completion when the floor is free. The result enters context either way.

mode.tool_choice restricts tool use with the familiar vocabulary: "auto" (default), "none" (client tools withheld; broker protocol tools like job cancellation stay available), "required", {"allowed": [names]}, or {"tool": name}. Forced modes constrain the first planning round of each user turn; later rounds are unconstrained so the reply can still be spoken. Change it mid-session with {"type": "set_tool_choice", "tool_choice": ..., "one_shot": true?}. It applies from the next reply, one_shot reverts after one user turn, unknown names are rejected with invalid_tool_choice, and a mid-session set_tools resets the choice to "auto".

When requires_permission is true, the first call is held inside Dialt and is not sent to your host. A clear approval releases the original name and arguments exactly once; refusal discards it; ambiguity remains pending for clarification. Changed arguments create a new permission request. Pending permission IDs are one-time, session-scoped and expire after five minutes by default. Permission questions and acknowledgements are generated naturally from the conversation.

Dialt calls the host:

{"type":"tool_call","id":"turn3-fc1","name":"lookup_order","args":{"order_id":"A123"}}

Complete exactly once with a result or client cancellation:

{"type":"tool_result","id":"turn3-fc1","outcome":"succeeded","verified":true,"content":{"speak":"The order shipped.","data":{"status":"shipped"},"handle":"order-A123"}}
{"type":"tool_cancel","id":"turn3-fc1"}

For longer work, progress does not resolve the call or interrupt playback:

Or acknowledge an eligible call as deferred before its initial deadline:

{"type":"tool_deferred","id":"turn3-fc1","handle":"cc-task-123","status_label":"Claude Code task"}

An accepted tool_deferred_ack releases the voice turn and starts the longer deferred deadline. Progress, cancellation, partials and exactly one terminal result may then use the original ID or handle. On ordinary WebSocket resume, tool_deferred_resume re-announces jobs so the host can associate its local worker. Barge-in does not cancel them.

{"type":"tool_progress","id":"turn3-fc1","note":"checking the carrier"}

A running call that needs a mid-call decision raises it with a tool_partial_result carrying interaction (a stable id, a prompt, optional options); Dialt asks by voice at the next opportunity and acks the lifecycle over tool_job_narration (queued/started/superseded/cancelled/failed/resolved, with interaction_ids). The host can close an open interaction without completing the call: decision made elsewhere, no longer needed, or overtaken by newer intent:

{"type":"tool_interaction_update","id":"turn3-fc1","interaction_id":"overwrite-1","state":"resolved","note":"approved in the IDE"}

Pending or in-flight narration for that ask stops, the agent is told not to act on it, and every update receives a deterministic tool_interaction_update_ack (applied: false carries a stable reason such as already_closed for late or duplicate updates). A caller interrupting the ask's narration does not close the decision; it stays pending and answerable until the host closes it or the call ends. See the AsyncAPI contract for exact payloads.

  • Result content is limited to 16 KiB of compact UTF-8 JSON by default. Oversized results still resolve, but Dialt replaces them with bounded content containing tool_result_truncated, original_bytes and a preview.
  • Progress notes are limited to 500 characters and 12 notes per call.
  • A server tool_cancel asks the host to stop promptly because the result is no longer usable or the caller explicitly asked Dialt to cancel the pending job.
  • Barge-in never cancels tool work. It stops the spoken reply while the tool continues. Dialt exposes the pending job to its model using the declared safe status label; a semantic, explicit request to stop is handled by Dialt's managed cancellation tool. Hosts should not declare a duplicate generic stop tool.
  • Built-in web search always acknowledges first, as an expected_duration: "seconds" tool would.
  • Migrating: wait_for_tool is a deprecated alias: true is expected_duration: "instant", an explicit false is "seconds", omitted learns (an explicit expected_duration wins). Still accepted during alpha, removed at beta, and true no longer forces the reply to wait on its own.
  • Keep verbose logs and artifacts in your own system. Return a short spoken summary, structured data and, when useful, a handle or URL.

Errors, reconnects and limits

detailWhat to do
unauthorizedReplace a missing, unknown, expired or revoked credential.
invalid session_idUse a safe non-empty identifier and match the scoped credential.
insufficient creditAdd credit in API & Billing.
too many concurrent sessionsClose the existing session or wait for it to end.
server busy, try again shortlyRetry with exponential backoff.
server restarting, try again shortlyRetry shortly.
voice pipeline unavailable, try again shortlyRetry with backoff and surface a temporary-service message.
  • Two concurrent sessions per account by default.
  • Each connection is capped at two hours.
  • Voice is $5.00 per connected hour, metered per second with partial seconds rounded up.
  • Text uses the same meter at a 90% discount: committed user and assistant words are converted at 150 words per minute; prompts, tool payloads and eval judges are excluded.
  • An abnormal browser transport loss is retried automatically; the new connection starts a new conversation.
  • A clean server close ends the session and is not automatically reconnected.

To validate a key without opening a billable session, connect and send {"type":"auth","api_key":"ck_…"}. A valid key receives {"type":"ok"} and the socket closes.

WebRTC transport signaling (experimental)

Experimental: the API is stable, but this transport is newly shipped and still being hardened on real networks; ws remains the default and recommended fallback.

The same /ws connection can also negotiate a WebRTC (SRTP over UDP) media path instead of carrying audio directly. Signaling is two steps, because TURN credentials are minted by this server: a client that gathers ICE before it has those credentials can never use TURN.

  1. Open /ws as usual and send the start frame with "transport": {"kind": "webrtc"} and no SDP yet. Requires mode.kind dialt.
  2. The server replies webrtc_ice with STUN/TURN ice_servers.
  3. Only now build the RTCPeerConnection with those servers, gather, and send webrtc_offer with the local SDP.
  4. The server answers webrtc_answer with its SDP. The call then lives on the peer connection; the signaling socket may drop afterward without affecting it.
// server -> client
{"type":"webrtc_ice","ice_servers":[{"urls":["stun:stun.l.google.com:19302"]}]}

// client -> server
{"type":"webrtc_offer","sdp":"v=0..."}

// server -> client
{"type":"webrtc_answer","sdp":"v=0...","ice_servers":[...]}

Once connected, control frames ride a "control" RTCDataChannel, byte-identical to the JSON frames documented above: the same type values, the same fields. Microphone and assistant audio ride RTP media tracks instead of binary WebSocket frames.

Before closing, the server sends {"type":"bye","code":...,"reason":"..."} on the control channel, the WebRTC counterpart of a WebSocket close code.

Clients on this transport must not send client_event/playback_stopped: the server can see what it queued but never sent on the RTP track, so it synthesizes that report itself on a hard-clear interruption.

Platform support

PlatformStatusWhat you provide
Desktop Chrome / EdgeRecommended and production validatedBrowser SDK and scoped credential route.
Desktop FirefoxProtocol support; live validation pendingBrowser SDK and scoped credential route.
Safari / iOS WebKitNot yet a supported production targetContact us before committing to this path.
Python service / telephony / custom hardwareSupported headless transportCapture, pacing, playback and echo cancellation.
Raw WebSocketSupported advanced integrationThe complete media and session lifecycle.
Native desktop / mobile SDKsPlannedNot available yet.