Errors
Every failure, on REST and on the WebSocket, uses the same envelope.
The envelope
Section titled “The envelope”{ "error": { "code": "tier_unavailable", "message": "Tier 'ultra' is not available on this deployment.", "request_id": "req_01JD4Z2Q8W6M" }}| Field | Notes |
|---|---|
code |
A stable string. Branch on this. |
message |
Written for a human reading a log. Wording may change, do not parse it. |
request_id |
Quote this when you contact support. |
On the WebSocket the same object arrives wrapped in an event, and the socket closes afterwards:
{ "type": "error", "error": { "code": "invalid_audio_format", "message": "Expected float32 PCM. Frame length is not a multiple of 4 bytes.", "request_id": "req_01JD4Z2Q8W6M" }}Status codes
Section titled “Status codes”| Status | Meaning |
|---|---|
400 |
The request is malformed, or asks for something that cannot be served. |
401 |
The API key is missing, malformed or revoked. |
402 |
The credit balance is too low to start the request. |
403 |
The key is valid but not permitted to do this. |
404 |
No such path. |
413 |
The uploaded file is larger than the endpoint accepts. |
415 |
The uploaded media type cannot be decoded. |
422 |
A parameter is the right shape but not an accepted value. |
429 |
Rate limited. |
500 |
Something failed on our side. |
503 |
Capacity is unavailable right now. |
code |
Status | What to do |
|---|---|---|
invalid_api_key |
401 |
Check the key, or create a new one in the console. |
missing_api_key |
401 |
Send Authorization: Bearer utt_live_…. |
payment_required |
402 |
Add credits. See Pricing and credits. |
permission_denied |
403 |
The key cannot use this endpoint. |
invalid_request |
400 |
Fix the request. The message says what is wrong. |
unsupported_language |
422 |
Use a value from Languages. |
unknown_tier |
422 |
Use one of the four tier names. |
tier_unavailable |
400 |
This deployment cannot serve that tier. See Availability. |
invalid_audio_format |
400 |
Frames must be float32 mono little endian PCM. |
unsupported_media_type |
415 |
Batch only. Convert the file. |
file_too_large |
413 |
Batch only. Split the file, or stream it. |
no_audio_received |
400 |
"eof" arrived before any audio frame did. |
rate_limited |
429 |
Back off and retry. |
internal_error |
500 |
Retry. If it persists, send us the request_id. |
capacity_unavailable |
503 |
Retry with backoff. |
Out of credit
Section titled “Out of credit”utter is prepaid. If the balance is below the minimum hold, the request is refused before any transcription happens, so nothing is transcribed and nothing is debited.
{ "error": { "code": "payment_required", "message": "Credit balance is too low to start this request.", "request_id": "req_01JD4Z2Q8W6M" }}On a WebSocket this arrives as an error event in place of ready, and the socket
closes. Retrying will not help until credits are added. See
Pricing and credits.
What to retry
Section titled “What to retry”Retry 429, 500 and 503, with exponential backoff and jitter.
Do not retry 400, 401, 402, 403, 413, 415 or 422. The same request
will fail the same way. The one case worth special handling is tier_unavailable: retry once
on the fallback tier you chose in advance, then give up.
Failures on a stream
Section titled “Failures on a stream”The WebSocket may also close without an error event, from a network drop or an
idle timeout. Treat any close before you have seen done as a failure of that
session, and handle it the same way:
- Keep every
finalyou received. Those segments are settled. - Discard the last
partial. It was never committed. - If you reconnect, resume from the end of your last
finalrather than from the start, so you do not pay twice for audio already transcribed.
Example
Section titled “Example”import timeimport requests
RETRY = {429, 500, 503}
def transcribe(path: str, key: str, attempts: int = 4) -> dict: for attempt in range(attempts): with open(path, "rb") as f: response = requests.post( "https://api.utter.cc/v1/transcribe", headers={"Authorization": f"Bearer {key}"}, files={"file": (path, f, "audio/wav")}, timeout=300, ) if response.status_code == 200: return response.json()
error = response.json().get("error", {}) if response.status_code not in RETRY or attempt == attempts - 1: raise RuntimeError( f"{error.get('code')}: {error.get('message')} " f"(request_id {error.get('request_id')})" ) time.sleep(2**attempt) raise AssertionError("unreachable")