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 raisesTTSError 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
The server closes1000 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.
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
1008is a credential or account problem, and retrying unchanged will not help.Unauthorized: missing Authorization header or token query parammeans no credential reached the server.Unauthorized: Invalid API keymeans the key or token was rejected; the raw key andBearer <key>both work, so check the key itself.Unauthorized: Signature has expiredmeans the temporary token expired: mint a new token and reconnect. A message that names your account or balance, such asUnauthorized: Insufficient funds, has to be fixed on the account first.3006for a bad parameter, a malformed frame or an oversizedGeneratemeans the request is wrong;error_typeandparamsay what to fix. Fix it before reconnecting; retrying unchanged fails identically. A parameter given more than once readsBad connection parameters: <parameter>: must be given at most once (given 2 times): send it once. For an AssemblyAI voice this connection does not serve, theerrortext lists the voices that are available on it, by theirvoice_idslugs. A3006afterBeginwhose text beginsText contains aand endsSend 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’sid. Sending the text again in shorter sentences avoids it.3012and3014concern a cloned voice, and retrying unchanged fails the same way, except for a pairing still in progress.3012is a voice that cannot be used:voice_not_found(check the ID, that an AssemblyAI voice is named by its library slug such asaai-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) orvoice_blocked(contact support).3014voice_not_pairedmeans 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 inmodel_id, or check the voice in the AssemblyAI dashboard. Theerrortext says what to do; The texts of 3014 lists them. A3006witherror_typenot_ownermeansextend_biometric_consentwas set on a voice that another account owns: open the session without it.3006from the idle timeout is not a request error. Every session closes after two minutes without a frame in either direction, or after the shorterinactivity_timeoutyou set. Itserror_typeisinactivity_timeout, and theerrortext beginsNo message received forand endsSend KeepAlive frames to hold an idle session open.Reconnect when you have something to say, or sendKeepAliveat least every two minutes while idle if you want the socket to stay open.1011,1012,3005and3009are transient. Back off exponentially with jitter, then reconnect, and wait at leastretry_after_secondswhen theErrorframe sets it.1011is 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.3005is a failure on the server side, for exampleThe TTS service is temporarily unavailable; please reconnect and retry, orVoice service temporarily unavailable; please retry.when the service that routes voices cannot answer at connect; re-send the request that was in flight. A3005witherror_typeat_capacitymeans the service had no room for the session:This server is at capacity; please retry in 2 seconds.before your credentials are read, orThe TTS service is at capacity; please retry in 3 seconds.afterBegin. Waitretry_after_secondsbefore 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, andretry_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.3010and3011mean pacing, not a broken request.3010has three triggers: more than 8000 characters sent but not yet synthesized (Input text rate exceeded: ...), more than 20Flush,KeepAlive,Cancelor emptyGenerateframes 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.3011means 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 theErrorframe either and sees1006when 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_atgives the exact time, and a temporary token with a shortermax_session_duration_secondsbrings it forward. Atexpires_atthe server stops taking new input, speaks what you have already sent, then closes. Readexpires_atand open a replacement socket a little before it so the next request has somewhere to go.1006is the connection being lost without a close handshake, on your side or because the server was replaced. Reconnect.1009means a message over 1 MiB, counted after its fragments are joined. A well-formed client cannot reach it, becauseGenerateis capped at 2000 characters; check what else you are putting in the message.
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
Theerror 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.
Related
- Message frames: the
Errorframe and where it sits in the frame order. - Limits: the thresholds behind
3006,3008,3009,3010and3011. - WebSocket API basics:
inactivity_timeout, and the parameters a3006can name. - Voice cloning: the errors of a session with a cloned voice, and what to do about each.
- Authenticate with a temporary token: minting the token a browser client reconnects with.
- HTTP API basics: the error bodies and codes of the HTTP API.
Need help?
If you get stuck, contact our support team at support@assemblyai.com or create a support ticket. Include theid
from the Begin frame, or the timestamp of the connection attempt if the
session never reached Begin.