Skip to main content
This page is about the WebSocket API, whose errors arrive as frames. The HTTP API answers errors with HTTP statuses and a JSON body instead; see HTTP API errors, and The same condition in each API for how the two line up. Errors in the WebSocket API arrive in-band, on a socket that opened successfully. Connecting is not a sign that anything worked: credentials are checked and parameters are validated after the upgrade completes, and both report failure as an Error frame followed by a close. A client that ignores Error frames sees the socket open and then close a moment later with no audio, and nothing in its own logs says why: on a failure close the WebSocket close reason is normally the fixed string See Error message for details. Only when the Error frame could not be delivered, for example to a client that stopped reading, does the close reason carry a shortened copy of its text. Handle Error frames before you write anything else.
error_code equals the WebSocket close code that follows. The code tells you the category, error_type the specific cause as a stable string, and param the connection parameter or message field at fault. retryable says whether reconnecting with the same parameters can succeed, and retry_after_seconds, when it is set, the earliest useful retry. Branch on error_code and error_type. The error text is written for people and may be reworded, so log it and quote it in support requests rather than matching on it. Every failure close the server initiates is preceded by an Error frame, and that frame is the last message of the session: no Termination follows it. A few closes carry no Error frame at all: the clean 1000 after your own Terminate, and the ones the WebSocket layer raises on its own, which are 1006 (the connection was lost), 1009 (a message over 1 MiB), 1011 when a client stops answering pings, and 1002 or 1007 when a client breaks the WebSocket protocol or sends text that is not valid UTF-8. Ignore frame types you do not recognize; new server-to-client types may be added. Sessions open on the path /v1/ws (or /v1/ws/). A request on a wrong path, such as /v1/ws/session or /v1/wss, is answered with HTTP 404 and never becomes a WebSocket, so a client sees a failed handshake rather than an Error frame. A request on the session path with any method other than GET fails the same way, with HTTP 405 and an Allow: GET header, and so do a handshake with two Authorization headers and a GET that announces a body (a Transfer-Encoding header, or a Content-Length other than 0), both with HTTP 400. On the session path, connect-time checks run in a fixed order: room for the connection on the server it reached (3005 at_capacity, before your credentials are read), credentials (1008, or 1011 on an internal error), then connect parameters (3006), then your account’s limit of new sessions per minute (3009), then the session’s language and voice: a language no model of the region speaks or an AssemblyAI voice the session’s model does not speak (3006), with 3005 when the service that routes voices cannot answer. Any other voice in voice_id, such as a cloned voice, is looked up in this step: 3012 when no voice your account can use has that voice_id or the voice cannot be used, 3014 when it is not paired with the session’s model, and 3006 not_owner when extend_biometric_consent is set on a voice that another account owns. Only then does Begin arrive. After Begin, a session the speech service finds no room for closes 3005 at_capacity, in rare cases a voice the session’s model does not speak is refused with 3006 right after Begin rather than before it, and a cloned voice deleted or changed while the session starts or runs closes it with 3012 or 3014. Keep handling Error frames for the whole session.

Close codes

Error types

error_type refines the close code. New values may be added; treat an unknown one by its close code and retryable.

In the SDKs

The Python SDK raises TTSError and the JavaScript SDK TextToSpeechError: from connect() and the with block on a session, and from synthesize() and stream() over HTTP. Both carry the Error frame or the HTTP error body as attributes. The message is str(error) in Python and error.message in JavaScript. The program below is complete and runs as written. It reads Begin before sending anything, so an Error that arrives in its place is handled instead of surfacing as a raw connection error on the first send(), and it maps each close code and error_type to an exception that says what to do next. The Error branch sits inside the normal receive loop on purpose: a WebSocket has one reader, so a separate loop that consumed frames while looking for errors would swallow your audio.

A clean close is still a close

Any close, including a normal 1000, means the session is over. In a browser, every later send() on that socket is silently discarded: no exception, no audio. Server-side libraries usually raise instead (Python websockets raises ConnectionClosedOK). If your client does not listen for the close event, a missed close is indistinguishable from a server that has nothing to say.
The server closes 1000 only after your own Terminate, once Termination has been sent. Anything you send after Terminate, even while the socket is still open, is ignored: no error, no audio, and it does not count toward the session totals. If you close the socket yourself without Terminate, the server cancels the session at once, discards anything not yet spoken, and sends no Termination. On a socket held open across many requests, the close you will actually see is 3008 when the session reaches expires_at, or 3006 with error_type inactivity_timeout when two minutes pass without a frame in either direction (sooner if you set inactivity_timeout), for example because the user went quiet. Neither is a fault. Treat every close as the end of the session: mark the client not ready, and reconnect before the next request rather than discovering the problem when it fails. This matters most in the browser, where a close that goes unnoticed breaks every request that follows. The handler below gates reconnects on the close code, backs off with jitter, and fetches a fresh temporary token for every attempt: each connection gets a token of its own, so a used or expired token is never sent again. setReady, showError, stillNeeded and connect are your own functions; fetchTemporaryToken asks your server for a new token as described in Authenticate with a temporary token.
An idle session also ends with 3006, after two minutes without a frame in either direction or after the shorter inactivity_timeout you set. The handler above does not reconnect then, which is usually what you want: a socket opened at once would sit idle and close again. Open the next socket when the user has something to say, or send KeepAlive at least every two minutes while idle to keep the socket ready.

Retry guidance

  • 1008 is a credential or account problem, and retrying unchanged will not help. Unauthorized: missing Authorization header or token query param means no credential reached the server. Unauthorized: Invalid API key means the key or token was rejected; the raw key and Bearer <key> both work, so check the key itself. Unauthorized: Signature has expired means the temporary token expired: mint a new token and reconnect. A message that names your account or balance, such as Unauthorized: Insufficient funds, has to be fixed on the account first.
  • 3006 for a bad parameter, a malformed frame or an oversized Generate means the request is wrong; error_type and param say what to fix. Fix it before reconnecting; retrying unchanged fails identically. A parameter given more than once reads Bad connection parameters: <parameter>: must be given at most once (given 2 times): send it once. For an AssemblyAI voice this connection does not serve, the error text lists the voices that are available on it, by their voice_id slugs. A 3006 after Begin whose text begins Text contains a and ends Send shorter sentences. means a piece of your text, split as How text is read describes, did not fit the model with this voice. The server splits every text into pieces that fit, so this is rare and points to a fault on our side: report it with the session’s id. Sending the text again in shorter sentences avoids it.
  • 3012 and 3014 concern a cloned voice, and retrying unchanged fails the same way, except for a pairing still in progress. 3012 is a voice that cannot be used: voice_not_found (check the ID, that an AssemblyAI voice is named by its library slug such as aai-jane, and that you connect to the host of the voice’s data zone), voice_expired (create the voice again from a new consented recording; to keep a voice, renew its consent before it expires) or voice_blocked (contact support). 3014 voice_not_paired means the voice has no ready pairing with the session’s model: if the voice was just paired with the model, try again once the pairing is ready; otherwise name a model the voice is paired with in model_id, or check the voice in the AssemblyAI dashboard. The error text says what to do; The texts of 3014 lists them. A 3006 with error_type not_owner means extend_biometric_consent was set on a voice that another account owns: open the session without it.
  • 3006 from the idle timeout is not a request error. Every session closes after two minutes without a frame in either direction, or after the shorter inactivity_timeout you set. Its error_type is inactivity_timeout, and the error text begins No message received for and ends Send KeepAlive frames to hold an idle session open. Reconnect when you have something to say, or send KeepAlive at least every two minutes while idle if you want the socket to stay open.
  • 1011, 1012, 3005 and 3009 are transient. Back off exponentially with jitter, then reconnect, and wait at least retry_after_seconds when the Error frame sets it. 1011 is an internal error while authenticating. 1012 (The server is restarting; reconnect to continue the session.) means the server is being replaced; the next session lands on another one. 3005 is a failure on the server side, for example The TTS service is temporarily unavailable; please reconnect and retry, or Voice service temporarily unavailable; please retry. when the service that routes voices cannot answer at connect; re-send the request that was in flight. A 3005 with error_type at_capacity means the service had no room for the session: This server is at capacity; please retry in 2 seconds. before your credentials are read, or The TTS service is at capacity; please retry in 3 seconds. after Begin. Wait retry_after_seconds before reconnecting, and back off further if it repeats. 3009 (Too many sessions opened for this account in the last minute (allowance 100 per minute); retry in 17 s.) means your account started more sessions in the current one-minute window than its limit allows, and retry_after_seconds, between 1 and 60, is the time until the next window starts: a session opened after that wait is counted in the new window. As for Streaming Speech-to-Text, the limit counts new sessions per minute, not sessions currently open, so closing sessions early does not help; opening fewer does. A voice agent call opens one session, so the limit is the number of calls that can start in a minute. The number in the text is your starting limit; on a paid plan the limit in force can be higher. Keep one socket open and send many requests through it rather than opening a new socket for every utterance. Your limit is your Streaming Speech-to-Text limit, shown on the dashboard, counted separately from your speech-to-text sessions. See Rate limits for how limits scale with usage.
  • 3010 and 3011 mean pacing, not a broken request. 3010 has three triggers: more than 8000 characters sent but not yet synthesized (Input text rate exceeded: ...), more than 20 Flush, KeepAlive, Cancel or empty Generate frames per second sustained (Control frame rate exceeded: ...), or more than 10,000 messages waiting to be handled. Send text at roughly the pace it is spoken and control frames only as the conversation needs them. 3011 means 300 seconds of synthesized audio were waiting for a client that had stopped reading; read from the socket at least as fast as the audio plays. Such a client usually cannot receive the Error frame either and sees 1006 when the socket is torn down. In both cases reconnect, but do not resend the same burst. See Limits.
  • 3008 (Session reached its maximum duration; open a new connection to continue) is routine on a long-lived socket, not a fault. Sessions last one hour by default; Begin.expires_at gives the exact time, and a temporary token with a shorter max_session_duration_seconds brings it forward. At expires_at the server stops taking new input, speaks what you have already sent, then closes. Read expires_at and open a replacement socket a little before it so the next request has somewhere to go.
  • 1006 is the connection being lost without a close handshake, on your side or because the server was replaced. Reconnect.
  • 1009 means a message over 1 MiB, counted after its fragments are joined. A well-formed client cannot reach it, because Generate is capped at 2000 characters; check what else you are putting in the message.
Back off before every reconnect that follows a failure, whatever the code. Every reconnect that gets past the credential and connect parameter checks counts as a new session start, so a tight retry loop, for example after a 3005, can turn a transient failure into a 3009. Every retry is a new session, and a close loses whatever the old one had not finished: text buffered but not yet synthesized, flushes still in flight, and audio not yet delivered. Re-send unfinished requests on the new socket. Because configuration is fixed at connect time, build the connect URL in one function that takes a freshly minted token, so a reconnect cannot drift from the original configuration and a used or expired token is never sent again.

HTTP API errors

The HTTP API reports an error before the first audio as an HTTP status with a JSON body: its own body on /v1/speech and /v1/speech/events, described in HTTP API basics, and OpenAI’s body on the OpenAI-compatible route. Its code is the WebSocket API’s error_type wherever the WebSocket API has one, so one condition carries one name on every route. retryable and Retry-After say whether and when to try again. The status is sent together with the first audio, so every failure before it is a real HTTP status. A failure after the first audio cannot change the status any more: /v1/speech and the OpenAI-compatible route cut the connection before the end of the body, and /v1/speech/events ends its stream with an Error event. Retry a cut request from the start. A cut caused by a server failure is not billed. HTTP API basics lists every code with its status.

The texts of 3014

The error text of a 3014 says what to do. In these texts, <voice ID> is the cloned voice’s UUID, also when you sent its slug, <model> is the model’s name, <reason> is a short code for the cause, and <successor> is the model that replaces a retired one, or the current model when none is named. The HTTP API and the OpenAI-compatible route answer with the same texts.

The same condition in each API

On the OpenAI-compatible route the code field holds the HTTP API’s code; the column shows the status and OpenAI’s type.

Need help?

If you get stuck, contact our support team at support@assemblyai.com or create a support ticket. Include the id from the Begin frame, or the timestamp of the connection attempt if the session never reached Begin.