There are two error shapes, depending on when the failure happens. A request-time error — a bad key, a locked tier, an invalid param — comes back immediately as a non-2xx response with an error code, a human message, and a next field telling you the recommended remediation. A generation that fails after it was accepted uses a different shape entirely — see Generation failures below. Build retries and fallbacks against error / error.code — never parse message for branching, it can change wording without notice.
The request-time error shape
HTTP/1.1 403 Forbidden
{
"error": "MODEL_NOT_ALLOWED",
"message": "Your account is not granted access to \"video-cinematic-4k\".",
"next": "Call GET /api/v1/models to see allowed slugs, or contact support to request access.",
"model": "video-cinematic-4k",
"request_id": "req_abc123xyz"
}
Always pass request_id when you write to support — it lets us pull up your exact log line in seconds instead of digging through traces. Every response, success or error, carries one (also echoed on the X-Request-Id response header).
Error codes
Auth · 401
Code
When
What to do
401 AUTH_MISSING
No Authorization header was sent at all.
Send Authorization: Bearer vvx_live_… from a key created in Settings → API & Webhooks.
401 KEY_INVALID
The token doesn't start with vvx_live_, or doesn't match any key on file.
Create a new key. Revocation is permanent; there's no un-revoke.
Access & billing · 402 / 403
Code
When
What to do
402 INSUFFICIENT_CREDITS
Your balance can't cover this request's estimated cost.
Top up at /pricing, or reduce the request size (lower resolution, fewer outputs). The body includes balance and needed so you can render a useful upsell.
403 TIER_LOCKED
API access requires Pro, Pro Plus, or Ultimate — your account is on Free or Standard.
Your account isn't granted access to this specific model.
Call GET /api/v1/models to see allowed slugs, or email sales@getvivix.com to request access — some models need a per-customer arrangement.
403 API_DISABLED
The public API is switched off platform-wide — a kill switch, not something about your account.
This surface is off while we improve it. Check back later; there's nothing to fix on your end.
403 ACCOUNT_BANNED
Your account is banned.
Contact support if you believe this is an error. A body may carry until (ISO timestamp) when the ban is temporary.
403 ACCOUNT_FROZEN
Your account is temporarily frozen — generation disabled.
Contact support. Same optional until field as a ban.
403 ACCOUNT_RESTRICTED
A narrower wallet-level restriction than a full ban or freeze.
Contact support to resolve the restriction.
Content & request · 400
Code
When
What to do
400 CONTENT_POLICY_VIOLATION
The prompt, or an input image, was flagged by our pre-dispatch content-safety check.
No credits were charged. Adjust the prompt or image and retry — the body carries a category when one applies.
400 ENHANCE_NOT_SUPPORTED
enhance: true was sent for a model outside the supported set.
Call GET /api/v1/models — models with enhance_supported: true accept the flag — or drop it.
400 OPTION_NOT_AVAILABLE
A param value exists on this model in general, but not for the mode/config you sent.
Call GET /api/v1/models — only the options listed for this model, in this mode, are accepted.
Not found · 404
Code
When
What to do
404 NOT_FOUND
No generation exists with that id — or it belongs to a different account. Deliberately identical either way, so a guessed id never confirms it exists.
Verify the generation_id from a previous /api/v1/generate response.
Validation & idempotency · 409 / 422
Code
When
What to do
422 UNKNOWN_MODEL
The model slug doesn't match any real model.
Call GET /api/v1/models to list valid slugs.
422 INVALID_BODY
The catch-all shape/param error — unparseable JSON, an out-of-range duration, an unsupported aspect_ratio/resolution/size/quality, and more. Also fires as 413, same code, on the upload endpoints when a file exceeds the size limit.
Check details.field in the body — most causes name the exact field. Call GET /api/v1/models for the values a model accepts.
422 MISSING_PROMPT
This model needs a non-empty prompt and none was sent.
Add prompt: "<your text>" to the body.
422 MODEL_NOT_API_SUPPORTED
The model needs multiple role-tagged inputs the public API adapter can't shape yet.
Use this model in the web Studio for now.
422 INPUT_VIDEO_NOT_SUPPORTED
You sent input_video to a model that bills that mode by the source clip's exact length — length the API has no way to measure.
Use the web Studio, or drop input_video to use this model's other modes.
422 INVALID_NUMBER_RESULTS
number_results is outside the model's allowed range.
Call GET /api/v1/models for each model's limit — 1 for most video, up to 4 for most image.
422 MODEL_CONTRACT_VIOLATION
The request violates a per-model contract check (a required input missing, an invalid combination).
Call GET /api/v1/models for this model's required inputs and allowed values.
422 IDEMPOTENCY_KEY_REUSED
The same Idempotency-Key header was sent with a different request body than the first time.
Use a fresh Idempotency-Key for a different request, or resend the original body byte-for-byte.
409 IDEMPOTENT_REQUEST_IN_FLIGHT
An identical request with this Idempotency-Key is already being enqueued — a fast double-send, not a real retry.
Wait and retry (Retry-After: 3) — the original result will be returned.
Rate & concurrency · 429
Code
When
What to do
429 RATE_LIMIT
Over your per-minute submit limit: 240 (Pro), 420 (Pro Plus), 600 (Ultimate).
Read the Retry-After header (seconds) and back off. Batch where possible, or upgrade tier.
429 CONCURRENCY_LIMIT
Too many in-flight jobs at once: 5 (Pro), 12 (Pro Plus), 20 (Ultimate).
Wait for one to finish — poll GET /api/v1/generations/:id — or upgrade. The body carries in_flight and max_concurrent.
Server & availability · 500 / 503
Code
When
What to do
500 INTERNAL_ERROR
Something went wrong on our side before your request was ever billed or enqueued.
Retry with exponential backoff (start at 1s, double each time, max 30s). Capture request_id for support.
503 SAFETY_CHECK_UNAVAILABLE
Our own image-safety checker is down — not a verdict on your content, just unable to check it.
No credits were charged. Retry in a few minutes.
Generation failures (async)
POST /api/v1/generate returns 202 the moment a job is accepted — before it has actually rendered. If something goes wrong afterthat (the provider is at capacity, the model rejects the request, a post-render safety check fails), you won't see it as a non-2xx response. You'll see it on the next poll of GET /api/v1/generations/:id, which stays 200 OK with status: "failed" and an error object of its own:
HTTP/1.1 200 OK
{
"generation_id": "5b91a581-ee19-4f86-9fea-bd29471d69d5",
"status": "failed",
"model": "grok-imagine-image-quality",
"output_url": null,
"output_urls": null,
"credits_used": 0,
"duration_ms": 3180,
"created_at": "2026-05-10T12:34:56Z",
"completed_at": "2026-05-10T12:35:00Z",
"request_id": "req_9f2c7a1b3d8e",
"error": {
"code": "model_unavailable",
"message": "This model is temporarily unavailable. Please try another model — your credits have been refunded.",
"retryable": true
}
}
This error.code is a different, lowercase set — model_unavailable, rate_limited, content_blocked, upstream_detail, unknown, and others — not the uppercase codes tabled above. Don't guess whether to resubmit: read error.retryable. true means the identical body has a real chance of succeeding on a second try (capacity, a transient hiccup); falsemeans something about the request itself won't change on a retry (a blocked prompt, an unsupported setting) — resubmitting as-is just burns another charge for the same result.
Failed generations refund the charge automatically in almost every case. The one exception: error.code: "CONTENT_POLICY_VIOLATION" here means the output rendered but was flagged by post-generation safety review — those credits are forfeited, not refunded, the same policy the web app applies. Full response shapes (pending / completed / failed) are on the status endpoint page.
Recommended patterns
Idempotent retries. Send an Idempotency-Key header (any string you generate, e.g. a UUID) on POST /api/v1/generate. Retry with the same key and the same body and you get the original response back, unbilled — no double charge. The same key with a different body is a 422 IDEMPOTENCY_KEY_REUSED; the same key while the first request is still being enqueued is a 409 IDEMPOTENT_REQUEST_IN_FLIGHT. Without a key, every retry — including the automatic ones below — creates a brand-new, separately-billed generation.
async function generateWithRetry(payload: any, idempotencyKey: string, attempt = 0): Promise<any> {
const res = await fetch('https://getvivix.com/api/v1/generate', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VIVIX_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
})
if (res.ok) return res.json() // 202 — job accepted, poll /generations/:id next
const body = await res.json()
// Permanent failures — don't retry as-is.
if ([400, 401, 402, 403, 404, 422].includes(res.status)) {
throw new ApiError(body)
}
// Idempotency race — the original is still being enqueued. Short wait, same key.
if (res.status === 409) {
await sleep(3000)
return generateWithRetry(payload, idempotencyKey, attempt + 1)
}
// Rate-limited — wait the server's recommended duration, same key.
if (res.status === 429) {
const retryAfter = Number(res.headers.get('Retry-After') ?? '5')
await sleep(retryAfter * 1000)
return generateWithRetry(payload, idempotencyKey, attempt + 1)
}
// Server-side hiccup — exponential backoff up to 5 attempts, same key.
if (res.status >= 500 && attempt < 5) {
await sleep(Math.min(30_000, 1000 * 2 ** attempt))
return generateWithRetry(payload, idempotencyKey, attempt + 1)
}
throw new ApiError(body)
}