Skip to main content
The Streaming TTS API turns text into speech and sends the audio back as it is synthesized, so playback can start long before the sentence ends. This page gets you from an empty file to playable audio twice: first as a WAV file from one request, then as a conversation spoken over one WebSocket session, the way a voice agent uses it.

Before you begin

  • An API key. Copy it from API Keys and set it once:
  • The SDK. Skip this if you only want the curl command. Text to speech is in the Python SDK from release 1.8.0 and in the JavaScript SDK from 4.42.0.

Generate your first audio

One request, one text, the whole audio back as a WAV file. The SDKs call the HTTP API, and the cURL tab shows the same request without an SDK. Save the program as hello.py or hello.mjs; the .mjs extension makes it an ES module, which the import and the top-level await need.
Run it with python hello.py or node hello.mjs, and hello.wav plays in any audio player. The SDKs print this:
Four things about this run:
  • Only the text is required. voice_id names Audrey here by her ID. Leave it out and you get aai-jane, the default voice, in English at 24 kHz 16-bit PCM. Each voice speaks the language it was recorded in; Voices explains how to name one, find its ID and set the language, and the same voice_id takes a voice you cloned in the dashboard.
  • The response says what it ran with. The voice, model, language, encoding and sample rate come back as response headers, which the SDKs expose on the result. Read the sample rate from there rather than from your own request when you write a header or set up a player.
  • The duration varies between runs. Synthesis is not deterministic, so the same sentence can come back a few hundred milliseconds longer or shorter. Billing counts characters, not seconds.
  • The JavaScript SDK uses two spellings. synthesize() and stream() take the API’s own field names, voice_id and sample_rate, while session() takes camelCase options, voiceId and sampleRate. Results are camelCase, and the Begin and Termination frames keep their wire names.
The requests above go to the US endpoint, which processes your text and audio in the US. To keep processing in the EU, point the SDK or the URL at tts.eu.assemblyai.com instead. See Cloud endpoints and data residency.

Speak a conversation over one session

A voice agent speaks many short texts, one per turn, and needs the first audio of each as fast as possible. The WebSocket API is built for that: one connection stays open for the whole conversation, each sentence is a request on it, and the audio of a request streams back while the request is still being spoken. Opening a connection per sentence pays the handshake every time and quickly hits the per-account limit on new sessions per minute. The programs below open a session, speak a four-sentence reply through it and write the raw audio to a file. Save them as reply.py or reply.mjs.
Run it with python reply.py or node reply.mjs, then play the file. It is raw PCM with no header, so tell the player its format: with SoX, with ffmpeg, or by converting it to a WAV file any player opens.
The output looks like this, with byte counts that differ a little from run to run:
What the session did for you:
  • The server speaks first. Connecting waits for the Begin frame, which echoes the configuration the session actually got, with the session id to quote in support requests. A rejected key or parameter, an unknown voice_id included, arrives as an Error frame in its place: the Python with and the JavaScript connect() raise it. See Error handling.
  • Each sentence is one request. speak() sends the text in Generate frames, concludes the request with a Flush and yields the audio frames until the request’s FlushDone. The iterator ends with the request, which is what lets the loop count bytes per sentence. See Message frames.
  • The audio has no header. A session streams raw codec bytes, 16-bit PCM here, so the file will not play if you hand it to an <audio> element or a Blob. Write a WAV header yourself, or decode the samples and play them as they arrive. Chunk sizes vary and are not aligned to samples, so carry an odd trailing byte over to the next chunk. See Audio formats on WebSocket API basics.
  • Leaving the session ends it cleanly. Exiting the Python block and awaiting close() in JavaScript send Terminate, which speaks any text not yet flushed, and wait for the Termination summary: the seconds of audio and the characters of text that reached synthesis.
A session lasts at most an hour, and the server closes it after two minutes without a frame in either direction, so an agent waiting on a silent caller sends keep_alive() (keepAlive() in JavaScript) now and then. Keep the number of requests you have sent but not yet read bounded: the server accepts text faster than it speaks it, but only so far ahead. The SDKs refuse a text that would overrun that on its own, and a session that keeps sending far ahead of its speech is closed. See Limits.

Stream text in as an LLM writes it

Text that arrives a token at a time does not have to wait for the end of the sentence. generate() appends text to the current request, flush() concludes it and returns its flush_id, and audio() yields the request’s audio. The server starts speaking once it holds whole sentences, before the flush. cancel() is the barge-in: it stops the request in flight and drops anything queued. The fragments below continue the programs above: the imports and config in Python, client in JavaScript. You supply the three functions they call: llm_tokens() yields the text as your LLM produces it, user_started_talking() says whether to interrupt, and play() takes a chunk of audio. An LLM client that streams asynchronously pairs with the asyncio twin, aai.AsyncTTSSession, whose methods are awaited.
Consecutive texts are joined exactly as sent, with no separator: Good followed by morning. is spoken as Goodmorning., so keep the space at the edge of each piece. Requests can be pipelined, a flush() before the previous request’s audio has been read, and their audio comes back in order. See Generate and Cancel.

Using the WebSocket API directly

Not using an SDK? Connect to wss://tts.us.assemblyai.com/v1/ws with any WebSocket library, with your API key in the Authorization header and the voice and audio options as query parameters, and handle the frames yourself. The program below needs websockets 14 or newer (pip install -U websockets). Saved as ws.py and run with python ws.py, it reads Begin, sends one Generate and a Flush, collects the Audio frames until FlushDone, then terminates and writes a WAV file.
Two things the SDKs hide that this program shows:
  • Audio.audio is base64. Each Audio frame is JSON with the codec bytes base64-encoded, under the flush_id of the request it belongs to. flush_id starts at 0, each Flush takes the next id, and ids skip one after a Cancel, so read them from the frames rather than counting.
  • WordBoundaries is on by default on the wire. A frame with one entry per word, its start and end in milliseconds and a confidence, follows each FlushDone. Ignore any frame type you do not handle; the protocol may gain frames. The SDKs turn word boundaries off unless you ask for them. See Word boundaries.
If the socket closes without an Error frame, for example on a network drop, the async for loop raises websockets.exceptions.ConnectionClosedError. Error handling lists the close codes you can see either way, and the WebSocket API reference every query parameter.

Where to next

  • Optimizing accuracy and latency: the session, text and format habits that get the first audio of each reply out sooner and read as you meant it.
  • Choose an interface: the SDKs, the HTTP API, the WebSocket API and the OpenAI-compatible route, and which fits how your text arrives.
  • Authenticate with a temporary token: open a session from a browser or an app without shipping your API key.
  • Voices: AssemblyAI’s voices and the languages they speak.
  • HTTP API basics: streamed and buffered responses, MP3 and telephony formats, errors and limits.
  • LiveKit: wire Streaming TTS into a LiveKit voice agent.
  • WebSocket API basics and Message frames: authentication, the connect sequence, and every frame in the protocol.
  • Limits: text, session and rate limits, and how each one reports a breach.

Need help?

Contact support@assemblyai.com or open a support ticket. Include the session id from Begin or the x-request-id of an HTTP response.