Client Messages

The messages you send to the Flux TTS /v2/speak WebSocket — Speak, Flush, Interrupt, Configure, and Close.

All client-to-server traffic on /v2/speak is JSON text frames. You stream synthesis text with Speak, end a turn with Flush (or Interrupt on barge-in), adjust delivery mid-stream with Configure, and shut down with Close. The server replies on a parallel set of Server Messages.

A turn is one complete agent response, bounded by Flush or Interrupt. The server streams a turn’s audio as text arrives; Flush completes the turn and Interrupt cancels it. For how these messages drive turn state, see The Speech Lifecycle.

Speak

Send text to be synthesized into the active turn. The Speak shape is unchanged from /v1/speak. The server tracks the active turn internally and assigns it a speech_id — a server-generated turn identifier, included in SpeechStarted (the start of a turn) and SpeechMetadata (the completion of the turn’s synthesis). Binary audio frames aren’t labeled with the speech_id, but all of a turn’s audio — and only that turn’s audio — arrives between those two messages. Clients do not specify one.

1{
2 "type": "Speak",
3 "text": "Sure, I can help you cancel your subscription."
4}
FieldTypeRequiredDescription
type"Speak"yesMessage type identifier.
textstringyesText to synthesize — see Text handling below.

Streaming LLM tokens

Streaming is just sending Speak messages as tokens arrive, then flushing at the end of the turn:

1{"type": "Speak", "text": "Sure, "}
2{"type": "Speak", "text": "I can "}
3{"type": "Speak", "text": "help you "}
4{"type": "Speak", "text": "cancel your "}
5{"type": "Speak", "text": "subscription."}
6{"type": "Flush"}

You may send Speak at any time. Text sent while the current turn is still generating appends to that turn. Once you Flush, though, the turn is closed — a later Speak starts a new turn, which the server queues as pending behind any turn still being synthesized. Only Flush ends a turn.

Text handling

Send plain text. The server applies text normalization (for example, expanding numbers and dates) before synthesis, but it does not reorder your content or insert or strip whitespace between successive Speak messages — so you can stream LLM tokens directly without coordinating chunk boundaries.

Insert whitespace between distinct generations. Because the server doesn’t add whitespace between Speak messages, sending "Hello world." immediately followed by "How are you?" is processed as "Hello world.How are you?", which can trigger sentence-boundary artifacts. When you concatenate separate LLM responses (a reply, a tool-call result, another reply), insert a single space — or the appropriate separator for non-whitespace languages — between them.

Markup handling

The model synthesizes plain text — SSML and competitor audio tags aren’t interpreted. Rather than reject the connection or pass markup through (which causes artifacts), the server strips a defined set of known markup patterns and continues synthesis; the WebSocket stays open. The detector matches a closed list — W3C SSML core elements and vendor namespaces, ElevenLabs v3 bracketed audio tags (a curated allowlist), and Cartesia Sonic-3 inline tags. Anything outside that list (Markdown, HTML, custom XML) is forwarded verbatim.

For each Speak that contains detected markup, the server strips it, synthesizes the cleaned text, and emits one Warning with code INPUT_MARKUP_STRIPPED (one per Speak, not per tag).

Billing and reporting use the cleaned text. Markup stripping runs before normalization and billing, so billable_character_count and an interrupt’s text_spoken reflect the cleaned text, never the original markup. Configure adjusts speed only; inline pause and pronunciation controls are coming soon.

Flush

End the active turn. The server drains the buffer, generates the remaining audio, and reports the turn. Flush is how you signal “the agent’s response is complete.”

1{"type": "Flush"}

On Flush, the server generates any remaining audio for the active turn and emits Flushed when the turn’s buffer has actually been flushed — not on receipt, and it can be held back behind earlier pending turns — followed by SpeechMetadata with the turn’s billing and timing. The next Speak begins a new turn with a new speech_id.

Flush is what ends a turn. The server may already be streaming a turn’s audio before you flush, but only Flush closes the turn and produces Flushed + SpeechMetadata. A Flush with no active turn is a no-op and produces a NO_ACTIVE_SPEECH warning.

Interrupt

The user has barged in. Cancel the active turn and report what was actually spoken. Interrupt stops synthesis and clears the audio buffer — it does not reset model state, so the voice stays consistent into the next turn.

1{
2 "type": "Interrupt",
3 "playback_offset": {"type": "time_ms", "value": 2340}
4}
FieldTypeRequiredDescription
type"Interrupt"yesMessage type identifier.
playback_offsetobjectnoPlayback position in milliseconds, measured from the start of the session’s audio (not the current turn), as {"type": "time_ms", "value": <number>}. Required for the text_spoken / text_remaining split — when omitted, SpeechInterrupted omits both and reports audio_played_ms from the server’s own totals. Each interrupt’s offset must advance past the position the previous interrupt established.

Interrupt always cancels the active turn — there is no per-turn targeting, and unknown fields are rejected. The server responds with SpeechInterrupted, reporting audio_played_ms, the text split (when a playback_offset was provided), and a nested metadata block. The next Speak begins a new turn.

An Interrupt the server cannot act on — no audio generated yet, an earlier interrupt still in flight, or a non-advancing offset — is answered with a Warning instead of SpeechInterrupted. See the warning codes.

Stop playback locally, then Interrupt. The instant you detect barge-in, stop playback client-side; the round-trip is for context reconciliation, not for stopping audio. Any frames that arrive after you send Interrupt but before SpeechInterrupted were already in flight — discard them. See Interruption Handling for the full pattern.

Configure

Update synthesis configuration mid-conversation without reconnecting.

1{
2 "type": "Configure",
3 "speed": 1.15
4}
FieldTypeRequiredDescription
type"Configure"yesMessage type identifier.
speedenum (number)noSpeech-rate multiplier. One of 0.85, 0.9, 0.95, 1.0 (default), 1.05, 1.1, 1.150.05 increments. Not supported for every language; an unsupported model or language returns SPEED_NOT_SUPPORTED.

Updates apply at the next segment boundary — the active segment finishes under the prior configuration, and audio already synthesized is never re-generated. The server replies with ConfigureSuccess on receipt and validation (not on application), or ConfigureFailure (SPEED_OUT_OF_RANGE / SPEED_NOT_SUPPORTED), which leaves the prior configuration active. Omitted fields keep their current values.

Close

Gracefully close the connection. The server finishes draining all queued audio, emits a final SessionMetadata with cumulative totals, then closes the socket.

1{"type": "Close"}

Keeping a session alive

The server closes an idle session after 60 seconds with no inbound client message (NET-0004). If your agent may go quiet longer than that between turns, send a WebSocket Ping or Pong to reset the timer.