API · ERRORS

Every error tells you what to do next.

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

CodeWhenWhat to do
401 AUTH_MISSINGNo Authorization header was sent at all.Send Authorization: Bearer vvx_live_… from a key created in Settings → API & Webhooks.
401 KEY_INVALIDThe token doesn't start with vvx_live_, or doesn't match any key on file.Generate a fresh key in Settings → API & Webhooks. Keys are shown only once, at creation.
401 KEY_REVOKEDThe key was revoked — by you, or an admin.Create a new key. Revocation is permanent; there's no un-revoke.

Access & billing · 402 / 403

CodeWhenWhat to do
402 INSUFFICIENT_CREDITSYour 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_LOCKEDAPI access requires Pro, Pro Plus, or Ultimate — your account is on Free or Standard.Upgrade your plan, then create a new API key. Pro starts at $15.
403 MODEL_NOT_ALLOWEDYour 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_DISABLEDThe 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_BANNEDYour 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_FROZENYour account is temporarily frozen — generation disabled.Contact support. Same optional until field as a ban.
403 ACCOUNT_RESTRICTEDA narrower wallet-level restriction than a full ban or freeze.Contact support to resolve the restriction.

Content & request · 400

CodeWhenWhat to do
400 CONTENT_POLICY_VIOLATIONThe 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_SUPPORTEDenhance: 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_AVAILABLEA 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

CodeWhenWhat to do
404 NOT_FOUNDNo 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

CodeWhenWhat to do
422 UNKNOWN_MODELThe model slug doesn't match any real model.Call GET /api/v1/models to list valid slugs.
422 INVALID_BODYThe 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_PROMPTThis model needs a non-empty prompt and none was sent.Add prompt: "<your text>" to the body.
422 MODEL_NOT_API_SUPPORTEDThe 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_SUPPORTEDYou 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_RESULTSnumber_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_VIOLATIONThe 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_REUSEDThe 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_FLIGHTAn 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

CodeWhenWhat to do
429 RATE_LIMITOver 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_LIMITToo 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

CodeWhenWhat to do
500 INTERNAL_ERRORSomething 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_UNAVAILABLEOur 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)
}

Where to next