Skip to content

Errors

Every failure, on REST and on the WebSocket, uses the same 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 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.

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.

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.

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 final you received. Those segments are settled.
  • Discard the last partial. It was never committed.
  • If you reconnect, resume from the end of your last final rather than from the start, so you do not pay twice for audio already transcribed.
import time
import 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")