Error response shape
Every Generate endpoint (/generate-and-post, /jobs, /blog-to-video, /videos/magic, /videos/text-to-video, /videos/image-to-video, /videos/ai-influencer, /clipper/youtube, /avatars, /batch) answers every error in this one envelope:
{
"success": false,
"error": {
"code": "INPUT_INVALID",
"message": "prompt: Prompt must be 1000 characters or less",
"retryable": false,
"details": { ... } // optional, code-specific
},
"meta": { "request_id": "8f0c…" }
}- error.code — stable identifier. Branch on this.
- error.message — human-readable explanation for developers.
- error.retryable — whether retrying the same request later can succeed. Always a boolean.
- error.details — optional, code-specific context (for validation errors, the failing fields). Some codes also put extra fields directly on
error(for example the 402cost_usd_centsandbalance_usd_cents). - meta.request_id — the same value as the
X-Request-Idresponse header. Include it when contacting support.
Validation errors are always 400 INPUT_INVALID and are never charged: an invalid AI video request (for example a prompt over 1,000 characters or an unsupported resolution) is rejected before billing. Older responses also carried a top-level message; it is kept as a deprecated alias of error.message.
Other routes. A few older pass-through routes outside Generate (for example schedules) can still return { "success": false, "message": "...", "error": "<text>" } where error is a string. Clients that call those should check whether error is an object and fall back to message when it is not.
Dry runs: check a request before paying
Send "dry_run": true in the body of any Generate endpoint. The request goes through the same validation and normalization as a real call, and you get the price back. Nothing is queued, nothing is charged and no Idempotency-Key is needed. Invalid input returns the same 400 a real call would.
curl -X POST https://api.reelsbuilder.ai/api/v1/videos/text-to-video -H "Authorization: Bearer $REELSBUILDER_API_KEY" -H "Content-Type: application/json" -d '{ "prompt": "A lighthouse at dawn", "quality_tier": "medium", "duration_sec": 30, "dry_run": true }'
# -> 200
{
"success": true,
"data": {
"dry_run": true,
"valid": true,
"mode": "text_to_video",
"normalized_request": { "model": "kling-3.0-standard", "prompt": "A lighthouse at dawn", "options": { "duration": 30, "native_audio": false, "music": true, ... } },
"price": {
"usd_cents": 430,
"display": "$4.30",
"breakdown": [{ "item": "generate_ai_video", "usd_cents": 430, "model": "kling-3.0-standard", "tier": "medium", "duration_sec": 30, "narrated": false }]
},
"estimated_duration_sec": 30,
"estimated_render_seconds": { "min": 180, "max": 900 },
"warnings": [],
"charged": false
},
"meta": { "request_id": "…" }
}price.usd_centsis exactly what the real call would charge (the same pricing function bills it).warningslists things worth fixing, such as an alias that has a canonical name.estimated_render_secondsis a typical range, not a guarantee./batchaccepts a top-leveldry_runand returns each item's result plus the total price.- Sending
dry_runto a paid endpoint that does not support it (for example/post) returns400 DRY_RUN_NOT_SUPPORTEDinstead of running for real.
Error code reference
| Code | Status | Meaning | Client action |
|---|---|---|---|
AUTH_REQUIRED | 401 | No API key was sent. Use Authorization: Bearer <key> or X-API-Key. | Fix request, then retry |
AUTH_INVALID_API_KEY | 401 | The key is invalid or revoked (or the Authorization scheme is not exactly "Bearer"). | Fix request, then retry |
INPUT_INVALID | 400 | The request body failed validation. Read error.message / error.details. | Fix request, then retry |
INPUT_INVALID_FORMAT | 400 | A header or field has the wrong format, e.g. an Idempotency-Key outside 8–256 characters. | Fix request, then retry |
INPUT_INVALID_OBJECT_ID | 400 | An id in the path (such as a job id) is not a valid id. | Fix request, then retry |
IDEMPOTENCY_KEY_REQUIRED | 400 | This call needs an Idempotency-Key header. Dry runs (dry_run: true) never do. | Fix request, then retry |
DRY_RUN_NOT_SUPPORTED | 400 | dry_run was sent to a paid endpoint that is not a Generate endpoint (for example /post). Nothing ran or was charged. | Fix request, then retry |
CHANNEL_NOT_FOUND | 400 | Clipper: no YouTube channel at that URL. | Fix request, then retry |
INSUFFICIENT_API_CREDITS | 402 | Prepaid balance is below the call's price. Body includes cost_usd_cents, balance_usd_cents and topup_min_usd_cents. | Fix request, then retry |
JOBS_FORBIDDEN | 403 | The job belongs to a different account. | Don't retry |
ENDPOINT_NOT_FOUND | 404 | No endpoint exists at that method + path. | Don't retry |
JOBS_NOT_FOUND | 404 | No job with that id. | Don't retry |
SOCIAL_ACCOUNT_NOT_FOUND | 404 | POST /post: no active connected account for that platform or id. | Fix request, then retry |
RESOURCE_ALREADY_EXISTS | 409 | Idempotency-Key reused with a different body or path. Use a new key. | Fix request, then retry |
REQUEST_IN_PROGRESS | 409 | An identical request from your account is still being handled. Nothing was created or charged for this call. Wait Retry-After seconds and send it again to receive the first request's result. | Retry with backoff |
JOBS_CANCEL_NOT_ALLOWED | 409 | The job is already terminal and cannot be cancelled. | Don't retry |
NO_NEW_CHANNEL_VIDEOS | 409 | Clipper: every recent upload on the channel was already clipped by your account. | Don't retry |
content_policy_violation | 422 | The script or image was blocked by content moderation (body also has blocked: true). Refunded. | Fix request, then retry |
CUSTOMER_CONTEXT_UNAVAILABLE | 422 | brand_id / customer selectors are not supported on generation endpoints yet. Remove them. | Fix request, then retry |
PRIVATE_DELIVERY_UNAVAILABLE | 422 | Private delivery was requested on a route that does not support it (see Jobs > Private delivery for the supported routes), or it is not enabled. | Fix request, then retry |
RATE_LIMIT_EXCEEDED | 429 | Read/MCP request limit exceeded. Wait Retry-After seconds. | Retry with backoff |
RENDER_CONCURRENCY_LIMIT_EXCEEDED | 429 | 5 heavy renders are already active on the account. Wait for one to finish; the request was not queued. | Retry with backoff |
ENDPOINT_NOT_IMPLEMENTED | 501 | The endpoint's schema is published but it is not live yet. | Don't retry |
CHANNEL_UNAVAILABLE | 502 | Clipper: YouTube did not return the channel feed. | Retry with backoff |
INTERNAL_ERROR | 500 | Unexpected server error. | Retry with backoff |
API_CREDITS_INTERNAL_ERROR | 500 | The balance could not be checked or charged. Nothing was queued. | Retry with backoff |
Failed jobs report their own error in data.job.error (code, message, retryable) on GET /api/v1/jobs/{id}; plan tokens and API credits for failed jobs are refunded.
Retry decision tree
2xx→ success.400,401,402,422→ fix the request (or top up), then send it with a newIdempotency-Key. Error responses are cached against the old key for 24 hours — see Idempotency.409 RESOURCE_ALREADY_EXISTS→ you reused a key for a different request. Generate a new key.429→ wait forRetry-After(rate limit) or for an active render to finish (concurrency). See rate limits.5xx→ retry with exponential backoff (for example 1s, 2s, 4s, 8s, capped at 60s).501→ the endpoint is not live yet; don't retry.
Handler pattern (TypeScript)
type ApiErrorBody = {
success: false;
message?: string; // deprecated alias of error.message
error?: { code: string; message: string; retryable: boolean; details?: unknown } | string;
meta?: { request_id?: string };
};
function describeError(status: number, body: ApiErrorBody) {
if (body.error && typeof body.error === "object") {
return { status, code: body.error.code, message: body.error.message, retryable: body.error.retryable };
}
// Older non-Generate routes only: error is a string (or missing)
return { status, code: "UNKNOWN", message: body.message ?? String(body.error ?? ""), retryable: status >= 500 };
}
const res = await fetch(url, init);
if (!res.ok) {
const err = describeError(res.status, await res.json());
switch (err.code) {
case "INSUFFICIENT_API_CREDITS":
throw new Error("Top up API credits, then retry with a new Idempotency-Key");
case "AUTH_REQUIRED":
case "AUTH_INVALID_API_KEY":
throw new Error("Check the API key");
case "RATE_LIMIT_EXCEEDED":
case "RENDER_CONCURRENCY_LIMIT_EXCEEDED":
throw new Error("Back off and retry later");
default:
throw new Error(`${err.code}: ${err.message} (request ${res.headers.get("x-request-id")})`);
}
}Unknown codes
New codes may be added. Give your handler a default branch that logs unfamiliar codes and uses retryable rather than crashing.
Reporting an issue
Email admin@reelsbuilder.ai with:
- The
X-Request-Idheader from the failing response - The
error.codeand approximate timestamp (UTC) - What you were trying to accomplish