Skip to content
Lucent ImagingLucent Imaging icon
Core concepts

Errors

Failures come back in the same envelope as successes, carrying a stable code to branch on and a human message for your logs.

Shape

{
  "data": null,
  "error": {
    "code": "CREDIT_LIMIT_EXCEEDED",
    "message": "This order would exceed the account's credit limit.",
    "details": {
      "outstandingCents": 128400,
      "limitCents": 150000,
      "availableCents": 21600,
      "orderCents": 41700
    }
  }
}

code is the contract - stable, enumerated, and additive-only. message is written for a person and may change at any time, so never branch on it. details is present only where there is something structured worth adding, and its shape follows the code.

Handling

Branch on the code, and always keep a default arm - we may add codes within v1, so an unfamiliar one is expected rather than exceptional. When you do not recognise a code, fall back to the status: 4xx is something to fix in the request, 5xx is ours and may be retried.

const res = await fetch(url, init);
const body = await res.json();

if (body.error) {
  switch (body.error.code) {
    case "QUOTE_EXPIRED":
      return reprice();
    case "CREDIT_LIMIT_EXCEEDED":
      return notifyAccounts(body.error.details);
    case "RATE_LIMITED":
      return retryAfter(Number(res.headers.get("Retry-After") ?? 5));
    default:
      // Always a default arm: the code list is additive, so an unfamiliar
      // code is expected rather than exceptional. Fall back to the status.
      if (res.status >= 500) return retryWithBackoff();
      throw new Error(`${body.error.code}: ${body.error.message}`);
  }
}

Retrying POST /orders is safe as long as you reuse the same Idempotency-Key. See orders.

Code reference

Every code the API publishes. This list may grow; it will not shrink or be renamed without a new API version.

CodeStatusMeaning

ARTWORK_NOT_ALLOWED

400

A line named artwork that this account did not upload.

Only URLs returned by your own POST /uploads calls may be referenced. Upload the file again under your key.

ARTWORK_REQUIRED

400

A line carried no artwork for a product that has to print something. The message names the line.

GET /catalogue publishes requiresArtwork per product. Upload the file with POST /uploads and send the URL it returns as the line's imageUrl.

IDEMPOTENCY_KEY_REQUIRED

400

POST /orders was called without an Idempotency-Key header.

It is required on the one irreversible call in the API. Send a unique value per order attempt.

INVALID_QUANTITY

400

The quantity is outside what the product allows.

MAT_BELOW_MINIMUM

400

A mat border falls under the studio minimum on at least one side. The board is unmakeable, not merely unusual - too narrow a border has nothing to hold the artwork.

Widen every side to at least the minimum, or order the piece unmatted.

PRODUCT_UNAVAILABLE

400

A material named on a line is not available for that product - most often a paper id used on a canvas line, or a moulding the studio has withdrawn.

Re-read GET /catalogue. Canvas, Everyday Art and fine-art media are separate, exclusive pools.

PRODUCT_UNSUPPORTED

400

The productSlug is not orderable through this API. The message says why - some products are storefront-only, and photo restoration is quoted by hand.

GET /catalogue lists everything you may order.

QUOTE_ENVIRONMENT_MISMATCH

400

The quote was priced by a key from the other environment.

A test-priced basket can never become a live job, nor the reverse. Re-price with the key you intend to order with.

SHIPPING_ADDRESS_INCOMPLETE

400

The destination is not deliverable as given.

country must be an ISO-2 code. A country NAME is the single most common integration bug here.

SHIPPING_UNAVAILABLE

400

The delivery method is not available for this basket - or you omitted shippingMethodId deliberately to discover the options.

error.details.shippingOptions carries every method this basket can use, with prices. This is the intended way to discover them; there is no static list.

VALIDATION_ERROR

400

The request body did not match the schema. error.details lists the offending paths.

Unknown fields are ignored rather than rejected, so a misspelled field name shows up as a missing value elsewhere, not as a complaint about the typo.

UNAUTHORIZED

401

The key is missing, malformed, revoked, expired, or belongs to a disabled account.

Deliberately indistinguishable - the response will not tell you which. If the key worked yesterday, contact the studio.

CREDIT_LIMIT_EXCEEDED

402

The order would take the account past its credit limit.

error.details carries outstandingCents, limitCents, availableCents and orderCents. Nothing was written - no project, no invoice, and the quote is still unspent.

FORBIDDEN

403

The key authenticated but lacks the scope this endpoint needs.

Scopes are fixed when a key is minted. Ask the studio for a key with the scope.

NOT_FOUND

404

No such quote or order for this account.

Another partner's identifier answers 404, not 403 - the API will not confirm that someone else's record exists. A malformed identifier answers 404 too.

IDEMPOTENCY_KEY_REUSED

409

The key was already used for a different request body, or the first request with it is still in flight.

A retry of the same request replays the original response instead. If the first attempt is still running, the response carries Retry-After.

ORDER_NOT_CANCELLABLE

409

The studio has already done something irreversible - shipped it, printed it, or recorded money against it. The message says which.

Contact the studio.

QUOTE_ALREADY_ORDERED

409

That quote has already produced an order. A quote is single-use, and stays spent even if the order it produced was later cancelled.

error.details.orderId names the order. To re-place a cancelled order, price a fresh quote.

QUOTE_EXPIRED

409

The quote lapsed before it was ordered. Quotes hold for 24 hours.

error.details.expiresAt says when. Price the basket again.

RATE_LIMITED

429

Too many requests on this key for the endpoint's bucket.

Honour Retry-After, which is sent in seconds.

INTERNAL_ERROR

500

Something failed on our side.

Safe to retry with backoff. On POST /orders, retry with the SAME Idempotency-Key - that is exactly what it is for.