POST /v1/text-to-speech/{voice_id}/stream for one-shot streaming generation. Use wss://api.breeze.blue/v1/text-to-speech/{voice_id}/stream-input when your product sends text incrementally during a live conversation.
The realtime WebSocket always streams raw pcm_s16le, 24000 Hz, mono, 16-bit audio as binary frames. It does not accept output_format; use the HTTP text-to-speech endpoints for mp3 or wav.
Choose an integration
Raw WebSocket API
Create a browser-safe session, exchange JSON turn events, and save binary PCM audio.
Python and TypeScript SDKs
Use typed connection helpers while consuming audio concurrently with incremental text input.
Browser playback
Mint a short-lived token on your backend and play PCM chunks with the Web Audio API.
CLI smoke test
Validate credentials, voice routing, TTFA, and multi-turn behavior before writing application code.
Create a browser session
Create short-lived browser tokens from your backend. Do not expose long-lived API keys in browser code.direct_websocket_url when the response provides it, so the credential stays out of the URL. Older clients and custom integrations can continue to open websocket_url. Wait for session.ready, send a turn, and keep consuming frames until turn.done.
The response includes client_secret, websocket_url, expires_at, and the fixed audio_format. It also includes direct_websocket_url when the direct transport is available. model_id, language_code, voice_settings, inactivity timeout, and logging behavior are fixed when the session is created. instructions supplies the initial value and is the one session setting that can later be replaced between turns through the WebSocket. When voice_settings.guidance_scale is omitted, the voice’s saved setting is used.
The client_secret expires 60 seconds after the session is created; open the WebSocket within that window. The token is validated only at connect time — once the connection is established, the session can stay open for up to 30 minutes regardless of the token expiry.
Connect
direct_websocket_url is present, connect to that query-free URL and offer these two WebSocket subprotocols in order:
breeze-realtime-v1; it never echoes the token-bearing protocol. A direct client must fail the connection if the selected protocol is different or absent. The client_secret remains a short-lived credential: do not log the offered protocols or put the token into URLs. Browsers can pass the protocol array to the native WebSocket constructor, and server-side clients can use their WebSocket library’s subprotocol option.
Server-side clients may also connect with Authorization: Bearer ... or xi-api-key. Browser clients should use client_secret.
API-key connections accept the model_id, language_code, inactivity_timeout_seconds, and enable_logging query parameters. client_secret connections accept no other query parameters: initial session settings come from the session-creation request, and any additional query parameter is rejected with VALIDATION_ERROR. After the connection is ready, use session.update to replace instructions; query parameters cannot update it. output_format is always rejected.
Wait for the session.ready event before starting the first turn; it carries the session limits your client should honor.
Raw WebSocket quickstart
This Node.js 22+ example uses onlyfetch and the native WebSocket. It creates a browser-safe session, waits for session.ready, sends one turn, and writes the binary audio frames to out.pcm.
pcm_s16le, 24000 Hz, mono, 16-bit audio. Use the browser example for chunk-by-chunk playback or the CLI to save a WAV container without writing audio plumbing.
Client events
Send text frames as JSON. Binary client frames are rejected with aVALIDATION_ERROR error frame; the session stays open.
Update instructions between turns
Keep the same WebSocket open when only the speaking direction needs to change. Send one update while the session is idle:turn.start and later turns. Wait for session.updated before starting that next turn. Only one update may be pending at a time. An update sent while a turn is active receives a recoverable BAD_REQUEST error; it does not cancel or otherwise interrupt the active turn, and the previous instructions remain in effect.
instructions must be a string with non-whitespace content and at most 1,000 characters. Other session settings cannot be changed over the WebSocket. To change the model, language, voice settings, inactivity timeout, or logging behavior, create a new session.
Limits
- Each
text.appendpayload is at most 2 KB (2048 UTF-8 bytes). Read the exact value fromsession.ready.max_append_bytes. - Total turn text is capped at the text-to-speech generation limit, currently 1,000 characters. Read the exact value from
session.ready.max_turn_characters. - One session has one active turn at a time. Wait for
turn.doneorturn.cancelledbefore sending the nextturn.start— aturn.startwhile a turn is active is a protocol error that cancels the active turn. - Send
session.updateonly between turns, with at most one update awaitingsession.updated. The acknowledged instructions apply from the next turn. client_secrettokens expire 60 seconds after session creation.- Idle timeout defaults to 30 seconds and can be set up to 180 seconds.
- A session can stay open for up to 30 minutes.
- The server sends
session.expiringabout two minutes before the maximum lifetime so the client can prepare a replacement connection. - Each API key can hold up to 20 concurrent realtime sessions. See Rate limits.
Server events
Audio is sent only as binary PCM frames. Decode it as signed 16-bit little-endian PCM at 24000 Hz, mono. All other server frames are JSON text frames with atype field.
session.ready
Sent once after the session is set up. Client events are processed after this frame.
session.updated
Acknowledges an idle session.update after the synthesis connection has applied the new instructions.
session.expiring
Sent once, about two minutes before the physical WebSocket reaches its maximum lifetime. It is advisory: the current connection remains usable until expires_at, but a long-running client should open a fresh session and move the next turn to it. Never move or replay a turn that is already active.
turn.started
Acknowledges turn.start.
audio.started
Sent once per turn, immediately before the turn’s first binary audio frame.
turn.done
Sent when all audio for a completed turn has been delivered.
turn.cancelled
Same fields as turn.done with status set to cancelled. Sent after turn.cancel, when a turn ends without producing any audio (for example turn.end with no appended text), when a protocol error cancels the active turn, or when the session closes with a turn still active.
usage.committed
Sent after turn.done, once billing for the turn has been booked. It is emitted only for completed turns and can arrive while a later turn is already running.
usage.committed is a best-effort notification on the physical WebSocket that produced the turn. If the client closes or rotates that WebSocket after turn.done but before background settlement completes, the charge still commits even though the event can no longer be delivered. Use history and usage APIs as the durable billing record; do not interpret a missing usage.committed frame as an unbilled turn.
pong
Reply to a client ping. No additional fields.
session.closed
Reply to a client session.close, sent before the server closes the WebSocket with code 1000.
error
Errors and close codes
Not every error ends the session:- Validation, protocol, and turn-capacity errors (
VALIDATION_ERROR,BAD_REQUEST,GENERATION_CAPACITY_EXCEEDED,GENERATION_CONCURRENCY_EXCEEDED) send anerrorframe and keep the session open. Errors caused by turn commands cancel the active turn and produceturn.cancelled. Asession.updaterejected because a turn is active is the exception: the current turn continues and noturn.cancelledevent is sent. - Authentication and policy errors (
AUTH_REQUIRED,AUTH_SESSION_EXPIRED,BILLING_INSUFFICIENT_CREDITS,RESOURCE_NOT_FOUND,RATE_LIMITED) send anerrorframe, then close with code 1008. - Server and upstream failures (
UPSTREAM_GENERATION_ERROR,UPSTREAM_TIMEOUT,GENERATION_INVALID_RESPONSE,AUTH_CONFIG_MISSING, and other contract codes reported by the synthesis service) send anerrorframe, then close with code 1011 — or 1012 withmeta.reconnect: truewhen reconnecting is expected to succeed. - Timeouts send an
errorframe withIDLE_TIMEOUT(close code 1001) orSESSION_EXPIRED(close code 1000).
Keepalive and timeouts
The idle timer resets on every client frame and, while a turn is synthesizing, on every audio or event frame from the synthesis service. A turn that keeps producing audio longer thaninactivity_timeout_seconds is never cut off as idle — only a session with no client traffic and no active synthesis times out.
During idle gaps between turns, send ping well inside the inactivity_timeout_seconds window; the server replies with pong. A practical interval is half the effective idle timeout, capped at 15 seconds, with a small amount of jitter:
ping send can succeed on a half-open connection after inbound traffic has stopped. Raw WebSocket clients should treat a missing pong or other inbound frame within their acknowledgement deadline as a transport failure; managed SDKs use a 5-second deadline by default.
Independent of activity, a session closes after 30 minutes with a SESSION_EXPIRED error frame and close code 1000. Long-running products should react to session.expiring by creating a fresh session, waiting for its session.ready, routing the next turn to the replacement, and gracefully closing the old connection after any active turn reaches turn.done or turn.cancelled.
Billing, history, and privacy
Realtime turns are billed per character of appended text, counted after Unicode normalization — the same metering as HTTP text to speech. Billing is committed when the turn settles:- A completed turn bills
text_charactersand emitsusage.committedonce the charge is booked. - A cancelled turn that already delivered audio still bills the appended text;
usage.committedis not emitted for cancelled turns. - A turn that never produced audio (for example
turn.endwith no text, or a cancel before the first audio frame) is not billed. - Delivered audio is always billed, even if your balance runs out mid-turn: the balance can go negative and is offset automatically by your next top-up. Once the balance reaches zero, new turns are rejected with
BILLING_INSUFFICIENT_CREDITS.
history_item_id in turn.started and turn.done). With logging enabled (the default), the turn text and audio are stored and retrievable through the history endpoints. With enable_logging: false, Breeze keeps the history record for accounting and billing but never stores the turn text or audio.
Minimize time to first audio
text.flush tells the model to synthesize the text buffered so far instead of waiting for more context. Use it at natural sentence boundaries — flushing mid-phrase can hurt prosody, while never flushing delays the first audio until turn.end.
When the text comes from a streaming LLM, forward tokens as they arrive and flush on sentence boundaries:
- Send
turn.startas soon as the LLM starts responding. - Send each text delta as
text.append(split deltas larger than 2 KB). - Send
text.flushwhen a sentence completes (.,?,!, or newline). - Send
turn.endwhen the LLM response finishes.
client_secret expiry — the token only needs to be valid at connect time, so connecting immediately and holding the open session is the safest pattern.
Measure your integration with audio.started.ttfa_ms, which reports the server-observed time from turn.start to the first audio frame for every turn.
Connection lifecycle
Realtime WebSocket sessions are optimized for live conversation TTFA. They are not resumable audio streams. Network changes, service deployments, and upstream realtime worker restarts can close an active WebSocket. Between turns, Breeze may transparently replace an interrupted downstream synthesis connection. This recovery is bounded and only happens when no turn is active; the public WebSocket stays open and retains the latest acknowledged instructions together with the other session settings. If the downstream connection cannot be restored, the public session closes with the reconnect signal below. For conversations longer than one physical session, keep conversation, ASR, LLM, and playback state in your application. Open the replacement WebSocket before the current deadline, wait forsession.ready, and switch only at a turn boundary. Keep the overlap short so planned rotation does not consume two session slots for longer than necessary.
If the connection closes before turn.done or turn.cancelled, treat the active turn as interrupted. Stop playback for that turn, discard or explicitly mark any partial audio in your UI, create a new realtime session when using browser client_secret, reconnect, and start a new turn from your own conversation state. Do not reuse turn_id as a resume token; it is only a client correlation id.
When Breeze can send a structured restart signal, it sends an error frame with meta.reconnect: true and meta.retry_after_ms, then closes the WebSocket with close code 1012. The SDKs also surface abnormal WebSocket closes as a session.closed event; session.closed.reconnect is true for close code 1012.
Use short exponential backoff with jitter for reconnects, for example 250 ms, 500 ms, 1 s, then 2 s, capped at 5 s. Browser clients should request a fresh client_secret from your backend after a reconnect failure or token expiry.
SDK quickstart
Start consuming the connection before appending text. Audio can arrive as soon asflush() is processed and may continue after endTurn() / end_turn(); keep the single consumer running until turn.done. TypeScript events use camelCase fields (turnId, historyItemId, ttfaMs); the wire protocol and the Python SDK use snake_case.
session.ready, obtain fresh session credentials, and move the next turn to a replacement WebSocket at a turn boundary. By default, a managed physical epoch becomes eligible for rotation when it reaches 600 seconds even though the raw WebSocket server allows up to 30 minutes. An active turn remains on its original epoch and can delay the switch beyond the 600-second threshold; the 30-minute server hard limit still applies. Set Python max_physical_session_seconds=0 or TypeScript maxPhysicalSessionMs: 0 to disable the client age threshold and rely on the server-advertised lifetime.
Both SDK connection types expose the same update operation: TypeScript uses connection.updateInstructions(...), and Python uses await connection.update_instructions(...). Keep consuming the existing iterator until its matching session.updated event arrives, then start the next turn. Managed connections retain the confirmed value across physical-session rotation and idle reconnect. SDK-created replacement sessions include it at creation; replacements created by a custom sessionFactory receive the confirmed value before the logical iterator exposes their next session.ready.
Each managed heartbeat must be acknowledged by pong or another inbound server frame within 5 seconds. A successful WebSocket write alone is not proof that both directions are healthy. Python exposes this wait as heartbeat_timeout_seconds; TypeScript exposes it as heartbeatTimeoutMs. When the acknowledgement times out between turns, the manager opens a fresh physical epoch with bounded retry. When it times out or the transport closes during an active turn, the manager raises TURN_INTERRUPTED and never replays the turn; rebuild any retry from application conversation state. The same logical iterator emits another session.ready for each physical epoch.
Python start_turn(...) waits when an idle replacement is already in progress. TypeScript keeps the existing synchronous startTurn(...) API and provides await startTurnWhenReady(...) for the same race-safe behavior. These methods wait only before turn.start has been sent and send it once on the replacement; append, flush, end, and cancel commands are not buffered. Once a turn start write begins, a connection failure remains TURN_INTERRUPTED and is never replayed.
A managed connection represents an active realtime call, not user presence. Exit the Python async context or call TypeScript connection.close() when the call ends, the user leaves, or the page enters a long-lived background state. The automatic heartbeat otherwise continues to occupy a server WebSocket slot by design.
Browser TypeScript clients provide a sessionFactory that obtains a newly minted session from their own backend for every physical epoch:
408, 425, 429, and 5xx) from terminal 4xx policy or configuration responses. A plain JavaScript Error cannot carry this distinction and is retried only inside the same bounded setup budget.
Both SDKs also expose a low-level keepalive ping() and support connecting with a pre-created session’s client_secret to keep setup off the critical path. Every SDK connection has exactly one event consumer; do not run separate audio and event iterators at the same time. See the SDK READMEs for error types and tuning options.
Browser playback with the native WebSocket
Browsers can talk to the realtime endpoint directly with aclient_secret minted by your backend — no SDK and no API key in the page. This example schedules PCM chunks on an AudioContext:
AudioWorklet that pulls from a ring buffer — createBufferSource scheduling is fine for short clips but accumulates drift on long conversations.
CLI
Use the CLI as the fastest end-to-end smoke test. It validates authentication, voice access, realtime routing, PCM handling, and per-turn TTFA without requiring WebSocket code:--stdin-format text treats every non-empty line as a complete turn. --stdin-format jsonl provides a managed local control format: {"type":"turn","text":"..."} submits a turn, and {"type":"session.update","instructions":"..."} changes delivery instructions for later turns. Dynamic session instructions use Breeze TTS 2, so this managed JSONL mode selects breeze-tts-2 when --model is omitted while preserving an explicitly selected model. The CLI waits for the current turn to finish, then waits for the exact session.updated acknowledgement before it processes the next record. Only confirmed instructions are retained across physical-session rotation and idle reconnect. An update that may have been written but was not acknowledged is never replayed, and later turns are not sent under an uncertain value.
Managed CLI conversations use the same lifecycle as the SDK managers: a physical WebSocket becomes eligible for turn-boundary rotation at 600 seconds, each keepalive probe waits up to 5 seconds for an inbound acknowledgement, and two consecutive missed probes mark the connection unhealthy. An active turn can delay the planned switch but is never moved between sockets. An idle failure reconnects with fresh session credentials. An active-turn failure is terminal for that turn and is never replayed.
--events is a raw, connection-scoped protocol mode. It forwards public client events such as session.update verbatim but does not serialize turns, wait for acknowledgements, or carry confirmed instructions to another physical session. The input owner must start another command or session at a safe turn boundary after that socket ends.
See CLI realtime conversation for JSON output, multi-turn stdin, interruptions, exit codes, and file behavior.
Continue building
Create realtime TTS session
Mint short-lived
client_secret tokens for browser WebSocket connections.CLI realtime turns
Run realtime conversation turns from the terminal with
breeze tts realtime.Streaming
Compare one-shot HTTP streaming with the realtime WebSocket for your latency needs.
Errors
Look up every error code with HTTP status, domain, and retry guidance.
Text to speech
Use the HTTP endpoints for one-shot generation, async jobs, and
mp3 or wav output.Output formats
Decode the fixed PCM realtime frames and pick encodings for the HTTP endpoints.

