API

Try-On API

Server-to-server. Asynchronous — start a generation, poll for the result.

Base URL https://www.genfashion.app/api/tryon/v1. Every request carries x-api-key with a secret key (sb_secret_…).

A publishable key (pk_…) is refused here. It ships inside the storefront widget where anyone can read it, and what makes that safe is the origin allowlist — which a server request has no origin to satisfy. Use the widget's own endpoint for browser traffic.

Generation is asynchronous: POST /try-on returns a generationId, and you poll GET /generations/:code.

Endpoints

MethodPathPurpose
POST/try-onStart a generation. Returns generationId
GET/generations/:codePoll status; resultImageUrl when COMPLETED
GET/account/creditsRemaining credit balance
GET/customers/:externalUserIdWhat is held about one shopper, and their consent trail
DELETE/customers/:externalUserIdErase that shopper — see Privacy

Webhooks are configured in the console rather than over this API: Settings → Webhooks. Signed POST on generation.completed and generation.failed, so you do not have to poll.

Not built yet

These were previously documented on this page as though they existed. They did not, and no version of this API served them:

PathStatus
POST /images/uploadNot built. There are no reusable imageIds, and no server-side cropping
GET /products, DELETE /products/:externalIdNot built. Manage the catalogue from the console or a store sync

Two rows left this table on 2026-09-13, because they were built rather than because the claim was softened: DELETE /customers/:externalUserId above, and webhooks. If you integrated against either while it was listed here as missing, it works now — and in the erasure case, nothing you asked to delete before that date was deleted.

POST /try-on

json
{
  "person":   { "url": "https://example.com/shopper.jpg" },
  "products": [{ "externalId": "shirt-42" }],
  "background": "original"
}

person takes exactly one of url or dataUrl (a base64 data:image/… string). Sending both is an error rather than a silent preference, so a caller who meant one of them finds out.

products accepts up to 6 items and each must already exist in your catalogue, referenced by externalId. There is no inline upsert and no one-shot product: a garment this API has never seen cannot be attributed, cached or reported on, so it is refused with PRODUCT_NOT_FOUND rather than accepted and half-supported. productIds: ["shirt-42"] is accepted as a shorthand.

background is one of original, white, studio, city, nature.

modelId may name a specific model by its code (mdl_…); omit it to use the deployment's default.

output controls what is composited onto the result:

FieldDefaultEffect
output.aiLabelthe account settingVisible AI MODIFIED badge, bottom-right
output.watermarktrueYour logo, bottom-left. A no-op with no logo configured

Omitting a field is not the same as sending false — an absent key uses the account default, so a call that never mentions aiLabel does not silently strip the disclosure.

The provenance metadata has no switch. It is written into every result regardless of either field. See Privacy for what it contains and, just as importantly, what it is not.

Optional x-external-user-id (header) or externalUserId (body) is your own opaque identifier for the end user. Send no PII — it is stored on the generation and used as the rate-limit subject. It is capped at 128 characters.

Send it if you may ever have to honour an erasure request. It is what DELETE /customers/:externalUserId matches on, and the only identifier you can map back to a person who emails you. Without it a try-on is tagged with the widget's per-browser reference, which does not survive a cleared cookie.

This page has said "it is stored on the generation" for longer than that was true — until 2026-09-13 the value was read, used for the rate-limit key and then dropped, so the column was empty on every row. It is stored now. Try-ons made before that date cannot be found by customer id, because the id was never written down.

Response

json
{ "generationId": "gen_b2c1…", "status": "PROCESSING", "pollAfterMs": 5000 }

202 Accepted.

The call is not instant, and the reason is worth knowing. Photo screening runs before any generation model and fails closed. A refused photograph writes no generation at all — there is nothing to poll for — so screening happens inside this request and comes back as a synchronous 4xx you can act on. That takes a few seconds. The ~45s of actual generation is what you no longer wait for.

So a 4xx from this endpoint means the request was rejected outright and no credit was consumed. A 202 means it is running.

GET /generations/:code

json
{
  "generationId": "gen_b2c1…",
  "status": "COMPLETED",
  "resultImageUrl": "https://…signed…",
  "garmentPhrase": "blue and black check shirt",
  "durationMs": 45137,
  "creditsCharged": 1,
  "externalUserId": null,
  "createdAt": "2026-09-12T11:08:08Z",
  "expiresAt": "2026-09-19T00:00:00Z"
}

status is PROCESSING, COMPLETED or FAILED. A FAILED generation also carries error and message.

resultImageUrl is a signed URL valid for 15 minutes. Fetch the bytes and store them your side if you need them longer. The object itself is deleted after the account's retention period (expiresAt), and it is served with max-age=0 so a URL signed before deletion stops working when the object goes — a cached CDN copy outliving a deletion is not a retention promise anyone should rely on.

A generation belonging to another account returns 404, the same as one that does not exist, so this cannot be used to discover which ids are real.

GET /account/credits

json
{ "balance": 213, "providerSpendUsd30d": 1.42 }

providerSpendUsd30d is what inference cost the operator over the window. It is not a bill.

Limits

Median generation~45–56s (measured 2026-09-12 across every model in the pool)
Garments per try-on6, composed in one provider call
Upload10 MB — JPEG, PNG, WebP, HEIC
Rate limitsPer end user, per account, and per platform. A 429 carries retry-after

An earlier version of this page claimed a ~9s median and a 99.9% uptime target. Neither was ever measured and the first is wrong by roughly five times; both have been removed rather than restated. There is no uptime commitment.

Errors

Every error is { "error": "CODE", "message": "…" }. Codes written for integrators — unknown product, no credits, rate limited, image rejected — come back as themselves. Anything else becomes INTERNAL_ERROR with a generic message and is logged our side: an API must not return a database constraint name or a stack to its caller.

CodeMeaning
UNAUTHORIZEDMissing or unrecognised key
KEY_REVOKEDThe key existed and was revoked
PUBLISHABLE_KEY_REFUSEDA pk_… key was sent; this API needs sk_…
PERSON_REQUIRED / BAD_IMAGE / UNSUPPORTED_IMAGE_TYPE / FILE_TOO_LARGEThe person photo
GARMENT_REQUIRED / TOO_MANY_GARMENTSThe products array
PRODUCT_NOT_FOUND / PRODUCT_IMAGES_REQUIREDThe catalogue
IMAGE_REJECTEDScreening refused the photograph
SCREENING_UNAVAILABLEScreening could not run. Fails closed — no generation happens
NO_CREDITSBuy more before retrying
RATE_LIMITEDWith retry-after
NO_MODELThe deployment has no routable model configured

Testing

There is no free test mode. The mock provider this page once described was removed from the routable pool in migration 0006, so every call reaches a real provider and costs real money (~$0.17 a generation at current measured rates). Use a test-mode key against a small credit balance if you want a blast radius.