Skip to main content
POST
Streaming text to speech
Building with an AI agent? Point it at docs.kova.ai/for-agents.md — the complete API contract as one plain-text page.
POST /v1/tts/stream returns a text/plain body of SSE-style records — one JSON object per data: line, separated by blank lines. The request body is identical to Text to speech. Use streaming whenever you want playback to begin before generation completes, and for any text over roughly 1,000 characters — the synchronous endpoint would otherwise make you wait tens of seconds for the first byte.
The default response encoding is mp3, not raw PCM. Each audio_chunk is base64-encoded audio in your chosen response_format; concatenate the decoded chunks to reconstruct the file.

Response headers

Event stream format

The body is a sequence of records. Each one starts with data: (note the trailing space), contains one JSON object, and ends with \n\n:
There is no data: [DONE] sentinel and no terminating event. The stream is over when the HTTP body ends. Because the final record is also followed by \n\n, splitting the whole body on \n\n leaves a trailing empty string — skip empty segments rather than trying to parse them.

Event types

audio

timestamps

Emitted only when the request set timestamps: true.
The three arrays are parallel — words[i] spans start_seconds[i] to end_seconds[i].
timestamps events arrive repeatedly and incrementally — not once at the end. Each event carries only the words finalized since the previous one, typically one to three at a time. Append them in arrival order to rebuild the full word list; never overwrite.
A real stream for a fourteen-word sentence looks like this:
start_seconds are always absolute offsets from the beginning of the utterance, so no rebasing is needed as you append. Audio and timestamps events interleave with no fixed ratio or ordering guarantee. Handle them in whatever order they arrive.

Reconstructing the audio

Base64-decode each audio_chunk and concatenate in arrival order.
Streaming mp3 has no ID3 header. The synchronous endpoint returns MP3 beginning with ID3, while streamed MP3 is raw MPEG frames starting with the 0xFF 0xFB frame sync. Both are valid and play fine — but if your code sniffs for ID3 to detect MP3, it will fail on the streaming endpoint.
linear16 re-emits a WAV header on every chunk. Concatenating those chunks produces a file with dozens of embedded headers. For streaming, use encoding: "pcm" and write a single header yourself at the end.

Latency

Time to first audio chunk is under 200 ms on an already-open connection, plus your network round-trip. The floor is stable at roughly 185 ms measured from a client ~40 ms away, and does not vary with text length or voice.
Reuse your HTTP connection. Opening a new TLS connection costs about 90 ms (roughly 6 ms TCP + 82 ms TLS 1.3 handshake) — close to half your latency budget again. A client that opens a fresh connection per request sees ~275 ms instead of ~185 ms.This is easy to get wrong with streaming clients: if you stop reading the response body early and abandon the stream, most HTTP libraries close the connection rather than returning it to the pool, so every request silently pays full setup. Read each response to completion, and keep one long-lived client rather than creating one per call.

Examples

Parsing the stream without an SDK

The framing is simple enough to handle directly. Buffer bytes, split on \n\n, strip the data: prefix:
Note the buffering: a single TCP read is not guaranteed to contain a whole record, so accumulate until you see \n\n rather than parsing each iter_bytes chunk directly.

See also

Authorizations

x-api-key
string
header
required

Body

application/json
text
string
required
voice
string
required
normalize_text
boolean
default:false
response_format
AudioResponseFormat · object
temperature
number | null
timestamps
boolean
default:false

Response

Successful Response