Errors
Every error response (4xx and 5xx) uses a stable envelope. Branch on error.type, not on the HTTP status code alone. Keep request_id in your logs so failed requests can be traced.
Error envelope
error.type is a stable enum value (see the taxonomy below). error.message is human-readable and may include specifics like remaining-vs-needed counts. error.details is optional, present only on types that carry structured detail. request_id is an opaque identifier that also appears in the X-Request-Id response header. Treat it as a string of unspecified shape: if you send your own X-Request-Id, it is echoed back verbatim.
{
"error": {
"type": "rate_limited",
"message": "Rate limit exceeded"
},
"request_id": "req_a1b2c3d4e5f6a7b8c9d0e1f2"
}The same request_id is returned in the X-Request-Id response header on every response, success or failure. If you send X-Request-Id with a value up to 128 characters, the server uses it instead. This is useful when matching API responses to your own logs.
Error type taxonomy
The values of error.type are part of the wire contract: never renamed, never removed, only added.
| Type | HTTP | Meaning |
|---|---|---|
bad_request | 400 | Validation failed: malformed video ID, missing required body field, bad Idempotency-Key, etc. |
unauthorized | 401 | Missing or invalid API key. See Authentication. |
insufficient_quota | 402 | Not enough credits to fulfill the request. See Quotas. |
forbidden | 403 | Authenticated but the plan doesn't allow this action, such as a free plan submitting a bulk job. Also returned when a video is unavailable, age-restricted, or geo-blocked, which can be temporary. |
not_found | 404 | Resource doesn't exist or doesn't belong to your account. Also used for missing captions, an unavailable requested caption language, or disabled video comments. Read the message for the cause; these failed fetches do not charge credits. |
conflict | 409 | Generic conflict, for example canceling an already-canceled job, exporting a job that's still processing. |
idempotency_conflict | 409 | Idempotency-Key reused with a different body, or duplicate request in flight. See Idempotency. |
gone | 410 | Resource existed but is no longer available. Today this fires when a bulk-job export has expired (100 days after terminal). |
request_too_large | 413 | Bulk job exceeds your plan's per-job video cap. |
rate_limited | 429 | Per-minute rate limit exceeded, or active-bulk-job cap reached. See Rate limits. |
service_degraded | 503 | Bulk job submissions are temporarily unavailable. Sync transcript requests still run. |
internal_error | 5xx | Server-side error or upstream failure. Customers can see HTTP 500, 502, or 504 all carrying this type. Retry with backoff. |
Recommended client behavior
Branch on error.type:
const res = await fetch(url, { headers });
if (!res.ok) {
const body = await res.json();
const { type, message } = body.error;
const requestId = body.request_id;
switch (type) {
case "rate_limited":
// POST /jobs: per-minute throttle -> wait, same key
// explicit active-job-cap rejection -> wait for capacity, new key
break;
case "insufficient_quota":
// surface to the user, link to /pricing
break;
case "service_degraded":
// backoff, try again in a few minutes
break;
case "unauthorized":
// not retryable: fix the key
break;
case "forbidden":
// plan limits are not retryable, but an unavailable video can be
// temporary: retry once before giving up on it
break;
case "internal_error":
// log requestId, retry with exponential backoff
break;
default:
// 4xx: fix the request before retrying
break;
}
}Type stability
Error types are only added, never renamed. Treat unknown types by falling back to the HTTP status code.