Client Messages
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.
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.
{
"type": "Speak",
"text": "Sure, I can help you cancel your subscription."
}| Field | Type | Required | Description |
|---|---|---|---|
type |
"Speak" |
yes | Message type identifier. |
text |
string | yes | Text to synthesize — see Text handling below. |
Streaming LLM tokens
Section titled “Streaming LLM tokens”Streaming is just sending Speak messages as tokens arrive, then flushing at the end of the turn:
{"type": "Speak", "text": "Sure, "}
{"type": "Speak", "text": "I can "}
{"type": "Speak", "text": "help you "}
{"type": "Speak", "text": "cancel your "}
{"type": "Speak", "text": "subscription."}
{"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
Section titled “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.
Markup handling
Section titled “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).
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.”
{"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.
Interrupt
Section titled “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.
{
"type": "Interrupt",
"playback_offset": {"type": "time_ms", "value": 2340}
}| Field | Type | Required | Description |
|---|---|---|---|
type |
"Interrupt" |
yes | Message type identifier. |
playback_offset |
object | no | Playback 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.
Configure
Section titled “Configure”Update synthesis configuration mid-conversation without reconnecting.
{
"type": "Configure",
"speed": 1.15
}| Field | Type | Required | Description |
|---|---|---|---|
type |
"Configure" |
yes | Message type identifier. |
speed |
number | no | Speech-rate multiplier. 0.5 to 1.5 in 0.05 increments, defaulting to 1.0. 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_INCREMENT_INVALID / SPEED_NOT_SUPPORTED), which leaves the prior configuration active. Omitted fields keep their current values.
Gracefully close the connection. The server finishes draining all queued audio, emits a final SessionMetadata with cumulative totals, then closes the socket.
{"type": "Close"}Keeping a session alive
Section titled “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.
Related resources
Section titled “Related resources”- Server Messages — the responses to everything on this page
- The Speech Lifecycle — how these messages drive turn state
- Build a Flux TTS Voice Agent — these messages in a full agent loop
- Getting Started — connect and send your first turn