Skip to content

Errors and debugging

Every status code the speech APIs return, what causes it, and how to recover.

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_id that 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 model or audio_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-Key header — or, for text-to-speech streaming, as the hello frame’s auth_token field. See Authentication.
  • The whole prefix.secret value 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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close