Errors
Every failure, on REST and on the WebSocket, uses the same envelope.
The envelope
Section titled “The envelope”{ "error": { "type": "invalid_tier", "message": "Tier 'ultra' is not available on this deployment.", "request_id": "req_01JD4Z2Q8W6M" }}| Field | Notes |
|---|---|
type |
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 error arrives as an event and the socket closes afterwards.
The event is flat: error is the type string, with message and request_id
alongside it.
{ "type": "error", "error": "undecodable_audio", "message": "The audio could not be decoded.", "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. |
422 |
The request body or query did not validate. |
429 |
Rate limited. |
500 |
Something failed on our side. |
503 |
Capacity is unavailable right now. |
Branch on type.
type |
Status | What to do |
|---|---|---|
unauthorized |
401 |
Send Authorization: Bearer utt_live_…, or check the key is not revoked. |
payment_required |
402 |
The balance is too low to start the request. |
forbidden |
403 |
The key is valid but not permitted to do this. |
not_found |
404 |
No such path. |
invalid_tier |
400 |
Use one of the four tier names, and one this deployment serves. See Latency tiers. |
empty_audio |
400 |
The upload was empty. |
undecodable_audio |
400 |
The audio could not be decoded. Send float32 mono PCM on the stream, or a supported container to batch. |
no_audio_received |
400 |
"eof" arrived before any audio frame did. |
audio_too_large |
413 |
The upload is over 200 MB. Split it, or stream it. |
invalid_request |
422 |
A parameter did not validate. The message says what is wrong. |
rate_limited |
429 |
Back off and retry. |
internal_error |
500 |
Retry. If it persists, send us the request_id. |
upstream_unavailable |
503 |
The serving layer is unavailable. 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": { "type": "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 or 422. The same request
will fail the same way. The one case worth special handling is invalid_tier from an unavailable tier:
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('type')}: {error.get('message')} " f"(request_id {error.get('request_id')})" ) time.sleep(2**attempt) raise AssertionError("unreachable")