Error shape
A failure comes back as JSON carrying a machine-readable code, a human-readable message, and the status:
{
"error": "invalid_transaction_id",
"message": "A valid UUID is required for the transaction ID.",
"code": 400,
"timestamp": "2026-09-09T19:36:33+05:30"
}| Field | Description |
|---|---|
error |
Stable code identifying what went wrong. Branch on this. |
message |
Human-readable detail. Log it, but do not match on it, since wording can change. |
code |
Mirrors the HTTP status. |
timestamp |
When the error was generated, RFC 3339. |
Some responses carry a status field set to error alongside these.
On a WebSocket, failures before the socket opens come back as a normal HTTP
response on the upgrade, so you get a readable status rather than a silent
close. Once the socket is open, a failure arrives as a frame carrying an
error field. Check for it on every frame you parse, before reading text.
Every status code
The request is malformed. Deterministic, so do not retry without changing something.
Common causes:
- A
transaction_idthat is not a valid UUID. Any other string is rejected, even though it may look reasonable. - A model name that does not exist, usually a typo. See Models and languages.
- Malformed JSON in a streaming config frame.
- A missing required field, such as
modeloraudio_file.
The API key is missing, malformed, or does not match any account.
On synthesis this is the only auth failure status: a key that is
wrong, revoked, out of credit, or missing the tts:synth scope all come
back as 401. Transcription splits those across 402 and 403.
Check, in this order:
- The key was actually sent, as an
X-API-Keyheader — or, for text-to-speech streaming, as thehelloframe’sauth_tokenfield. See Authentication. - The whole
prefix.secretvalue was sent, not just the prefix. - No trailing newline or whitespace crept in from a shell variable.
- The key has not been revoked. Creating a new key revokes the previous one, so a caller that missed a rotation starts failing here. See API keys.
The account does not have enough credits to process the request.
Transcription only. Synthesis returns 401 for an empty balance,
the same as for any other refused key.
Do not retry a 402. The same request fails identically until the
balance is topped up. Top up on the Billing page in the app, or
enable auto-recharge so the next call succeeds without manual
intervention.
For streaming, this can also end a connection that was already running,
so keep some headroom rather than running the balance down to zero. On
the synthesis socket the same situation arrives as an
insufficient_funds frame.
Either the account is inactive, or the key does not carry the scope this endpoint requires.
Transcription only. Synthesis returns 401 for both.
An inactive account needs support. A scope problem needs a different key, though every key created today is granted all scopes.
An unexpected server-side failure. Not something the caller can fix by changing the request.
Retry once with backoff. If it persists, capture your transaction_id
and the call_id if you got one, and report them. Those are what tie
your failure to the server-side record.
The service is unavailable or temporarily overloaded.
Retryable, with exponential backoff, jitter, and a cap on attempts.
Unlike a 400, the same request may well succeed shortly afterwards.
Error codes
The error field carries one of these. Authentication and balance failures are
covered by the statuses above.
| Code | Status | Meaning |
|---|---|---|
invalid_request |
400 |
The request arguments could not be parsed. |
invalid_transaction_id |
400 |
transaction_id is not a valid UUID. |
model_not_found |
400 |
The model you named does not exist. Check it against Models and languages. |
invalid_hotwords_format |
400 |
hotwords could not be parsed as JSON. |
failed_to_get_audio_file |
400 |
No audio file was present in the request. |
failed_to_read_audio_file |
400, 500 |
The upload could not be read. Check the file is not truncated or corrupt. |
audio_conversion_failed |
400 |
The uploaded audio could not be decoded. |
invalid_audio_file_format |
400 |
The upload is not an audio format the service can read. |
audio_processing_failed |
400, 500 |
The audio could not be processed. |
failed_to_transcribe_audio |
500 |
Transcription failed after the audio was accepted. |
config_already_initialized |
400 |
Streaming only. A second config frame was sent on one connection. |
internal_server_error |
500 |
An unexpected failure. |
Errors on a stream
Streaming fails in two places, and they behave differently.
Before the socket opens, the upgrade is rejected as a normal HTTP response. Every status above applies, and you can read it the same way you would any failed request.
After the socket opens, a failure arrives as a frame with an error field:
async for raw in socket:
frame = json.loads(raw)
if "error" in frame:
raise RuntimeError(frame)
...Frames you can see here include {"error": "unexpected connection close"},
{"error": "read error: …"}, and the idle timeout below.
Debugging workflow
Capture your transaction_id and call_id
You generate transaction_id on every request, and the response carries a
call_id. Log both on success and failure. They are the handles that tie
your logs to the server side.
Isolate the key
Make the smallest possible call with the same key. If a tiny file also
fails 401, the problem is the key. If it succeeds, the problem is the
request.
Read the status, then the message
A 400 means fix the request. A 402 means top up. A 503 means retry.
Only the message tells you which field was wrong.
Check the app
Usage & Analytics shows whether requests are arriving at all, and Billing shows your balance.
Common integration mistakes
| Symptom | Likely cause |
|---|---|
400 on a request that looks correct |
transaction_id is not a valid UUID, or the model name has a typo. |
401 right after creating a key |
Creating a key revokes the previous one, and a caller still holds the old value. |
401 with a key that works elsewhere |
A trailing newline from a shell variable, or only the prefix was sent instead of the whole prefix.secret value. |
| Confident but wrong transcript | The model’s language does not match the audio, or the declared sample_rate does not match what you are sending. |
| Transcripts stop arriving but the socket stays open | You are waiting on a fixed delay instead of the eos frame. |
| Every stream cutoff logged twice | The close after an error frame is being counted as a second failure. |
| Stream cut off with credits apparently remaining | The balance ran out mid-connection. Keep headroom. |