Errors
The API error envelope and the full code table, from validation and invalid_api_key to insufficient_balance, rate_limited and concurrency_limit.
Errors use the standard envelope:
{
"error": {
"code": "validation",
"message": "The request body failed validation; `fields` maps each invalid field to a stable message key.",
"retryable": false,
"requestId": "0f9c2e5a-8f4b-4d61-9a3e-2c7d1b5f0a44",
"fields": { "url": "url_invalid" }
}
}messageexplains what happened in plain text. It is the field to read while integrating, and the one to keep in your logs.codeidentifies the kind of error and is what your code should branch on: always one of the codes below, never an arbitrary string.retryablesays whether repeating the SAME request can succeed. It is the one decision the HTTP status does not answer on its own: a429is worth repeating, a409from idempotency never will be. Branch on it instead of keeping your own table of which errors deserve a retry.requestIdidentifies this response, and is also in theX-Request-Idheader. Keep it in your logs: it is how we find, on our side, what happened on that exact call.fields(only onvalidation) points to a stable key per invalid field.
retryable: true does not mean "retry now": on rate_limited the Retry-After
header says when, and elsewhere exponential backoff with jitter applies.
Codes
| HTTP | code | When it happens |
|---|---|---|
| 400 | validation | Invalid body. See fields. |
| 400 | chunk_empty | Chunk with no content. |
| 400 | upload_mismatch | What arrived does not match the expectedBytes declared. |
| 401 | invalid_api_key | Missing or invalid key in the Authorization header. |
| 402 | insufficient_balance | No credits left. Top up in the dashboard. |
| 404 | not_found | Unknown transcript, or one from another account. |
| 404 | upload_not_found | Unknown upload, or one from another account. |
| 409 | upload_busy | Another chunk of this upload is still being written. |
| 409 | idempotency_conflict | Idempotency-Key reused with a different body. |
| 409 | transcript_processing | A transcript still processing cannot be deleted. |
| 409 | transcript_finished | The transcript already finished and can no longer be canceled. |
| 409 | no_webhook | The transcript has no webhookUrl (or has not finished) to replay. |
| 413 | chunk_too_large | Chunk over 16 MiB. |
| 413 | upload_too_large | File (or upload) would pass 2 GiB. |
| 429 | rate_limited | Rate limit hit. See the Retry-After header. |
| 429 | concurrency_limit | Account with no top-up yet already running 2 transcriptions. |
| 500 | internal | Our fault. Retry; contact support if it persists. |
The API reference documents, per endpoint, exactly which codes each status can return.
Rate limits
Every response from a limited route carries the three headers, not just the
429:
| Header | What it says |
|---|---|
RateLimit-Limit | The ceiling for the window |
RateLimit-Remaining | How many requests still fit in it |
RateLimit-Reset | In how many seconds the next slot opens |
They are what lets you slow down BEFORE hitting the ceiling instead of finding it
by being refused. 429 rate_limited also carries Retry-After with the real
wait, in seconds. The ceilings are per account, on a sliding window:
| Path | Per minute |
|---|---|
POST /v1/transcripts | 120 |
POST /v1/uploads | 120 |
PUT /v1/uploads/{id} | 600 |
A backlog releasing all at once reaches these easily, because a customer's peak
looks nothing like their average traffic. Spread the burst over the Retry-After
wait instead of retrying immediately; nothing is lost, only delayed. If you need
more, talk to us before the migration starts.
Validation keys
| Field | Key | Meaning |
|---|---|---|
url | url_invalid | Not a valid http/https URL. |
url | url_or_upload_id | Send url or uploadId, exactly one. |
uploadId | upload_id_invalid | Invalid format. |
uploadId | upload_empty | The upload received no bytes. |
language | language_invalid | Invalid language code. |
speakers | speakers_invalid | Not true, an integer from 1 to 20, or "min-max". |
idempotencyKey | idempotency_key_invalid | Longer than 64 characters. |
webhookUrl | webhook_url_invalid | Not a reachable public http/https URL. |
glossary | glossary_invalid | Over 100 entries, or an entry with an empty side or over 80 characters. |
reuseIfIdentical | reuse_needs_sha256 | The upload was created without sha256, so the content cannot be recognised. |
expectedBytes | expected_bytes_invalid | Not a positive integer within the 2 GiB limit. |
sha256 | sha256_invalid | Not 64 hexadecimal characters. |
since / until | date_invalid | Not a valid ISO 8601 date-time. |
cursor | cursor_invalid | The cursor did not come from a nextCursor of this API. |
metadata | metadata_invalid | Over 20 pairs, or a key/value over its length limit. |
externalId | external_id_invalid | Empty, or over 128 characters. |
retentionDays | retention_invalid | Outside the 1 to 365 day range. |
wait | wait_invalid | Outside the 1 to 60 second range. |
Transcript failures
When a transcript ends in status: "failed", its error field carries
{ code, message } with the reason. Failed transcripts are never charged.
error.code | Meaning |
|---|---|
audio_too_long | Audio longer than the 10 hour limit. |
insufficient_balance | The audio is longer than the remaining balance covers. Top up and resend. |
audio_unreachable | The url could not be downloaded. |
audio_invalid | The audio could not be decoded. |
audio_too_large | The downloaded file is over 2 GiB. |
audio_mismatch | The stored audio does not match the sha256 declared on the upload. |
engine_failed | Failed on every retry. Nothing was charged; send it again. |
status: "canceled" is not a failure: it is a transcript you stopped with
POST /v1/transcripts/{id}/cancel. It is terminal, error stays null, and
nothing is charged.
Accuracy & quality
6.34% WER in Portuguese and 4.6% diarization DER, measured by us against human transcripts, with the protocol behind every number stated.
Safe retries
Idempotency-Key, the retryable field, long polling with ?wait=, cancellation, cursors and the limits the API reports about itself: what an integration needs to defend itself.