Skip to main content
Realtime text to speech is a WebSocket channel for conversational products. Reuse one physical connection across nearby turns to avoid repeated setup, while treating a long conversation as a logical session made of bounded WebSocket connections. Use 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.
This HTTP request only creates the short-lived session. It does not synthesize audio. Official SDKs and the CLI use 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

When direct_websocket_url is present, connect to that query-free URL and offer these two WebSocket subprotocols in order:
The server selects only 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 only fetch 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.
The output is headerless 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 a VALIDATION_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:
The server forwards the update through the active synthesis connection and acknowledges it only after the new value has been applied:
The acknowledged value applies to the next 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.append payload is at most 2 KB (2048 UTF-8 bytes). Read the exact value from session.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.done or turn.cancelled before sending the next turn.start — a turn.start while a turn is active is a protocol error that cancels the active turn.
  • Send session.update only between turns, with at most one update awaiting session.updated. The acknowledged instructions apply from the next turn.
  • client_secret tokens 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.expiring about 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 a type 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 an error frame and keep the session open. Errors caused by turn commands cancel the active turn and produce turn.cancelled. A session.update rejected because a turn is active is the exception: the current turn continues and no turn.cancelled event is sent.
  • Authentication and policy errors (AUTH_REQUIRED, AUTH_SESSION_EXPIRED, BILLING_INSUFFICIENT_CREDITS, RESOURCE_NOT_FOUND, RATE_LIMITED) send an error frame, 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 an error frame, then close with code 1011 — or 1012 with meta.reconnect: true when reconnecting is expected to succeed.
  • Timeouts send an error frame with IDLE_TIMEOUT (close code 1001) or SESSION_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 than inactivity_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:
Track the reply as well as the write. A 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_characters and emits usage.committed once the charge is booked.
  • A cancelled turn that already delivered audio still bills the appended text; usage.committed is not emitted for cancelled turns.
  • A turn that never produced audio (for example turn.end with 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.
Each turn creates a history item (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:
  1. Send turn.start as soon as the LLM starts responding.
  2. Send each text delta as text.append (split deltas larger than 2 KB).
  3. Send text.flush when a sentence completes (., ?, !, or newline).
  4. Send turn.end when the LLM response finishes.
Keep session setup off the critical path: create the realtime session ahead of time (for example while the user is still speaking) and open the WebSocket before the first text is ready. Time the pre-connect against the 60-second 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 for session.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 as flush() 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.
For a conversation that can outlive one physical WebSocket, use the opt-in managed connection:
The managed APIs derive a safe heartbeat from 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:
The factory must distinguish transient setup responses (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 a client_secret minted by your backend — no SDK and no API key in the page. This example schedules PCM chunks on an AudioContext:
For production playback, prefer an 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:
The default --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.