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.
productExternalId, which matters for one-shot calls: it lets you re-use the same product later (server-generated IDs start with _anon_).
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
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
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 (setcrop=falseon upload).customer.url: the server downloads on every call. Always 4:5-cropped.customer.fileKey: multipart file in the same/try-onrequest. Always 4:5-cropped.
Alpha
v1
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)
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
productIdwithproduct.externalIdon every/try-oncall. - Replace
customerImageId(top-level) andcustomer: { imageId }(nested) withcustomer: { id }. - Replace
imageUrlswithimages: [{ url }, ...]. The alias was removed in 1.4.0 and now fails validation. - Drop any
PATCH /products/:externalIdcalls; usePOST /productswith partial fields. - Replace
GET /generationspolling with per-idGET /generations/:id. Track each generation id from the/try-onresponse. - Update error handling to read the new
{ code, message, status }shape: switch oncode, not message strings. Replace anyINVALID_PRODUCTchecks withPRODUCT_NOT_FOUND/VALIDATION_FAILEDas appropriate. - Drop any code reading
productIdfromPOST /productsresponses. Reference products byexternalIdinstead. - Drop any code reading
resultImageKeyfromGET /generations/:id. UseresultImageUrlonly. - Image fields on product responses are now
{ sourceUrl, order }; theid/storageKey/productIdcolumns 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
externalIdentirely; the server returnsproductExternalIdso 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.

