type field naming the frame.
Frame rules
These rules apply to everything you send:- Send text frames only, each one a JSON object with a
typefield. A binary frame, text that is not a JSON object, a missing or unknowntype, or a required field that is missing or of the wrong type (for example aGeneratewithouttext) ends the session: the server sends anErrorframe with code3006and closes. - Extra fields you add to a known frame are ignored.
- A WebSocket message over 1 MiB is refused by the WebSocket layer itself, with close code
1009and noErrorframe. The size counts the whole message, so a message sent in several fragments is measured once they are joined. - The server does not negotiate WebSocket compression (
permessage-deflate), so frames in both directions travel uncompressed.
Begin, in the frames below, or in the Error frame that ends the session.
Client to server
Generate
Generate appends text to the session buffer. The server does not always wait for Flush: once the buffer holds enough complete sentences, it starts speaking them under the current flush_id.
- Text that arrives in one
Generatestarts once its complete sentences add up to a long enough stretch of text. - Text streamed over several
Generateframes, as an LLM produces it, starts sooner: once its first sentences are complete, or at a clause end (,,;or:) when a long sentence has none yet. The rest of the request follows in parts that start early enough that the audio already sent does not run out while text is still arriving.
Flush and Cancel. A sentence ends once whitespace follows its ., ! or ?, so the end of your text so far does not count until the next text, or a trailing space, arrives; a period after an abbreviation or an initial, as in Dr. or J., does not end a sentence. A sentence of one word, such as Sure. or a list number such as 4., is never spoken on its own before Flush; it is read with the sentence after it. How much text the server gathers before it starts speaking can change, so do not rely on it: Flush is what concludes a request.
Short text is buffered, not spoken. A session that sends a short Generate and never flushes therefore stays silent, looking perfectly healthy, until the idle timeout closes it with 3006 two minutes later, and the leftover text is never spoken. Only Flush (or Terminate) guarantees that everything you sent is spoken and gives you the FlushDone boundary.
Text from consecutive Generate frames is joined exactly as sent, with no separator added. Good followed by morning. is spoken, and reported in WordBoundaries, as the single word Goodmorning.. Include the space or newline yourself when you split text across frames.
A single Generate accepts up to 2000 characters, counted as Unicode code points. Over that, the session closes with 3006. Empty text is accepted and does nothing, but it counts toward the control-frame limit of 20 per second, like Flush, KeepAlive and Cancel.
Flush
Flush concludes the current request: whatever is still unspoken is synthesized, then a FlushDone arrives. For short text this is when audio starts. For longer text the server may already be streaming, and Flush synthesizes the remainder.
FlushDone means the request’s audio has been fully delivered to your socket, not that it has finished playing. Synthesis runs several times faster than real time, so FlushDone usually lands well before playback ends.
A Flush with nothing left to speak still returns exactly one FlushDone, with audio_duration_ms of 0 and no Audio frames. The flush_id still advances and the session stays usable.
You do not have to wait for one FlushDone before sending the next Flush. Queued requests are answered in flush_id order. The server works through them one at a time, so each request you queue adds to the wait before its own audio starts.
When you are streaming text out of an LLM, flush on sentence or clause boundaries. Flushing per token produces choppy prosody, because each fragment is synthesized on its own and complete sentences give the model the context it needs. It also costs you the session: Flush is a control frame, capped at 20 per second sustained with a burst allowance of about 60, and a per-token loop quickly exceeds that and is closed with 3010. See Limits.
Cancel
Cancel is for barge-in, when the listener starts talking over the speech. It stops the in-flight synthesis and discards everything you have sent but not yet heard: text sent with Generate but not yet flushed, and every Flush still waiting its turn. None of them gets a FlushDone. The server answers with a Cancelled frame:
audio_duration_seconds is the audio that was streamed for the cancelled request only, in seconds rounded to 3 decimal places, and 0.0 when nothing was in flight. A Cancel with nothing in flight is still answered, and still advances flush_id. Once the session has reached expires_at the server reads no more frames, so a Cancel sent then gets no Cancelled: the request in flight streams to its FlushDone, and the session closes with 3008. Stop playback yourself in that case.
Three things to design around:
- A short tail of the old audio may still arrive.
Audioframes already on the way when you sentCancelcan land beforeCancelleddoes. EveryAudioframe between yourCanceland itsCancelledbelongs to a request you sent before theCancel: drop it, and stop the buffers you have already scheduled for playback, or they keep playing. EachCancelis answered by its ownCancelled, in the order sent. - The next request takes a new
flush_id. It is higher than every id before theCancel, but it can skip numbers, so read it from the firstAudioorFlushDoneafterCancelledrather than computing it. The nextGenerateandFlushcontinue under that id. The server handles your frames in the order you send them, so aGeneratesent afterCancelis not caught by the cancel; you do not have to wait forCancelledbefore sending the next reply. - Re-send anything you still want spoken. Unflushed text is gone; the next
Flushspeaks only what you send after the cancel.
WordBoundaries. The session totals in Termination still count the cancelled text that had been passed to synthesis and the audio that was delivered, and the output timeline used by WordBoundaries counts that audio too, the tail delivered between your Cancel and the server’s Cancelled included.
KeepAlive
inactivity_timeout you set, and any frame restarts that timer, so you only need KeepAlive while a session is idle: send one at least once per timeout to hold the socket open. It is a control frame and counts against the same 20 per second cap as Flush and Cancel.
Terminate
- Text you sent with
Generatebut never flushed is flushed for you and gets its ownFlushDonewith the nextflush_id. - Audio still in flight is delivered, and any pending
WordBoundariesarrive. - A
Terminationframe carries the session summary, then the socket closes with code1000and reasonSession ended.
Terminate is never read: no error, no audio, and it is not counted in the summary. If the server produces nothing for 30 seconds while a FlushDone is still owed, the session closes with 3005 instead of Termination. That window restarts whenever the server produces output, so a long final request is never cut short.
You can also just close the socket. The server then discards any synthesis still in flight and sends no Termination; the audio it already delivered to you still counts.
Server to client
Begin
Begin is always the first frame, sent before the server reads anything from you. It arrives only after the server confirms it has room for the connection, your credentials are checked, your parameters are validated, your account’s limit of new sessions per minute is checked, and the session’s language and voice are available on its model; if any of those fail you get an Error frame and a close instead. Read configuration as the session you actually got: every connection parameter except token and model_id with its effective value, whether you sent it or not, then what you requested where it differs (word_boundaries_requested), the model the session runs on (model), the session’s lifetime and limits, and the names of any query parameters the server ignored (unknown_params). Read the configuration back describes these keys. The example above is abridged to them; a session with a cloned voice also shows the keys listed below. configuration also carries keys for options these pages do not cover, and it may gain keys over time, so read the keys you need, ignore the others, and do not validate it against a closed set. Quote id in support requests. expires_at is a Unix timestamp in seconds; when it passes, the server stops accepting input, flushes and drains what it has, and closes with 3008.
configuration carries two more keys about the voice. With a cloned voice, language is the voice’s own:
Audio
Audio frame carries audio; the server never sends an empty one. Frame sizes are not fixed, so do not assume a chunk length: at 24 kHz they are about 80 ms each, the last frame of a request included; at other sample rates they vary, and the last frame of a request is often shorter. Audio for one request never interleaves with another’s, and all of a request’s Audio arrives before its FlushDone, but it can begin before you send Flush. See Audio formats on WebSocket API basics.
FlushDone
Flush, including the implicit flush that Terminate performs, in flush_id order. It means every byte of that request’s audio has been sent to you, not that it has been played. audio_duration_ms is 0 when the request had nothing left to synthesize.
To check that you received as much as the server sent, compare audio_duration_ms with bytes_received / (sample_rate * bytes_per_sample) * 1000, where bytes_per_sample is 2 for pcm_s16le and 1 for pcm_mulaw and pcm_alaw.
WordBoundaries
FlushDone and never after Termination. If you have already flushed again it may arrive after the next request’s first Audio frames, so match it to its request by flush_id, not by position. Each entry has word, start, end and confidence. words has one entry for every whitespace-delimited token of the request’s text, in order, with its punctuation attached; a token with no sound of its own, such as a dash standing alone between spaces, gets a zero-length entry (start equal to end, confidence 0) at the edge of its neighboring word. start and end are milliseconds on the session’s continuous audio timeline, counted from the first audio the session delivered, and audio_start_ms is where this request’s audio begins on that same timeline. To place a word within this request’s own audio, subtract audio_start_ms from start; never add the two. A long request gets one frame like any other. The frame is best-effort: a cancelled request gets none, a request whose audio leaves part of its text unspoken gets none, and it can be missing when timing data is unavailable. See Word boundaries.
Termination
session_duration_seconds is a whole number of seconds, truncated. total_input_char_length is every character of your Generate frames that was passed to synthesis, whether or not it was spoken, so text you cancelled or never flushed counts too. Text a Cancel discarded while the server was still holding it back under text intake pacing was never passed to synthesis, and does not count. audio_duration_seconds is the audio actually delivered to you, including the delivered part of a cancelled request, in seconds rounded to 3 decimal places.
voice_expires_at is when the consent period of the session’s cloned voice ends, as a Unix timestamp in seconds, after the session’s renewal of the voice’s consent, if any, and null otherwise. Termination may gain keys over time; ignore keys you do not use.
Termination is sent only on a clean finish. If the session fails you receive an Error frame and a close code instead, and if you close the socket without Terminate no summary is sent.
Error
Sent immediately before every failure close the server initiates.
error_code equals the WebSocket close code that follows, and the close reason is normally the fixed string See Error message for details, so the frame is where the detail lives. If the Error frame could not be delivered, for example to a client that stopped reading, the close reason carries a shortened copy of its text instead. There is no Error frame for a clean 1000 close, nor for closes raised by the WebSocket layer itself: 1009 for an oversized message, 1011 when pings go unanswered, 1002 or 1007 for a frame that breaks the WebSocket protocol or is not valid UTF-8, or 1006 when the connection drops. See Error handling.
Frame order
A single short request through a session looks like this. Client frames are on the left, server frames on the right:Begin arrives once, as the very first frame. flush_id starts at 0 and each request you conclude with Flush takes the next id, so a session that handles several requests can use it to keep their audio apart. After a Cancel the next id can skip numbers.
Two things the single-request picture hides:
WordBoundariestrails itsFlushDone. With several requests in flight it often lands after the next request’s firstAudioframes.Audioframes themselves never interleave across requests.- Audio can start before
Flush. Once enough complete sentences are buffered (see Generate), the server begins streaming them;Flushthen completes the request and produces theFlushDone.
Generate and Flush groups before the first FlushDone arrives. This is the real order for three requests flushed back to back, with runs of Audio frames collapsed:
Termination the socket closes with 1000. The Audio counts and the exact position of each WordBoundaries differ between runs. The program on Word boundaries prints this sequence for a session of your own.
Frames that do less than they look like
KeepAlive is not a warm-up. It restarts the idle timer and nothing else. It does not reduce latency, does not reserve anything, and sending it in a tight loop trips the control-frame rate limit and closes the session with 3010. If you want a request to be fast, hold the socket open, with a KeepAlive whenever two minutes might otherwise pass without a frame; that is what removes the setup cost.
There is no warm or prewarm endpoint. The only client-side warm-up that exists is connecting early and holding the socket. See Optimizing accuracy and latency for what that saves.
Related
- WebSocket API basics: configuring the session before any of these frames, and decoding and playing
Audioframes (Audio formats). - Word boundaries: using
WordBoundariestimings for highlighting. - Error handling:
Errorframes and close codes. - Limits: the caps these frames are subject to.