docs/asyncapi/voice.yaml
in the API repo, and this page tracks it.
The flow at a glance
- Create or reuse an assistant session (REST, same as text).
- Mint a single-use ticket (REST, authenticated).
- Open the WebSocket with the ticket (no Bearer header on a raw
new WebSocket(), which is exactly why the ticket exists). - Say hello, get ready, then stream microphone audio and handle server frames.
Audio format
Send small frames continuously (20 ms frames work well). The server does the
end-of-speech detection; you never decide when an utterance is finished.
Client to server messages
Server to client messages
The engine field and live mode
ready.engine is either "relay" or "live" (absent means "relay", older servers
do not send it). The frame protocol is identical in both modes. A client written
against this page works on both. Two timing rules matter, and both are safe to follow
unconditionally:
- Render transcripts by
turn_no, never by arrival order. In live modetranscript.usercan arrive after the answer’saudio.begin, because transcription streams while the model is already speaking. - Never string-match
turn.message. In live mode it is the transcript of what the assistant actually said, phrased naturally, not a fixed template.
Barge-in, the part integrators get wrong
The user is allowed to interrupt the assistant mid-sentence. The full sequence:- User starts talking while audio is playing.
- Server sends
audio.cancelfor thatturn_no. - You stop playback immediately, flush the buffer, and send
playback.interruptedwith the milliseconds actually played. - The conversation continues; the transcript records only what was really heard.
Close codes
Billing
Voice turns meter into the same seconds bucket as everything else. Eachturn frame
carries billed_seconds, and metered frames surface the remaining balance so you
can warn the user before the tank runs dry.
Sandbox
sk_test_ sessions run the full protocol, including endpointing and frames, but
never call a vendor and never bill. Ideal for integration tests.