APIs
Synthesize speech (streaming)
Send text, receive audio chunks over HTTP as they are generated
You send text, the server returns audio chunks over HTTP as they are generated. Playback can begin before the full synthesis is complete, significantly reducing time-to-first-audio.
Best for real-time applications and conversational AI when you want low-latency playback without managing a persistent connection. Each request accepts up to 4,000 characters.
For the lowest latency, use the WebSocket API. For long-form content such as audiobooks, podcasts, or voiceovers, use the Async API. For tips on optimizing latency, see the latency best practices guide.
HTTP request and response framing
Send POST https://api.inworld.ai/tts/v1/voice:stream with
Authorization: Basic <INWORLD_API_KEY> and Content-Type: application/json.
Use the Portal's Base64 credentials unchanged, without encoding them again.
Keep the key on your server; see Authentication for
browser and mobile token options.
curl --no-buffer 'https://api.inworld.ai/tts/v1/voice:stream' \
--header "Authorization: Basic $INWORLD_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"text": "Hello, world!",
"voiceId": "Ashley",
"modelId": "inworld-tts-2",
"audioConfig": { "audioEncoding": "PCM", "sampleRateHertz": 48000 }
}'The JSON response is newline-delimited JSON (NDJSON): one JSON object per
line, not SSE (data: events) or a single JSON document. Each successful
message wraps its payload in result. Audio is Base64-encoded inside
result.audioContent, not raw audio in the HTTP body. Example lines below use
placeholders for the audio bytes:
{"result":{"audioContent":"<base64-audio>","usage":{"processedCharactersCount":13,"modelId":"inworld-tts-2"}}}
{"result":{"audioContent":"<base64-audio>","usage":{"processedCharactersCount":0,"modelId":"inworld-tts-2"}}}A network read may contain part of a line or several lines. Buffer until a
newline, parse each complete object, and Base64-decode its audio independently.
Do not call response.json() on the whole streaming response or concatenate
Base64 strings before decoding. Process a final non-empty line at EOF too.
Timestamp-only messages can arrive after the audio; keep reading until EOF.
| Request field | Meaning |
|---|---|
text | Up to 4,000 UTF-16 code units per HTTP streaming request |
voiceId | Use the exact voiceId from List voices, such as Ashley |
modelId | Set explicitly; choose from Models, for example inworld-tts-2 |
audioConfig.audioEncoding | Encoding of the decoded audio bytes; see the table below |
audioConfig.sampleRateHertz | Requested output sample rate in Hz; configure your PCM player to match |
The non-streaming endpoint
accepts 2,000 UTF-16 code units, while this endpoint accepts 4,000. In
JavaScript, text.length measures this unit; some emoji count as two. An
oversized request returns INVALID_ARGUMENT (code 3) with a message such as
text length should not exceed 4000 characters. SDK versions can enforce their
own smaller limits or split text into multiple requests. Set modelId explicitly
and consult the documentation for the installed SDK version instead of relying
on its default model or chunk size.
Choose an audio encoding
The table below compares common streaming choices; it is not the full encoding
list. The API also exposes OGG_OPUS, ALAW, MULAW, and FLAC. See the
audioConfig.audioEncoding API reference
for the complete enum and format descriptions.
| Encoding | Decoded bytes in an HTTP stream |
|---|---|
PCM | Raw signed 16-bit little-endian samples, with no WAV header; suitable for a PCM playback queue |
LINEAR16 | A complete WAV header in every audio chunk; do not feed the headers to a raw PCM player or concatenate chunks as one ordinary WAV file |
WAV | A streaming WAV header in the first audio chunk only |
MP3 | Compressed audio; use an MP3-capable streaming decoder/player |
The encoding names describe the audio after Base64 decoding. PCM and
LINEAR16 contain the same sample format but have different framing.
Read the stream in Node.js
This server-side example uses Node.js 20+ built-in fetch. onAudio receives
decoded PCM bytes; enqueue them for playback or write them to a raw PCM file.
A raw PCM file needs its sample rate and channel count supplied to the player.
async function streamSpeech(text, onAudio, { signal } = {}) {
const response = await fetch('https://api.inworld.ai/tts/v1/voice:stream', {
method: 'POST',
headers: {
Authorization: `Basic ${process.env.INWORLD_API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
text,
voiceId: 'Ashley',
modelId: 'inworld-tts-2',
audioConfig: { audioEncoding: 'PCM', sampleRateHertz: 48000 },
}),
signal,
});
if (!response.ok) {
const body = await response.text();
let message = body;
try {
const payload = JSON.parse(body);
message = (payload.error ?? payload).message ?? body;
} catch { /* An intermediary may return a non-JSON error. */ }
throw new Error(`HTTP ${response.status}: ${message}`);
}
if (!response.body) throw new Error('Missing response stream');
const decoder = new TextDecoder();
let pending = '';
let processedCharacters = 0;
async function consume(line) {
if (!line.trim()) return;
const message = JSON.parse(line);
if (message.error) throw new Error(message.error.message ?? 'Synthesis failed');
const result = message.result;
processedCharacters += result?.usage?.processedCharactersCount ?? 0;
if (result?.audioContent) {
await onAudio(Buffer.from(result.audioContent, 'base64'));
}
}
for await (const bytes of response.body) {
pending += decoder.decode(bytes, { stream: true });
let newline;
while ((newline = pending.indexOf('\n')) !== -1) {
const line = pending.slice(0, newline);
pending = pending.slice(newline + 1);
await consume(line);
}
}
pending += decoder.decode();
await consume(pending);
return { processedCharacters };
}Errors, usage, and cancellation
Check both the initial HTTP status and each parsed message. A stream can return
an HTTP 200 and later end with an error message:
{"error":{"code":3,"message":"Invalid request"}}Non-streaming endpoints can instead return {"code":5,"message":"Voice not found"}.
For shared error handling, inspect payload.error ?? payload. Codes in these
objects are gRPC status codes, not HTTP status numbers. Preserve any audio
already delivered as partial output and report the failure; a successful HTTP
status alone does not establish that synthesis finished.
For HTTP streaming, usage.processedCharactersCount carries the full input's
UTF-16 length on the first response message; subsequent messages report zero.
Sum this field across messages, or retain the first value. Reading only the last
message reports zero and loses the request's usage.
Pass an AbortController signal to streamSpeech and call abort() to cancel
the fetch and response reader. Handle the rejection as cancellation, and stop
and clear your playback queue separately. Cancellation does not refund text
already metered: the full submitted text is metered when the first response is
emitted, rather than in proportion to the audio you play. Aborting before you
receive audio is not proof that the server has not already metered the request.
Audio chunk size, arrival spacing, and time to first audio vary with text, model,
encoding, and load. Buffer for continuous playback rather than assuming fixed
chunk durations or an arrival rate equal to playback speed. For mono PCM16,
durationSeconds = decodedByteLength / (2 * sampleRateHertz); at 48 kHz,
9,600 decoded bytes represent 100 ms of audio. These are sizing calculations,
not latency guarantees.
Timestamp transport strategy
When using timestamp alignment, you can choose how timestamps are delivered alongside audio using timestampTransportStrategy:
SYNC(default): Each chunk contains both audio and timestamps together.ASYNC: Audio chunks arrive first, with timestamps following in separate trailing messages. This reduces time-to-first-audio.
See Timestamps for details on how each mode works.
Code Examples
Node.js SDK
Use stream() from the @inworld/tts SDK — async iteration over audio chunks with automatic retries.
JavaScript
View our JavaScript implementation example
Python SDK
Use stream() from the inworld-tts SDK — async iteration over audio chunks with automatic retries.
API Reference
Synthesize Speech Stream
View the complete API specification