Skip to main content
The Virtual Try-On API graduated from alpha to v1.0.0. The surface was reshaped along the way; this page walks you through what changed and how to update. For the full release notes see the Changelog.
Three small breakings, several quality-of-life additions, and a new lifetime model for products. Most integrations migrate in 10 minutes.
Heads-up on the 1.4.0 try-on/upload redesign (2026-06-17). The request shapes shown on this page (product, customer, customerId, useWatermark, retentionDays) were superseded by products: [...], person.image.source, externalUserId, and output.watermark / output.keepForDays. That redesign is not breaking (the shapes below are still accepted, deprecated), with one exception: the imageUrls alias was removed in 1.4.0. See the Changelog for the new canonical shapes.

At a glance

1. POST /try-on: single product field

The whole body collapses to one product field that covers three use cases.
The response now includes productExternalId, which matters for one-shot calls: it lets you re-use the same product later (server-generated IDs start with _anon_).
Sending the old productId field returns 400 Bad Request. There’s no alias.

2. PATCH /products/:externalId removed

The PATCH route now returns 404. Use POST /tryon/v1/products with just the fields you want to change; the endpoint now supports partial updates on existing products.
Alpha
v1
Creating a brand-new product still requires at least one image. title and description are optional (they improve the AI’s category classification when provided). validForDays is preserve-on-omit on updates.

3. GET /tryon/v1/generations (paginated list) removed

The public API used to expose a paginated list of every generation on the account. It now exposes only status by id: capture the generationId from each POST /try-on response and poll GET /generations/:id when you need the result.
Alpha
v1
If you were using the list endpoint to recover lost generation ids, switch to storing them as part of your own write; the create response is the only place an id is now revealed.

4. images replaces imageUrls

The alpha’s imageUrls: string[] was deprecated in v1 and removed in 1.4.0; sending it now fails validation. The images shape lets URLs and multipart uploads mix in the same payload.
Alpha (no longer accepted)
v1 (URL only)
v1 (multipart, mix URL + bytes)

Behaviour changes (non-breaking, but visible)

These don’t break callers, but the visible behaviour shifts. Worth a skim.

Product lifetimes by creation path

Lifetimes refresh on every generation. validForDays is preserved when you don’t supply it on updates. A product first created inline (15d) can be promoted to “kept forever” later by calling POST /products with validForDays: null.

One-shot products are reachable by ID

Every /try-on response now includes productExternalId. For one-shots, this is a server-generated ID (starts with _anon_) that you can re-use on subsequent calls to reference the same product. One-shot products don’t appear in GET /products (they’re not part of your catalog), but GET /products/:externalId and DELETE /products/:externalId accept their IDs. POST /products with an _anon_* ID returns 409 Conflict; that prefix is reserved for server-generated IDs.

Customer image: single customer object

The top-level customerImageId shorthand is gone. POST /try-on now takes one customer object with exactly one of three fields:
  • customer.id: recommended, image id from a prior /images/upload. The only path with control over cropping (set crop=false on upload).
  • customer.url: the server downloads on every call. Always 4:5-cropped.
  • customer.fileKey: multipart file in the same /try-on request. Always 4:5-cropped.
Alpha
v1
The old nested customer: { imageId } form is also gone; the field is now id. Migration is a two-line search-replace.
As of 1.3.0 (2026-06-17), the customer reference and each product image moved under a source sub-object: customer: { source: { id | url | fileKey } } and images: [{ source: { url | fileKey }, classifications? }]. The flat shape shown above is still accepted but deprecated; prefer source in new integrations. See the Changelog.Then as of 1.4.0 (same day), the whole try-on body was renamed: product became products: [...], customer became person.image, customerId became externalUserId, and useWatermark / retentionDays became output.watermark / output.keepForDays. Still not breaking; the older shapes remain accepted. See the Changelog.

Crop flag on customer-image upload

POST /tryon/v1/images/upload reads a crop form field. Default true matches the old always-crop behaviour. Pass crop=false to keep the original framing (studio shots, model previews, anything where the framing matters).

POST /try-on accepts multipart

When the inline product references uploaded files via fileKey, the request is multipart/form-data with a JSON data field plus named file fields. Pure-JSON URL-only callers are unaffected.

Partial updates on POST /tryon/v1/products

Hitting the endpoint with an existing externalId and only the fields you want to change merges over stored values. Sending a different images array still replaces the full list; there’s no per-image patching.

Concurrent identical try-on calls are safe

Two parallel /try-on calls with the same inline content resolve to the same product: no duplicate rows, no race conditions.

Error responses are now { code, message, status }

Every /tryon/v1/* error returns the same JSON shape, with a stable code from a documented enum. The previous responses mixed several formats: bare "message" strings, ad-hoc { error, code } objects, raw Zod issues. Branch on code going forward; treat message as polished text. Full catalog at Errors.
Was (alpha)
Now (v1)
The legacy INVALID_PRODUCT code is gone; it folded into PRODUCT_NOT_FOUND (ref to a missing externalId) and VALIDATION_FAILED (Zod rejection of the inline payload). Validation errors also carry a details array with the per-field issues.

Migration checklist

  • Replace productId with product.externalId on every /try-on call.
  • Replace customerImageId (top-level) and customer: { imageId } (nested) with customer: { id }.
  • Replace imageUrls with images: [{ url }, ...]. The alias was removed in 1.4.0 and now fails validation.
  • Drop any PATCH /products/:externalId calls; use POST /products with partial fields.
  • Replace GET /generations polling with per-id GET /generations/:id. Track each generation id from the /try-on response.
  • Update error handling to read the new { code, message, status } shape: switch on code, not message strings. Replace any INVALID_PRODUCT checks with PRODUCT_NOT_FOUND / VALIDATION_FAILED as appropriate.
  • Drop any code reading productId from POST /products responses. Reference products by externalId instead.
  • Drop any code reading resultImageKey from GET /generations/:id. Use resultImageUrl only.
  • Image fields on product responses are now { sourceUrl, order }; the id/storageKey/productId columns are no longer exposed.
  • (Optional) If you’re shipping byte uploads on every call, switch to upserting once and referencing by externalId; the TTL refresh keeps the row alive as long as you keep using it.
  • (Optional) For one-shot use cases, drop externalId entirely; the server returns productExternalId so you can reference the row if you want.
  • (Recommended) Adopt the current canonical shapes while you’re in there: products: [...], person.image.source, externalUserId, output.watermark / output.keepForDays. See the Changelog.

Need help?

Email support@genlook.app and we’ll happily review your integration’s diff and flag anything that needs updating.