POST /audio/speech, under the base URL https://tts.us.assemblyai.com/v1/openai. Point an OpenAI SDK, or a framework that is built on one, at that base URL with your AssemblyAI API key as its API key, and it speaks with AssemblyAI voices without other changes. This page is for code that already speaks the OpenAI speech API. For a new integration, the Python and JavaScript SDKs and the native LiveKit and Pipecat plugins reach the whole API, word timings and barge-in on one session included.
The route is part of the HTTP API and behaves like Create speech: the audio streams as it is synthesized, the status waits for the first audio so every earlier failure is a real HTTP status, a failure after that point cuts the connection, and the same limits and billing apply. Only the request fields and the error body follow OpenAI; Create speech (OpenAI compatible) is the route reference.
OpenAI SDKs
- Python
- JavaScript
openai 3, which is built on httpx2; with openai 2 or earlier, write httpx for httpx2 and DefaultHttpxClient for DefaultHttpx2Client. The JavaScript example needs npm install undici, because the SDK takes a dispatcher only together with undici’s own fetch; leave both out in a browser.
The SDKs send the key as Authorization: Bearer <key>, which the API accepts. hello.pcm holds raw 16-bit mono PCM at 24 kHz. In a browser, give the JavaScript SDK a temporary token minted with product=tts as its apiKey, never your API key, and set its dangerouslyAllowBrowser option to true: without it the SDK refuses to run in a browser.
Keep connections between replies
Build the client once per process and reuse it. The API keeps an idle connection open for an hour, but the OpenAI SDKs close it much sooner: the Python SDK after 5 seconds, and the JavaScript SDK on Node after 4, the default of Node’sfetch. A voice agent whose replies are further apart than that opens a new connection, a TCP and a TLS handshake, before nearly every reply. The limits and dispatcher settings in the examples above keep idle connections for 2 minutes.
Every reconnect costs two network round trips before the request can leave: a few milliseconds close to the API, and more the farther your client is from it; see Keep HTTP connections open. To open the connection before the first reply as well, call client.models.list() when a call starts; it reads GET /v1/openai/models, which synthesizes nothing and needs no credential. To see where the wait goes, read the Server-Timing header of a 200: it gives the server’s part, and the rest of the time your client measures is the network and the connection setup; see Keep latency low. See Optimizing accuracy and latency for the other habits that shorten the wait.
Voice agent frameworks
LiveKit and Pipecat both have native AssemblyAI plugins, documented on LiveKit and Pipecat, which run on the WebSocket API with one session per call, barge-in without reconnecting and word-timed transcripts. Use those for a new agent. The OpenAI plugins below are for a pipeline already built on them, and give up the word timings. Voice agents should ask forpcm: raw PCM starts playing on the first bytes and needs no decoding, while MP3 adds about 46 ms of encoder silence (at 24 kHz) to the start of every sentence and its first byte arrives later; see Stream raw PCM in the format your player needs.
- LiveKit
- Pipecat
Request fields
model and input are required. voice names the voice, response_format the audio (mp3 by default, pcm for a voice agent, wav, mulaw or alaw), and stream_format: "sse" asks for speech events instead of bytes. Create speech (OpenAI compatible) lists every field, its values and its refusals; the sections below cover the ones that are not plain OpenAI.
Models
Names are case-insensitive. OpenAI’s model names, such as
tts-1, are refused with 400 and the code model_not_found; List models (OpenAI compatible) answers the language model names, and the X-TTS-Model response header reports the model that spoke.
Voices
voice keeps its OpenAI name and takes one of these:
- An OpenAI voice name, in any letter case. Each one speaks with an AssemblyAI voice in its place, as the table below shows.
- The library slug of an AssemblyAI voice, such as
aai-jane, in lowercase: thevoice_idthat names the voice on every other route. - A voice’s ID, a UUID. A cloned voice can also go in as an object.
jane is not one of them. It is refused with 422 (invalid_request_error, code invalid_parameter, param voice), and the message names the slug to send, aai-jane.
In English, an OpenAI voice name speaks with the AssemblyAI voice in this table. In any other language, every OpenAI voice name speaks with
aai-jane, the default voice, though results are usually worse than in the voice’s own language, English; to choose the voice of a Spanish or German request, send the slug or ID of a voice of that language instead. See Voice and language. The response header X-TTS-Voice-Id gives the voice_id the request spoke with, and when you sent an OpenAI voice name, X-TTS-Voice-Alias holds it. Other values are not supported: a misspelt name is refused with 422 like a bare name, and a slug or UUID that names no voice with 400, code invalid_request, both on voice.
Cloned voices
A cloned voice is a voice you make in the AssemblyAI dashboard from a short reference recording of a speaker who consented to it. It belongs to your account, and it can be used for 60 days after its consent was last given unless you renew the consent. A cloned voice goes invoice as an object holding its ID or slug, {"id": "<voice ID>"}, which the OpenAI Python SDK passes through as it is, or as a string holding its UUID. The slug of one of your voices in a plain string is refused: send it as {"id": "<slug>"}. With a model’s name, such as assemblyai-tts-1, the request speaks the voice’s own language unless the language extension names another; with a language’s model, such as assemblyai-tts-es, it speaks that language. assemblyai-tts-1 speaks a cloned voice in all six languages, though results are usually worse than in the voice’s own language; see Voice and language.
400 invalid_request_error with the code voice_not_found, voice_expired, voice_blocked, voice_not_paired, model_retired, model_not_found or not_owner, and biometric_terms_not_accepted answers 403; see Errors.
Extensions
These fields are not part of the OpenAI request; most OpenAI clients pass them through an extra-body option (extra_body in the Python SDK):
The language is the one
language sets, else the one the model name implies, else the language of the voice named by its slug or ID, else English. assemblyai-tts-1 speaks every voice in all six languages, so an English voice can read Spanish text when the request asks for Spanish, though results are usually worse than in the voice’s own language; see Voice and language. Clients that cannot add fields pick a language with the model name.
word_boundaries must be left out or false. Use Stream speech with word timings for them.
Speech events
With"stream_format": "sse" the response is text/event-stream in OpenAI’s form: one data: line per event and no event: lines. While nothing is sent for 15 seconds, a : keepalive comment line arrives; skip lines that start with :.
- Each
speech.audio.deltacarries base64 of the next bytes of theresponse_format: withwav, the WAV header comes first; withmp3, whole MP3 frames. speech.audio.doneends the stream. Itsusagecounts characters, not tokens: the token counts are always0,input_charactersis the length ofinput, andbilled_characterswhat the request is billed. There is no[DONE]line.- A failure after the first audio sends
{"type": "error", "error": {...}}, with the fields of the error body below, and then cuts the connection. Withhttpx, read withiter_bytes()oriter_lines()and nochunk_size, or theerrorevent can be lost with the cut; see Streamed and buffered responses.
Errors
Errors use OpenAI’s body, so OpenAI SDKs raise their usual exceptions (openai.BadRequestError, openai.RateLimitError, …). The code is the one the HTTP API uses:
400 for a bad field, model or voice; 401 for the credential; 402 for funds; 403 (permission_error) with biometric_terms_not_accepted when your account has not accepted the biometric data terms that its own cloned voices need; 422 for a voice string that is not an OpenAI name, a slug or an ID; 429 for both the requests-per-minute limit (rate_limited) and no room just now (at_capacity); and 503 when the service is unavailable.
OpenAI SDKs retry a 429 on their own, twice by default. A request can be refused at_capacity after it was counted against your requests per minute, so each of those retries counts as a request too. To keep retries within your limit, set max_retries on the client and back off for Retry-After yourself.
Every response carries the request’s id in X-Request-Id, and a 200 also in X-Generation-Id; quote it in support requests. Send X-Client-Request-Id (1 to 512 printable ASCII characters) to have your own id echoed back.
Related
- Create speech (OpenAI compatible): the route reference, with every field, status and code.
- List models (OpenAI compatible) and List models (OpenRouter): the public models documents.
- HTTP API basics: hosts, headers, limits and billing shared with the native routes; Word boundaries for word timings over HTTP.
- LiveKit and Pipecat: the native plugins.
- Optimizing accuracy and latency: client habits that shorten the time to first audio.