Skip to content

Streaming guide

Both transports start sending audio after the first 80 ms frame is decoded. The difference is control: REST is one request in, one byte stream out; the WebSocket keeps a connection open for many utterances and tells you what is happening as it happens.

POST /v1/tts (chunked REST) WS /v1/tts/stream
Connection one per request one per conversation, utterances in order
Audio body bytes in the chosen container binary messages (pcm16 or Ogg/Opus pages)
Timing info x-request-id header; TTFA in your portal usage start, chunk, first_audio_ms, chunk_end, end events with ttfa_ms, rtf, audio_s
Normalised text not returned (use /v1/normalize) in the start event
Capacity refusal 503 + Retry-After before any audio error event with status: 503, retry_after; socket stays open
Cancel close the connection close the socket (or stop sending)
Best for files, server pipelines, simple integrations, serverless voice agents, IVR, anything with a human waiting

Time to first audio, honestly

Three numbers matter, and the portal playground shows all of them:

  1. Engine TTFA (ttfa_ms in end, and in Usage): from a worker picking the job up to the first decoded frame. ~18 ms warm with a cached text prefill, ~55–80 ms when the chunk's prefill is cold, on a Sapphire Rapids vCPU in Mumbai.
  2. Server TTFA (first_audio_ms on the WebSocket): from the request message to the first audio message, including queueing.
  3. Your TTFA: what your user hears, adding the network. Mumbai to Mumbai is a few ms; cross-country India ~30–60 ms; add TLS and connection setup for REST unless you keep connections alive.

Repeated text is fast: the server caches the text prefill per (voice, chunk). An IVR line that plays 10 000 times pays the cold cost once.

WebSocket event timeline

→ {"text": "...", "voice": "anjura", "format": "pcm16", "request_id": "turn-7"}
← {"type":"start","request_id":"turn-7","voice":"anjura","format":"pcm16","chunks":2,"normalized":"..."}
← {"type":"chunk","request_id":"turn-7","chunk":0,"text":"...","tokens":31}
← <binary: 3840 bytes = 80 ms of pcm16>                   ← first audio
← {"type":"first_audio_ms","request_id":"turn-7","chunk":0,"ms":44.2}
← <binary …> <binary …>                                   ← ~320 ms of audio per message after the first
← {"type":"chunk_end","request_id":"turn-7","chunk":0,"frames":39,"audio_s":3.12,"prefill_ms":0.1,"prefill_cached":true,"eos":true}
← {"type":"chunk","request_id":"turn-7","chunk":1,"text":"...","tokens":18}
← <binary …>
← {"type":"chunk_end","request_id":"turn-7","chunk":1, …}
← {"type":"end","request_id":"turn-7","audio_s":4.9,"frames":61,"chunks":2,"ttfa_ms":17.8,"wall_ms":690.3,"rtf":0.141}

Send several messages back to back; they are served in order on that connection, and every event carries the request_id you gave (or a generated one), so a client can correlate. Keep one connection per conversation; open a second connection if you need two things spoken in parallel (each connection uses one worker at a time).

Playback: buffer a little, not a lot

  • Audio messages after the first carry about 320 ms of speech each and arrive faster than real time (RTF ≈ 0.14–0.19), so the buffer only grows. Start playback when you have 100–200 ms buffered; that covers network jitter without adding noticeable delay.
  • pcm16 is 16-bit signed little-endian, 24 kHz, mono: 48 000 bytes per second. Schedule consecutive buffers back to back (Web Audio: source.start(nextTime); the browser snippet in the quickstart does this).
  • For telephony, resample 24 kHz → 8 kHz and encode to μ-law/A-law on your side, or ask for opus where the carrier supports it.

Sentence by sentence from an LLM

Send each sentence as soon as the model finishes it, as its own message; do not wait for the whole reply. The normaliser puts a terminal mark on every utterance and the chunker splits anything over ~50 tokens, so sentence-sized messages give the lowest latency and the most natural prosody. Give each message a request_id so you can drop audio for a turn that was interrupted.

Barge-in and cancellation

When the user starts talking, stop playing and close the WebSocket (or the REST connection): synthesis stops within one frame and you are billed only for audio already produced. Open a fresh connection for the next turn; a TLS handshake is far cheaper than finishing a sentence nobody hears. On the WebSocket you cannot cancel one utterance and keep the next on the same connection: closing cancels the one in flight and discards anything queued behind it, which is what barge-in wants.

Reconnects

Close code / condition Meaning Do
1000 you closed nothing
1001, 1006 server restart or network drop reconnect with backoff (100 ms, 200, 400 …)
error event status: 503 capacity exhausted for that utterance retry that utterance after retry_after s; keep the socket
error event with status: 400/413 bad text for that utterance fix and resend; keep the socket
error event timeout 120 s request limit hit split the text

A server shutting down for a deploy finishes in-flight streams first (SIGTERM is graceful), so a 1001 normally arrives between utterances.

REST tips

  • Keep connections alive (HTTP/1.1 keep-alive or HTTP/2) to avoid a TLS handshake per line.
  • Read the body incrementally; don't wait for the end if the audio is for a human.
  • "stream": false when you need a WAV with a correct length (e.g. uploading to a telephony provider that validates headers).
  • The x-request-id header is set from your request_id or generated; it appears in your usage log and in support conversations.