Errors and retries
Stable machine codes, what each one means, and which requests are safe to send again.
Every failure has the same shape:
Branch on code, never on message. The code is part of the contract; the message is written for a person reading a log and gets reworded whenever a clearer sentence turns up. Some codes add detail fields next to it (scope, granted, remaining, restriction, field, conflictCode, rev). New codes can appear in v1: treat an unknown one as a generic failure of its HTTP status rather than crashing.
Codes#
| HTTP | Code | Retry? |
|---|---|---|
| 400 (409) | invalid_request | No; fix the request. For 409, re-read the project and send again. |
| 401 | unauthorized | No, until the credential is fixed. |
| 402 | plan_required | No; upgrade the plan. |
| 402 | insufficient_credits | After topping up. |
| 402 (413) | quota_exceeded | When the allowance frees up (a render finishes, the month resets). |
| 403 | insufficient_scope | No; mint a key with that scope. |
| 403 | access_suspended | No. |
| 403 | app_suspended | No; check the contact address on the app. |
| 404 | not_found | No. |
| 413 | payload_too_large | No; import from a URL or use a smaller file. |
| 422 | prompt_rejected | No; reword it. |
| 429 | rate_limited | Yes, after Retry-After seconds. |
| 500 | internal_error | Once, with backoff, after re-reading state. |
| 501 | not_configured | Later. |
| 502 | provider_error | Yes, with backoff. |
| 503 | unavailable | Yes, after Retry-After (30 seconds). |
invalid_request#
400 (409). The request is malformed: a missing or wrong-typed field, a body that is not a JSON object, a bad command list. Also sent with status 409 when a project changed while your commands ran (conflictCode: "stale_rev", with the current rev). Retry: No; fix the request. For 409, re-read the project and send again.
unauthorized#
401. No bearer token, or one that is unknown, revoked or expired. Retry: No, until the credential is fixed.
plan_required#
402. The owner's plan does not include this feature. The scope was necessary but not sufficient. Retry: No; upgrade the plan.
insufficient_credits#
402. Not enough credits for this call. Nothing ran and nothing was charged. remaining is the balance. Also used when video generation is not available on a trial or a full-waiver promotion. Retry: After topping up.
quota_exceeded#
402 (413). An allowance is used up: the plan's project limit, monthly exports, renders running at once, one video job at a time. Sent with 413 when cloud storage is full. Retry: When the allowance frees up (a render finishes, the month resets).
insufficient_scope#
403. The credential is valid but lacks a scope. scope names the one needed and granted lists what the credential has. Retry: No; mint a key with that scope.
access_suspended#
403. The owning account is restricted. restriction says what is limited, until when, and links to the appeal. Retry: No.
app_suspended#
403. This app's kill switch is on, independent of the account's standing. Retry: No; check the contact address on the app.
not_found#
404. No such resource, or it belongs to somebody else. The two are deliberately indistinguishable. Retry: No.
payload_too_large#
413. An upload is over 32 MB or over the plan's upload cap, or an import is over the import limit. Retry: No; import from a URL or use a smaller file.
prompt_rejected#
422. A prompt failed the safety check. field names the field. Nothing was charged. Retry: No; reword it.
rate_limited#
429. Too many requests in the window. Refused requests do not count against the limit. Retry: Yes, after Retry-After seconds.
internal_error#
500. Our fault. Nothing is implied about whether the work happened. Retry: Once, with backoff, after re-reading state.
not_configured#
501. A provider this call needs (speech-to-text, a stock library) is not available. Nothing was charged. Retry: Later.
provider_error#
502. An upstream failed: a generation provider, a URL you asked us to import, a plugin, a frame render. A charged call is refunded. Retry: Yes, with backoff.
unavailable#
503. The rate limiter could not be reached, so the request was refused without running or charging anything. Retry: Yes, after Retry-After (30 seconds).
Retrying safely#
There is no Idempotency-Key header. Decide what to retry by what the call does:
| Request | After a timeout or a 5xx |
|---|---|
Any GET | Safe to retry. |
PATCH and PUT | Safe to retry: they set state, so repeating one gives the same result. |
DELETE | Safe; a second call answers 404. |
POST /v1/projects/{id}/commands | Not safe. A repeated batch adds its clips again. Re-read the project first. |
POST /v1/projects | Not safe. It creates a second project. List projects first. |
POST /v1/renders | Not safe. It queues a second render and uses another export. List GET /v1/renders?projectId=… first. |
Generation (/v1/ai/*) | Not safe. A repeat is charged again. A failure that was our fault (502) is refunded, so retrying one of those is fine. |
Back off exponentially (1, 2, 4, 8 seconds, with jitter) and give up after a few attempts. Honour Retry-After when it is present.
Rate limits#
Limits are per key, per minute, by plan. See rate limits.