Reference
WebSocket protocol
Every event the deployment runtime emits and consumes. Use this page if you're building your own UI without the widget (for example, embedding voice into a desktop app, a React Native client, or a server-side LiveKit bridge).
Connecting
Open a standard WebSocket to wss://<your-voice-orch>/v1/agents/<publicId>/ws?token=<connectToken>. The token is the same one your /api/voice-token handler mints: short-lived, single-use, scoped to one session. The runtime closes the connection with a 1008 policy-violation code if the token's deployment_id, public_id, or agent_version_id don't match, or if it's been replayed.
const { connectToken, websocketUrl } = await fetch('/api/voice-token', {
method: 'POST',
}).then((r) => r.json());
const url = new URL(websocketUrl);
url.searchParams.set('token', connectToken);
const ws = new WebSocket(url.toString());
ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log(data.type, data);
});Audio format
Mic audio you send up: PCM16, little-endian, 24 kHz, mono, base64-encoded, wrapped in input_audio_buffer.append events. TTS audio you receive: same format, in response.output_audio.delta events. The runtime downsamples to 16 kHz internally for STT and resamples back up for the playback ack, so you don't need to handle sample-rate conversion on your end.
Server → client events
The runtime emits these. Listen in your onmessage handler.
Lifecycle
| Event | Payload (notable fields) | Meaning |
|---|---|---|
platform.ready | voice_session_id, provider, model, voice | Session is wired up. Start sending audio. |
platform.warning | message | Non-fatal issue (e.g. an attached tool isn't supported in this runtime). |
platform.error | error | Fatal. The runtime will close the socket immediately after. |
platform.stt_degraded | severity, reason, retry_after_ms | STT upstream tripped a circuit breaker. severity is open (down) or recovered (back). Use to drive a degraded-mode banner in the UI. |
platform.tts_degraded | severity, reason, retry_after_ms | Same idea, for the TTS provider. |
pong | ts | Reply to a ping. The widget uses it as a keepalive. |
User-turn events
| Event | Payload | Meaning |
|---|---|---|
conversation.item.input_audio_transcription.completed | transcript, item_id | STT finalized one user turn. Use as the canonical user transcript. |
Assistant-turn events
| Event | Payload | Meaning |
|---|---|---|
response.created | response.id | Assistant turn started. Reserve a chat bubble. |
response.output_text.delta | delta, response_id | Streaming text chunk. Append to your UI. |
response.output_text.done | text, response_id | Final text for the turn. |
response.output_audio.delta | delta (base64 PCM16), response_id | Streaming audio chunk. The widget plays these; a custom UI feeds them to a Web Audio source. |
response.output_audio.done | response_id | No more audio for this turn. |
response.output_audio_transcript.done | transcript, response_id | What the assistant actually said. Identical to .output_text.done for the Daisy runtime. |
response.output_audio.cancelled | response_id | Audio for this turn was cancelled mid-flight (typically barge-in). |
response.cancelled | response_id | The entire response was cancelled. |
response.done | response.id, response.status | Turn complete. status is completed, cancelled, or failed. |
Tool & usage events
| Event | Payload | Meaning |
|---|---|---|
platform.tool_started | name, arguments, call_id | A function tool was invoked. Use to show "Booking…" affordances. |
platform.tool_result | name, call_id, result | Tool finished. result.ok is the success flag. |
platform.usage.llm | input_tokens, output_tokens | Per-turn LLM token spend. |
platform.usage.tts | chars | Per-turn TTS character spend. |
platform.usage.turn | Rollup of all of the above for the turn. | Fires once at the end of each turn. Useful for billing telemetry. |
platform.barge_in | response_id, at_ms | The user spoke while the assistant was talking; assistant playback stopped. |
Client → server events
Send these as JSON-encoded WebSocket messages.
| Event | Payload | Meaning |
|---|---|---|
input_audio_buffer.append | audio (base64 PCM16 24kHz mono) | One chunk of mic audio. Send ~50 ms cadence for low-latency turn detection. |
input_audio_buffer.commit | (none) | Mark the buffered audio as a complete user turn. The widget sends this on barge-in. |
conversation.item.create | item.role, item.content[].text | Inject a text turn into the conversation (e.g. a "show me option 2" button). |
response.create | response.modalities | Ask the agent to respond. Required after conversation.item.create; the runtime fires this automatically for audio turns. |
response.cancel | (none) | Stop the in-flight response. |
ping | ts | Keepalive. Server replies with pong. |
End-to-end example: text-only consumer
The simplest possible custom UI: sends typed text, receives assistant text, ignores audio entirely. Useful as a starter for chat-style integrations that don't need mic.
async function startTextSession() {
const { connectToken, websocketUrl } = await fetch('/api/voice-token', {
method: 'POST',
}).then((r) => r.json());
const url = new URL(websocketUrl);
url.searchParams.set('token', connectToken);
const ws = new WebSocket(url.toString());
ws.addEventListener('open', () => {
ws.send(JSON.stringify({
type: 'conversation.item.create',
item: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'Hello, what can you do?' }],
},
}));
ws.send(JSON.stringify({ type: 'response.create', response: { modalities: ['audio'] } }));
});
ws.addEventListener('message', (event) => {
const e = JSON.parse(event.data);
switch (e.type) {
case 'response.output_text.delta': process.stdout.write(e.delta); break;
case 'response.output_text.done': console.log('\n[turn complete]'); break;
case 'response.done': ws.close(); break;
case 'platform.error': console.error('error:', e.error); break;
}
});
}Recommended client design
- Pace audio sends. Send
input_audio_buffer.appendon a steady ~50 ms cadence; bursts cause the server-side VAD to oscillate. - Honor barge-in. When you receive
platform.barge_in, stop scheduling pending TTS audio frames immediately. Frames you've already scheduled will keep playing for ~50 ms, which is expected. - Apply a short crossfade. If you concatenate audio deltas naively, you'll hear clicks at chunk seams. The widget applies ~120 ms equal-power crossfade; the same trick works for any client.
- Reconnect on network blips, not on logical errors. A clean WS close from the server with code 1008 means the session was rejected (bad token, unauthorized origin). Don't auto-reconnect. Reconnect only on 1006 (abnormal closure).
- Keepalive at 15 s. Idle WebSockets get reaped by some proxies after 30–60 s of silence. The widget pings every 15 s; do the same in custom clients.