Skip to main content

POST /tts/bytes

Generates audio from the input transcript. The response body is the complete audio clip in the requested format; it arrives once generation finishes. For real-time playback, use /tts/sse or the WebSocket instead.

Request Body

voice is required — there is no default voice. The only accepted form is the nested object { "mode": "id", "id": "<voice-id>" }; a top-level voice_id is not supported, and mode must be "id". A request that doesn’t resolve to a voice is rejected rather than silently synthesized: missing_voice / missing_voice_id / unknown_voice_mode (400) for a bad specifier, voice_not_found (404) for an unknown id, and missing_transcript (400) for an empty transcript. See Errors for the full envelope.

Length limit

transcript can be at most 2,000 characters per request, and per message on the WebSocket. 2,000 characters is about 1.5–2 minutes of speech for typical voices. Characters are counted the same way as for billing: Unicode code points of transcript exactly as sent (after JSON decoding), with no normalization. Leading and trailing whitespace and newlines count too. You can check before sending:
  • Python: len(text)
  • JavaScript: [...text].length. Don’t use text.length: it counts UTF-16 units, so it overcounts characters outside the Basic Multilingual Plane, such as emoji ("😀".length === 2).
Over the limit returns 400 with error_code: "input_too_long". It’s returned before any work starts, so nothing is billed:
For longer content, split it into multiple requests, or stream it over WebSocket as several messages with continue: true — the limit is per message. For real-time playback, prefer streaming: /tts/bytes returns nothing until the whole clip has been generated.
The request body itself is capped at 64 KB — a body over 65536 bytes returns 413 with error_code: "payload_too_large". Normal requests are nowhere near this.

Output Format

output_format has three fields, each validated against a fixed set. Anything outside these values returns 400 — the API does not silently fall back.
  • The model is natively 48 kHz. 48000 skips resampling; lower rates are resampled on the server.
  • Container by endpoint: wav is available only on /tts/bytes. Streaming endpoints (/tts/sse, /tts/websocket) are raw-only — there’s no place to put a RIFF/WAV header in a chunked stream.
  • pcm_mulaw / pcm_alaw are 8-bit G.711 telephony codecs. Browsers can’t play them directly; decode to linear PCM first (or hand them straight to your telephony stack).

Telephony (G.711)

For Twilio / SIP media streams, request 8 kHz μ-law or A-law raw. G.711 encodings aren’t forced to 8 kHz, so always send sample_rate: 8000 with pcm_mulaw / pcm_alaw:
Unsupported formats. mp3 (container) and pcm_f32le (encoding) are not supported and return 400. Request wav + pcm_s16le and transcode client-side if you need another format.

Models

Billing

1 credit = 1 character of transcript, counted as described in Length limit (whitespace included). Billing is postpaid: a request starts only while your org’s balance is above 0. Because the balance is checked only when a request starts, your last request can take it slightly below zero.
  • /tts/bytes is all-or-nothing. The clip is returned only after generation finishes, so any failure is an HTTP error with no audio and isn’t charged. A successful response is charged, even if your client disconnects while downloading it.
  • /tts/sse is charged for the full transcript once at least one audio chunk has been delivered, including a stream that later fails with an error event. A stream that fails before its first audio chunk isn’t charged.
  • WebSocket turns are billed per turn and in 1,000-character batches. See WebSocket Streaming.

Examples

POST /tts/sse

Streams audio over Server-Sent Events for low time-to-first-audio without opening a WebSocket. The request body is identical to /tts/bytes, except the container must be raw (SSE can’t wrap a WAV/RIFF header):
Each event’s data is a JSON object. chunk events carry a base64-encoded slice of raw PCM; a final done event closes the stream:
The same length limit and 64 KB body cap apply. input_too_long (400) and payload_too_large (413) are returned as a normal HTTP error response before the event stream opens. Generation errors — overloaded (429), engine_error (usually 502), and engine_unavailable (503) — can also arrive mid-stream as an error event. The HTTP status is already 200, and the stream ends after this event with no done, so handle error events as well as chunk and done:
A stream is charged for the full transcript once its first audio chunk has been delivered, even if it later ends with an error event (see Billing). See Errors for every code.

WebSocket Streaming

For real-time, low-latency generation over a persistent connection — wss://api.nineninesix.ai/tts/websocket — see the WebSocket Streaming page, which includes the send/receive message schemas and an interactive Connect playground.