Skip to content

Reference

Errors

One error shape everywhere, stable machine-readable codes, and what to do about each.

Every endpoint fails the same way: a non-2xx status and a JSON body with one error object. Branch on code, which is stable; show message to people, which is written to be read.

Error shape#

402 Payment Required
{
  "error": {
    "code": "insufficient_credits",
    "message": "This needs 80 credits and you have 35. Presets, recolors, and exports stay free.",
    "details": { "needed": 80, "balance": 35 }
  }
}

details is null unless there is something structured to add. For a body that fails validation it is an array of "path: message" strings, one per problem:

400 Bad Request
{
  "error": {
    "code": "invalid_request",
    "message": "Request body is invalid.",
    "details": ["prompt: Too small: expected string to have >=3 characters"]
  }
}

Spec, clip and canvas validation use the same field: "Spec is invalid.", "Clip is invalid." or "Canvas graph is invalid." with every problem listed in details.

Status codes#

StatusMeaning
200 · 201Done. 201 when something was created synchronously (a preset animation, an upload, a canvas, a key, a webhook, an imported spec).
202Accepted: a job was started. The body carries the job (and, for POST /mascots, the new mascot).
400The request is malformed or fails validation.
401No API key or session, or the key is invalid or revoked, or the session expired.
402Not enough credits for a model-backed action, or no free seat for an invite.
403Your role doesn't allow this, an API key tried to manage people, a cross-site request was refused, or a share token is invalid.
404The mascot, animation, job, file, canvas, member or invite doesn't exist in this workspace.
409The mascot has no drawing yet, or a people change conflicts (already a member, already invited, the owner must transfer first).
410An invite was already used, revoked or has expired.
429Too many API-key requests this minute for your plan, or too many sign-in links requested.
500Something broke on our side. Safe to retry.

Error codes#

CodeStatusWhenWhat to do
invalid_request400Validation failed: bad fields, unknown preset, template or palette token, unsupported upload, file over 15 MB, an animated export without an animation, an image_url that can't be fetched.Read message and details, fix the request. Don't retry unchanged.
invalid_json400The body isn't valid JSON.Send JSON with Content-Type: application/json.
unauthorized401Missing Authorization header or session, an invalid or revoked key, or an expired session.Send Authorization: Bearer lr_sk_…, or sign in again. See Authentication.
insufficient_credits402The action costs more credits than the balance. details: { needed, balance }. Batches check the total up front.Wait for the monthly allowance, or change plan. See Credits.
seat_limit402An invite would take more seats than the plan has. Pending invites count. details: { seats, used, members, pending }.Remove a member or revoke an invite, or move to a plan with more seats. See Workspaces, roles and seats.
forbidden403Your role in the workspace doesn't allow the action (members can't manage keys, webhooks or people; only the owner changes the plan), or a share token is invalid, expired or for a different mascot.Ask an admin or the owner, or create a fresh share link with POST /mascots/{id}/share.
session_required403An API key called an endpoint that manages people (invites, roles, transfers). Keys belong to the workspace, not to a person.Do it from the studio, signed in.
csrf_failed403A cookie-authenticated write came from another site.Call the API from your own page's origin, or use an API key from your server.
invite_email_mismatch403The signed-in person isn't the invited address.Sign in with the address the invite was sent to, or ask for an invite to yours.
already_member · already_invited · owner_must_transfer · owner_role · personal_workspace409The person is already in the workspace or has a pending invite; the owner tried to leave or be demoted; or a personal workspace was deleted on its own.Resend the existing invite, transfer ownership first, or delete the account instead.
invite_used · invite_revoked · invite_expired410The invite link can't be used any more.Ask an admin for a new invite.
not_found404The resource doesn't exist or belongs to another workspace. A private mascot's bundle or preview without a key or token also returns 404.Check the id and the key's workspace.
not_ready409The mascot is still generating, so it has no spec to animate, export or edit.Wait for the creation job, then retry.
rate_limited429More API-key requests in the last minute than the plan allows (details: { retry_after, limit }), or too many sign-in links for one address or network.Wait Retry-After seconds, then retry. Higher plans allow more; see Credits & limits.
internal500An unexpected server error.Retry with backoff.

Failed jobs#

A job that starts fine can still fail later. That isn't an HTTP error: the job reaches status: "failed" (or "canceled") with a human-readable error string, and your webhook receives job.failed or job.canceled.

GET /api/v1/jobs/job_…
{
  "id": "job_…",
  "object": "job",
  "type": "generate",
  "status": "failed",
  "error": "Cost ceiling of $0.56 reached",
  "cost_usd": 0.5612,
  "result": null
}
  • Credits come back. Credits are held when a job starts and refunded automatically when it fails or is canceled.
  • Cost ceiling. A job whose model spend passes its ceiling is stopped with Cost ceiling of $… reached. See Holds, refunds and ceilings.
  • Server restarts. A job interrupted by a restart fails with Interrupted by a server restart. Retry the request.
  • Model errors are surfaced as AI service error: ….
  • A failed creation leaves the mascot with status: "failed" and the reason in mascot.error. A failed edit keeps the previous version current.

Handling errors in the SDK#

The SDK throws LumorigError with status, code, message and details copied from the body. It adds three codes of its own:

CodeStatusWhen
http_erroranyThe error body wasn't JSON, or an event stream couldn't be opened.
job_failed500createAndWait, fromPromptAndWait or createAndDownload finished with a job that didn't succeed. message is the job's error.
timeout408jobs.wait passed its timeoutMs (default 15 minutes). The job is still running.

Example#

retry.ts
import { LumorigError } from "@lumorig/sdk";

async function withRetry<T>(fn: () => Promise<T>, tries = 3): Promise<T> {
  for (let i = 0; ; i++) {
    try {
      return await fn();
    } catch (err) {
      const retryable = err instanceof LumorigError && (err.status >= 500 || err.code === "not_ready");
      if (!retryable || i === tries - 1) throw err;
      await new Promise((r) => setTimeout(r, 1000 * 2 ** i));
    }
  }
}