Errors
The codes this API actually returns, and the ones it never did.
This page was rewritten on 2026-09-13 because almost none of it was true. It documented 21 error codes, of which 15 did not exist anywhere in the codebase — no route returned them, no library raised them, nothing ever had. It also gave the wrong response shape: it told you to branch on a code field, and there is no code field.
That is the same defect that was found on the API reference on 2026-09-12 and corrected there. It was written from a specification rather than from the server, and nothing in the build catches prose. If you wrote error handling against the old page, the branches you wrote for INSUFFICIENT_CREDITS, NO_PERSON_DETECTED, CONTENT_POLICY, QUOTA_EXCEEDED, GENERATION_TIMEOUT and the rest have never fired and never will.
The shape
{ "error": "NO_CREDITS", "message": "This workspace is out of credits." }The field is error. Branch on it, never on message — messages are written for humans and get reworded. The HTTP status carries the same information; there is no status field in the body.
What you can actually receive
Any code not in this list is collapsed to INTERNAL_ERROR with a generic message before it reaches you, and the real one is recorded on our side. That is deliberate: an unrecognised code is assumed to be internal, because the cost of guessing wrong in that direction is a confusing message, and the cost of guessing wrong in the other is showing you a constraint name or a stack. The allowlist lives in INTEGRATOR_SAFE and is the authority for this table.
Auth
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | No x-api-key header, or the key is unknown |
KEY_REVOKED | 401 | The key existed and has been revoked |
PUBLISHABLE_KEY_REFUSED | 401 | A publishable key was sent to a server endpoint that requires a secret one |
NOT_FOUND | 404 | No such generation, customer or resource on this account |
Request shape
| Code | HTTP | Meaning |
|---|---|---|
BAD_REQUEST | 400 | Body is not JSON, or a required field is malformed |
PERSON_REQUIRED | 400 | No person supplied |
GARMENT_REQUIRED | 400 | No products supplied |
TOO_MANY_GARMENTS | 400 | More than 6 |
BAD_IMAGE | 400 | The bytes did not decode as an image |
EMPTY_IMAGE | 400 | Zero-length image |
UNSUPPORTED_IMAGE_TYPE | 400 | Not JPEG, PNG, WebP or HEIC |
FILE_TOO_LARGE | 413 | Over the size limit |
Catalogue
| Code | HTTP | Meaning |
|---|---|---|
PRODUCT_NOT_FOUND | 400 | An externalId this account has never synced. There is no inline upsert |
PRODUCT_IMAGES_REQUIRED | 400 | The product exists and has no usable image |
Account state
| Code | HTTP | Meaning |
|---|---|---|
NO_CREDITS | 402 | Buy a credit pack and retry |
RATE_LIMITED | 429 | Retryable. A retry-after header says how long |
NO_MODEL | 503 | This deployment has no try-on model configured |
MODEL_NOT_FOUND | 400 | The modelId you sent is not in the routable pool |
BAD_MODEL | 400 | The modelId you sent is not a model code (mdl_…) of this platform |
UNAVAILABLE | 503 | Try-on is briefly unavailable |
Screening
Screening runs before any generation model sees an image, and a refusal costs no credit.
| Code | HTTP | Meaning |
|---|---|---|
IMAGE_REJECTED | 422 | The person photograph was refused. The message is deliberately generic |
GARMENT_REJECTED | 422 | The product image was refused. Replace it in your catalogue |
SCREENING_UNAVAILABLE | 503 | Screening could not reach its provider, so the try-on was refused rather than allowed through. Retryable |
SCREENING_UNAVAILABLE is the one worth handling deliberately. It fails closed on purpose: an empty provider balance surfaces here rather than as a billing error, and it is what a four-day outage looked like in September 2026 — every try-on politely refused, nothing thrown.
Image quality
A separate question from screening: not may we use this photograph, but can we. These run alongside screening, also before any generation model, and a refusal also costs no credit.
| Code | HTTP | Meaning |
|---|---|---|
IMAGE_TOO_SMALL | 422 | The person photograph is below the minimum on its shortest side. The message names the actual size |
IMAGE_TOO_BLURRY | 422 | The person photograph is severely out of focus |
IMAGE_BAD_ASPECT | 422 | The person photograph is an extreme shape — a banner rather than a photograph of a person |
GARMENT_TOO_SMALL | 422 | Same, for a product image. The message names which garment |
GARMENT_TOO_BLURRY | 422 | Same, for a product image |
GARMENT_BAD_ASPECT | 422 | Same, for a product image |
Two things worth knowing before you handle these.
The focus check catches severe blur only, and this is a limit of the measure rather than a setting. It is the variance of a Laplacian, which cannot distinguish "out of focus" from "not much in the frame" — in calibration, a photograph at blur radius 2 and a sharp photograph of a plain subject scored 343 and 350. Any threshold that rejects the first rejects the second. So a mildly soft photograph will pass, and you should not treat the absence of these codes as a statement that a photograph is good.
There is no pose or framing check. A photograph cropped at the chest passes all three, and every model we have tested will then invent a torso and arms that were not in your input. If your users upload head-and-shoulders portraits, that is worth catching on your side.
Anything else
| Code | HTTP | Meaning |
|---|---|---|
INTERNAL_ERROR | 500 | Something on our side. The real code is in our logs, not in your response |
Storefront widget errors are a shorter list
The widget API is driven by a publishable key that ships in your page source, so its audience is a shopper rather than a developer and it names fewer codes: BAD_IMAGE, UNSUPPORTED_IMAGE_TYPE, EMPTY_IMAGE, FILE_TOO_LARGE, IMAGE_REJECTED, SCREENING_UNAVAILABLE, NO_CREDITS, RATE_LIMITED, SHOPPER_REQUIRED, NO_MODEL, IMAGE_TOO_SMALL, IMAGE_TOO_BLURRY, IMAGE_BAD_ASPECT.
CONSENT_REQUIRED (400) is specific to it: POST /api/storefront/v1/try-on refuses without a consent object, and the check runs before the photograph is decoded.
A few codes come back named but with the message rewritten for a shopper — PRODUCT_NOT_FOUND becomes "This item isn't set up for try-on yet" — so an integrator debugging in the console still sees what happened while the shopper reads something they can act on.
The image-quality codes split along that line rather than being on one side of it. IMAGE_TOO_SMALL, IMAGE_TOO_BLURRY and IMAGE_BAD_ASPECT reach the shopper verbatim, because the shopper is the one holding the photograph and the next thing that happens is they choose a better one. Their GARMENT_* counterparts are rewritten to "This item's photo can't be used for try-on", because the real message names pixel dimensions and is addressed to whoever maintains the catalogue — which is you, reading it in the console, not them.
Billing
Failed generations are never billed. The credit is spent when a generation reaches COMPLETED, so a failure is never debited in the first place — there is nothing to refund.
