> ## Documentation Index
> Fetch the complete documentation index at: https://docs.genlook.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating from the alpha

> What changed between the alpha and v1, with migration recipes.

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](/tryon-api/changelog).

<Note>
  Three small breakings, several quality-of-life additions, and a new lifetime model for products. Most integrations
  migrate in 10 minutes.
</Note>

<Note>
  **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`. **This is not a breaking change** — the shapes below are still accepted (deprecated). See the [Changelog](/tryon-api/changelog#1-4-0-2026-06-17) for the new canonical shapes.
</Note>

## At a glance

\| Area                          | Was (alpha)                        | Now (v1)                                                                                                                  |
\| ----------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
\| `POST /try-on` body shape     | `{ productId, customerImageId }`   | `{ product: { externalId or …inline… }, customer: { id or url or fileKey } }`                                             |
\| `PATCH /products/:externalId` | Redundant alias for upsert         | **Removed**. Use `POST /products` with the fields you want to change (partial updates supported).                         |
\| Product images on upsert      | `imageUrls: string[]` only         | `images: [{ url                                                                                                           | fileKey }]`. Mix URLs with multipart uploads. `imageUrls` still accepted as a deprecated alias. |
\| Customer image on `/try-on`   | `customerImageId` only             | One `customer` object with exactly one of `id`, `url`, or `fileKey`.                                                      |
\| Customer-image crop           | Always 4:5 person-aware            | `crop=true                                                                                                                | false`form field on`/images/upload`(default`true`).                                             |
\| Products & lifetime           | No expiry — products lived forever | Inline-created products expire 15 days after last use; products created via `POST /products` are kept forever by default. |
\| One-shot products             | Did not exist                      | Call `/try-on` without an `externalId` — the response includes a `productExternalId` you can re-use.                      |
\| `/try-on` response            | `{ generationId, status }`         | + `productExternalId` (the product's external ID, including any server-generated one).                                    |

## 1. `POST /try-on` — single `product` field

The whole body collapses to one `product` field that covers three use cases.

<CodeGroup>
  ```bash Alpha theme={null}
  POST /tryon/v1/try-on
  { "productId": "shirt-42",
    "customerImageId": "..." }
  ```

  ```bash v1 — reference theme={null}
  POST /tryon/v1/try-on
  { "product": { "externalId": "shirt-42" },
    "customer": { "id": "..." } }
  ```

  ```bash v1 — inline upsert theme={null}
  POST /tryon/v1/try-on
  { "product": {
      "externalId": "shirt-42",
      "title": "Red tee",
      "description": "...",
      "images": [{ "url": "https://cdn.example/red-tee.jpg" }]
    },
    "customer": { "id": "..." } }
  ```

  ```bash v1 — one-shot (no externalId) theme={null}
  POST /tryon/v1/try-on
  { "product": {
      "title": "Red tee",
      "description": "...",
      "images": [{ "url": "https://cdn.example/red-tee.jpg" }]
    },
    "customer": { "id": "..." } }
  ```
</CodeGroup>

The response now includes `productExternalId` — useful for one-shot calls so you can re-use the same product later (server-generated IDs start with `_anon_`).

<Warning>Sending the old `productId` field returns **400 Bad Request**. There's no alias.</Warning>

## 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.

```bash Alpha theme={null}
PATCH /tryon/v1/products/shirt-42
{ "title": "Red tee — restocked" }
```

```bash v1 theme={null}
POST /tryon/v1/products
{ "externalId": "shirt-42", "title": "Red tee — restocked" }
```

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`](/tryon-api/endpoints/generation-status) when you need the result.

```bash Alpha theme={null}
GET /tryon/v1/generations?limit=50&status=PENDING
```

```bash v1 theme={null}
GET /tryon/v1/generations/<id>
```

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`

`imageUrls: string[]` still works in v1 but is deprecated; it will be removed in v2. The new shape lets URLs and multipart uploads mix in the same payload.

```bash Alpha theme={null}
{ "imageUrls": ["https://cdn.example/front.jpg", "https://cdn.example/back.jpg"] }
```

```bash v1 — URL only theme={null}
{ "images": [
    { "url": "https://cdn.example/front.jpg" },
    { "url": "https://cdn.example/back.jpg" }
  ] }
```

```bash v1 — multipart (mix URL + bytes) theme={null}
Content-Type: multipart/form-data

data: { "images": [
  { "url": "https://cdn.example/front.jpg" },
  { "fileKey": "back" }
]}
back: <binary>
```

## Behaviour changes (non-breaking, but visible)

These don't break callers — but the visible behaviour shifts. Worth a skim.

### Product lifetimes by creation path

| Creation path                        | Default lifetime      | Override?                 |
| ------------------------------------ | --------------------- | ------------------------- |
| `POST /tryon/v1/products`            | Kept forever (`null`) | Yes — pass `validForDays` |
| `POST /try-on` inline + `externalId` | 15 days from last use | Yes — pass `validForDays` |
| `POST /try-on` one-shot              | 7 days from last use  | **No** — fixed            |

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`](/tryon-api/endpoints/upload-image). The only path with control over cropping (set `crop=false` on upload).
* `customer.url` — server downloads on every call. Always 4:5-cropped.
* `customer.fileKey` — multipart file in the same `/try-on` request. Always 4:5-cropped.

```json Alpha theme={null}
{ "product": { ... }, "customerImageId": "..." }
```

```json v1 theme={null}
{ "product": { ... }, "customer": { "id": "..." } }
```

The old nested `customer: { imageId }` form is also gone — the field is now `id`. Migration is a two-line search-replace.

<Note>
  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](/tryon-api/changelog#1-3-0-2026-06-17).

  Then as of **1.4.0** (same day), the whole try-on body was renamed: `product` → `products: [...]`, `customer` → `person.image`, `customerId` → `externalUserId`, and `useWatermark` / `retentionDays` → `output.watermark` / `output.keepForDays`. Still not breaking — the older shapes remain accepted. See the [Changelog](/tryon-api/changelog#1-4-0-2026-06-17).
</Note>

### 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 + 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](/tryon-api/errors).

```json Was (alpha) theme={null}
{ "error": "Product not found", "code": "INVALID_PRODUCT" }
```

```json Now (v1) theme={null}
{ "code": "PRODUCT_NOT_FOUND", "message": "Product 'shirt-42' not found.", "status": 404 }
```

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 }`.
* [ ] 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) Replace `imageUrls` with `images: [{ url }, ...]`. Deprecation window is one major.
* [ ] (Optional) If you're shipping byte uploads on every call, switch to upserting once and referencing by `externalId` — the new TTL refresh keeps the row alive as long as you keep using it.
* [ ] (Optional) For one-shot use cases, drop `externalId` entirely — server returns `productExternalId` so you can ref the row if you want.
* [ ] Add `productExternalId` handling to your response parsers if you want to capture anonymous IDs.

## Need help?

[support@genlook.app](mailto:support@genlook.app) — we'll happily review your integration's diff and flag anything that needs updating.
