Errors
The error envelope
Section titled “The error envelope”Errors use the same envelope as successful responses. success is false, data is null, and error explains what went wrong.
{ "success": false, "code": 403, "message": "You have reached your plan limit.", "data": null, "error": { "code": "PLAN_LIMIT_EXCEEDED", "details": { "limit": "concurrency", "max": 5, "used": 5 } }, "meta": { "requestId": "019a4f30-1c22-7a10-b5d3-42f1c0a8e777", "timestamp": "2026-12-01T09:41:07.512Z" }}The numbers in details are illustrative.
coderepeats the HTTP status.error.codeis the stable, machine-readable identifier inUPPER_SNAKE_CASE. Branch on this and on the HTTP status.messageis for people. Its wording can change, so never match on it.error.detailsis an object. Its keys depend on the code and are listed below. It can be empty.meta.requestIdidentifies the request. Include it when you contact support.
Error messages never contain transcript text, titles or file names.
Validation errors
Section titled “Validation errors”400 VALIDATION_ERROR lists every invalid field in error.details.fields:
{ "error": { "code": "VALIDATION_ERROR", "details": { "fields": [ { "field": "filename", "reason": "required" }, { "field": "options.language", "reason": "invalid_enum" } ] } }}reason is one of required, invalid, too_short, too_long, out_of_range, invalid_enum or unknown_field. Some endpoints use more specific values, such as invalid_boolean on Export. Treat an unfamiliar reason as a generic invalid value.
A body field the API does not know is unknown_field. The API does this on purpose, so a misspelled option does not pass silently.
Error codes
Section titled “Error codes”| Status | error.code |
Meaning | error.details |
|---|---|---|---|
| 400 | VALIDATION_ERROR |
A field or query parameter is missing or invalid. | fields: a list of { field, reason }. |
| 400 | FILE_TOO_LARGE |
The file is above the plan’s size limit. | maxBytes. |
| 400 | UNSUPPORTED_MEDIA_TYPE |
The file type is not supported. | extension, contentType. |
| 400 | UPLOAD_INVALID |
The parts you sent do not match the upload. | reason: part_count_mismatch, invalid_parts, missing_object or size_mismatch. |
| 401 | UNAUTHENTICATED |
The API key is missing, malformed, unknown or revoked. | Empty. The response has WWW-Authenticate: Bearer. |
| 403 | FEATURE_NOT_IN_PLAN |
The owner’s plan does not include the API. | feature, requiredPlan. |
| 403 | PLAN_LIMIT_EXCEEDED |
A plan limit blocks the request. | limit, max, used, and resetAt when the limit resets. |
| 403 | ACCOUNT_RESTRICTED |
The account can read and export, but cannot start new processing. | Empty. |
| 403 | ACCOUNT_SUSPENDED |
The account is banned or is being deleted. | reason: banned or deletion_pending. |
| 404 | NOT_FOUND |
The resource does not exist, belongs to another account, or the path is not served on this host. | Empty. |
| 405 | METHOD_NOT_ALLOWED |
The path exists but not for this HTTP method. | Empty. |
| 409 | INVALID_STATE_TRANSITION |
The resource is not in a state that allows this action. | reason: not_pending, not_ready or upload_expired, or status and action. |
| 410 | MEDIA_EXPIRED |
The original file was already deleted. | object: original. |
| 413 | PAYLOAD_TOO_LARGE |
The request body is larger than the endpoint accepts. | Empty. |
| 415 | UNSUPPORTED_MEDIA_TYPE |
The request Content-Type is not application/json. |
Empty. |
| 429 | RATE_LIMITED |
You are sending requests too fast. | policy, retryAfterSeconds. See Rate limits. |
| 500 | INTERNAL_ERROR |
An unexpected failure on our side. | Empty. |
| 503 | EXPORT_FAILED |
A file could not be generated. | format. |
| 503 | SERVICE_UNAVAILABLE |
A dependency or the service is not available right now. | reason: uploads_paused, rate_limiter_unavailable, database_unavailable or maintenance. |
UNSUPPORTED_MEDIA_TYPE appears twice because it covers two cases. With status 400 the uploaded file’s type is not accepted. With status 415 your request body was not sent as JSON.
Plan limits
Section titled “Plan limits”PLAN_LIMIT_EXCEEDED is the only code for limits that come from the plan. details.limit says which one. On this API you will mostly see concurrency, which means too many uploads and jobs are open at once. max is the plan’s value and used is where you are. When the limit resets at a fixed time, resetAt is that UTC time. It is left out for concurrency, which frees up as jobs finish.
Read the current values from Usage instead of hardcoding them.
Transcription errors inside a result
Section titled “Transcription errors inside a result”A transcription that ended as failed carries its own errorCode (MEDIA_UNREADABLE, NO_AUDIO_STREAM, INTERNAL_ERROR or WORKER_TIMEOUT). That is a property of the job and arrives with a normal 200 response. It is not an error.code. See Transcriptions.
What to retry
Section titled “What to retry”| Failure | Retry? | What to do |
|---|---|---|
429 RATE_LIMITED |
Yes | Wait for the Retry-After header, then retry. |
503 SERVICE_UNAVAILABLE, 503 EXPORT_FAILED |
Yes | Retry with exponential backoff and a little random jitter. For uploads_paused, start with a longer wait. |
500 INTERNAL_ERROR |
Yes, a few times | Back off. If it keeps happening, contact support with the requestId. |
| Network timeout or dropped connection | Yes, with care | GET requests are safe to repeat. For POST /media, check GET /media?status=pending_upload first, so you do not create a duplicate. POST /media/{id}/complete is safe to repeat. |
401, 403, 404, 405, 409, 410, 413, 415 |
No | The request or the account state must change first. |
400 |
No | Fix the request. Read error.details. |
Log the requestId of every failed call, and nothing from the request body.
Can't find what you need? Contact support, email [email protected], or open Help and then Contact in the app. scribewren.com