Failure handling
Errors and retries
Every error has one stable envelope. Display a safe user message, keep request_id for diagnostics, and branch on code rather than parsing message text.
{
"error": {
"code": "validation_error",
"message": "The request body did not match the documented schema.",
"request_id": "safe-correlation-identifier",
"details": { "fields": [{ "path": "input", "message": "Required" }] }
}
}| HTTP | Code | Quota charged? | Meaning and action |
|---|---|---|---|
| 400 | bad_request | No | Fix malformed JSON or the Idempotency-Key, then retry with the same logical key when the payload is unchanged. |
| 401 | unauthorized | No | Replace an invalid, expired, depleted, suspended, or revoked key. |
| 402 | quota_exhausted | No | Purchase another package or use a key with enough remaining units. |
| 403 | forbidden | No | Use the key only with its purchased operation. |
| 404 | not_found | No | Check the owned upload or job identifier; do not retry a different key. |
| 409 | conflict | No | Poll or retry later with the same idempotency key. |
| 413 | bad_request | No | Reduce the JSON body or upload to the documented package limit. |
| 415 | bad_request | No | Send strict JSON with Content-Type application/json. |
| 422 | validation_error | No | Match the strict documented schema and package limits. |
| 429 | rate_limited | No | Back off with jitter and retry the same logical request. |
| 500 | internal_error | No unless explicitly documented otherwise | Contact support with request_id before changing the idempotency key. |
| 503 | provider_unavailable | No | Retry safely with the same idempotency key; no quota is charged for confirmed non-chargeable failures. |
| 503 | service_not_configured | No | The required engine or storage dependency is not active. Wait or contact support; do not create a new logical request. |
Backoff algorithm
const retryable = new Set([409, 429, 503]);
for (let attempt = 0; attempt < 5; attempt++) {
const response = await sendWithTheSameIdempotencyKey();
if (response.ok) return response.json();
if (!retryable.has(response.status)) throw await toApiError(response);
const capMs = Math.min(30_000, 500 * 2 ** attempt);
await new Promise(resolve => setTimeout(resolve, Math.random() * capMs));
}Never retry payment or generation by blindly creating a new idempotency key. Preserve the original response body and safe request ID. A 500 is not automatically safe to replay under a new identifier.