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.

JavaScript
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

EventPayload (notable fields)Meaning
platform.readyvoice_session_id, provider, model, voiceSession is wired up. Start sending audio.
platform.warningmessageNon-fatal issue (e.g. an attached tool isn't supported in this runtime).
platform.errorerrorFatal. The runtime will close the socket immediately after.
platform.stt_degradedseverity, reason, retry_after_msSTT upstream tripped a circuit breaker. severity is open (down) or recovered (back). Use to drive a degraded-mode banner in the UI.
platform.tts_degradedseverity, reason, retry_after_msSame idea, for the TTS provider.
pongtsReply to a ping. The widget uses it as a keepalive.

User-turn events

EventPayloadMeaning
conversation.item.input_audio_transcription.completedtranscript, item_idSTT finalized one user turn. Use as the canonical user transcript.

Assistant-turn events

EventPayloadMeaning
response.createdresponse.idAssistant turn started. Reserve a chat bubble.
response.output_text.deltadelta, response_idStreaming text chunk. Append to your UI.
response.output_text.donetext, response_idFinal text for the turn.
response.output_audio.deltadelta (base64 PCM16), response_idStreaming audio chunk. The widget plays these; a custom UI feeds them to a Web Audio source.
response.output_audio.doneresponse_idNo more audio for this turn.
response.output_audio_transcript.donetranscript, response_idWhat the assistant actually said. Identical to .output_text.done for the Daisy runtime.
response.output_audio.cancelledresponse_idAudio for this turn was cancelled mid-flight (typically barge-in).
response.cancelledresponse_idThe entire response was cancelled.
response.doneresponse.id, response.statusTurn complete. status is completed, cancelled, or failed.

Tool & usage events

EventPayloadMeaning
platform.tool_startedname, arguments, call_idA function tool was invoked. Use to show "Booking…" affordances.
platform.tool_resultname, call_id, resultTool finished. result.ok is the success flag.
platform.usage.llminput_tokens, output_tokensPer-turn LLM token spend.
platform.usage.ttscharsPer-turn TTS character spend.
platform.usage.turnRollup of all of the above for the turn.Fires once at the end of each turn. Useful for billing telemetry.
platform.barge_inresponse_id, at_msThe user spoke while the assistant was talking; assistant playback stopped.

Client → server events

Send these as JSON-encoded WebSocket messages.

EventPayloadMeaning
input_audio_buffer.appendaudio (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.createitem.role, item.content[].textInject a text turn into the conversation (e.g. a "show me option 2" button).
response.createresponse.modalitiesAsk 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.
pingtsKeepalive. 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.

JavaScript
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.append on 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.