Voice frame protocol

What goes over the wire between a web page and a Leadlock voice agent. The hosted script speaks it for you, so most sites never touch it. Read this to debug a site or build your own client.

The voice is speech-to-speech end to end: the page streams microphone audio up and plays the agent's audio back. Every frame is a JSON text message with a type field. Both sides ignore a type they do not know, so /v1/ only ever changes additively. A breaking change would ship as /v2/.

URL and query

SurfaceURL
Demo page, website script (default)wss://leadlock-app.onrender.com/d/{slug}/voice
Share linkwss://leadlock-app.onrender.com/share/{slug}/voice

The script opens the socket when the visitor clicks, then at once sends context and talk_start.

Script tag

<script src="https://app.leadlock.ai/v1/leadlock-voice.js" data-leadlock-slug="YOUR_DEMO_SLUG"></script>

The MCP tool get_voice_doors returns this exact tag for an agent's demo. The script also reads the demo's saved look with one GET https://leadlock-app.onrender.com/d/{slug}/look request; that is the only other request it makes. Script source: app.leadlock.ai/v1/leadlock-voice.js.

Audio format

Client to server frames

TypeFieldsMeaning
contextvariables (object of strings)Page variables for the agent, sent before talk_start. The script sends current_page, and for a returning visitor visitor_name, returning_visitor and last_interest.
talk_startnoneThe visitor wants to talk: the session is admitted and the call starts. Until this arrives the socket is warm and nothing is billed. A first audio frame also counts as talk_start (that one frame is dropped).
audioaudio (base64 PCM16, 24 kHz mono)Microphone audio, streamed continuously.
stopnoneEnd the call. Before talk_start it abandons the warm socket.
host_tool_resultcall_id, result (any JSON)The page's answer to a host tool function_call. Echo the call_id from that frame. An unknown or already answered call_id is ignored.

Server to client frames

TypeFieldsMeaning
session.readyagent (agent name), sampleRate (number)The voice session is set up. Use sampleRate for playback.
audioaudio (base64 PCM16 mono)Agent audio. Queue chunks back to back for gapless playback.
transcriptrole (user or assistant), delta or text, timestampCaption text: delta is a partial piece, text a finished line. The script emits it as its transcript event.
transcript_flushrole, optional timestampThe current caption line for that role is finished.
interruptmessageThe visitor talked over the agent: stop and clear any queued agent audio now.
function_callfunction, arguments, timestamp; host tools add call_id and tool_kind: "host"; MCP tools carry tool_kind: "mcp"The agent called a tool. Page actions (web_*) and host tools run in the page; MCP and server tools are only reported.
function_resultfunction, result, success, timestamp; host and MCP tools add tool_kind and call_idThe outcome of a tool call. Informational; the script ignores it.
session_endreason, optional message, optional durationThe call ended on the server side. Reasons include ai_requested (the agent hung up), time_limit (with duration in seconds), silence_timeout and replaced. End quietly.
errormessage or error (text), optional codeSomething stopped the call. Show message when present. Codes include busy (always followed by close 1013), TRIAL_MINUTE_CAP_REACHED, CALL_VETOED_SUB_ACCOUNT_PAUSED and CALL_VETOED_SUB_ACCOUNT_INACTIVE.

Close codes

CodeWhenWhat the script does
1013Busy: the demo, the account or this visitor's address already has its full number of live calls. Sent right after an error frame with code: "busy".Shows "This demo is busy right now. Please try again in a minute."
4001Quiet eviction of a warm socket (reason evicted) to make room for a newer one. No error frame.Goes back to idle with no message. The next click reconnects.
1008The page's Origin is not on the demo's allowlist. The server closes before accepting, so the browser never sees 1008: it sees the handshake fail with HTTP 403 and the socket closes without opening.Shows "Voice chat is not available on this page right now." The same message covers an unreachable server.
1000Normal close.Ends quietly.
1001Going away (page or server shutting down).Ends quietly.
1005Closed with no status code.Ends quietly.

Any other close after the socket opened means the connection was lost: the script shows "Could not connect. Please try again."

Origin rules

Warm sockets and caps

A socket is warm from the handshake until talk_start, and live after it. Only live sockets are billed.

Host tools

A page can give the agent its own actions with LeadlockVoice.registerTool(name, schema, handler). The model waits for the handler's real result, unlike the 8 built-in page actions (web_navigate, web_scroll, web_scroll_to, web_highlight, web_clear_highlight, web_fill, web_click, web_remember), which are fire and forget. Tools are declared in the socket URL as ?host_tools= because the voice session is configured before the first frame is read.

Host tools work on agents that run on the Grok, OpenAI Realtime or Gemini Live voice models; other voice models never receive them, with no error. A host tool whose name matches one of the agent's own tools or MCP server labels is dropped.

LimitValue
Raw host_tools query value8192 characters, checked before decoding
Tools per page16
Tool name^[A-Za-z][A-Za-z0-9_]{0,63}$
Reserved name prefixesweb_, x_, file_search, collections_search
Description1024 characters
parametersJSON schema with "type": "object"
All declarations together6144 characters of compact JSON
Wait for a result10 seconds, then the model gets {"success": false, "error": "timeout"}
One result8192 characters of compact JSON, else {"success": false, "error": "result_too_large"}

A bad declaration is dropped, never fatal: a broken host_tools value just means no host tools.

Round trip

  1. The agent calls a host tool. The server sends function_call with a fresh call_id and tool_kind: "host".
  2. The page runs the handler and sends {"type": "host_tool_result", "call_id": "...", "result": ...}. A handler that throws sends {"success": false, "error": "<message>"} as the result.
  3. The server passes the result to the agent and sends function_result with the same call_id.

The handler runs in the visitor's own browser, so a host tool can do nothing the page could not already do.

For agents

The same reference is in Markdown at the end of llms-full.txt, and llms.txt lists every agent-readable page.