Error codes
The error envelope, every code the API returns, and when it fires.
Error envelope
Every failure — including schema validation rejections — comes back in this shape, with the HTTP status carrying the same meaning as the code.
json
{
"error": {
"code": "CAMPAIGN_NOT_DRAFT",
"message": "Recipients can only be added to draft campaigns",
"details": { "...": "present only on some codes" },
"timestamp": "2026-08-13T12:00:00.000Z",
"requestId": "req-abc123"
}
}Branch on code, never on message — messages are written for humans and are revised freely. requestId is what to quote in a support request.
details is present on a few codes and carries the machine-usable part: retryAfter (seconds) on RATE_LIMITED; shortfallVideos, estimatedShortfallCents and walletVideoRateCents on WALLET_TOPUP_REQUIRED; and a validation array on schema rejections.
Codes
| Code | HTTP | When |
|---|---|---|
| UNAUTHORIZED | 401 | Missing, malformed or revoked credential. |
| FORBIDDEN | 403 | Authenticated, but not allowed here — e.g. an API key on a dashboard-only endpoint. |
| VALIDATION_ERROR | 400 | Invalid input. Not retryable as sent. Also used at 409 for a few state conflicts (render cancel/retry, campaign unready). |
| NOT_FOUND | 404 | Generic resource miss — renders and library entries. |
| CAMPAIGN_NOT_FOUND | 404 | Unknown campaign id, not yours, or soft-deleted. |
| RECIPIENT_NOT_FOUND | 404 | Unknown recipient, or one under a campaign you do not own. |
| ASSET_NOT_FOUND | 404 | The campaign's base video is missing — usually swept by storage retention. Upload it again. |
| API_KEY_NOT_FOUND | 404 | Unknown or already-revoked API key id. |
| CAMPAIGN_NOT_DRAFT | 409 | A draft-only write (settings, assets, recipients) against a campaign that has left draft. |
| CAMPAIGN_NOT_READY | 409 | A lifecycle action against the wrong status — launching a draft, cancelling a completed campaign, downloading before completion, or a base video still being processed. |
| RECIPIENT_ALREADY_PROCESSED | 409 | Deleting a recipient that is past pending. |
| CONFLICT | 409 | Concurrency and integrity conflicts: deleting a rendering campaign, retrying a processing recipient, base-recording drift on retry, or an idempotent request still in flight (with Retry-After). |
| IDEMPOTENCY_KEY_REUSED | 422 | Same Idempotency-Key, different body. Reuse the original body or mint a new key. |
| RATE_LIMITED | 429 | Over 100 requests/min. Wait for Retry-After and retry. |
| PLAN_LIMIT_REACHED | 402 / 403 | 402 when plan render quota is exhausted with no self-serve wallet rate; 403 when the plan does not include the feature at all (API keys, webhooks). |
| WALLET_TOPUP_REQUIRED | 402 | Plan quota and wallet balance together do not cover the request. Add funds or upgrade. |
| FILE_TOO_LARGE | 400 | A video over the 200 MB limit. |
| INVALID_FILE_TYPE | 400 | An upload that is not mp4, webm or quicktime. |
| INTERNAL_ERROR | 500 | Server-side fault. Safe to retry. |
What to retry
429and500— retry, honoringRetry-Afterwhen present.409— retry only after the state actually changes. ACONFLICTcarryingRetry-Afteris the one that is genuinely worth retrying on a timer.400,401,403,404,422— fix the request; retrying it unchanged returns the same error.402— terminal until you upgrade or add funds. Send any retry with the same Idempotency-Key: a 402 releases the key, so a corrected retry is allowed rather than replayed.
