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

# Create Try-On

> Run a virtual try-on. Reference an existing product or describe one inline; pre-upload the customer image or ship it in the same request.

Creates a virtual try-on generation. Runs asynchronously — poll [Generation Status](/tryon-api/endpoints/generation-status) for the result. Each generation consumes **1 credit**.

The body has three parts: a [`products`](#products) array describing what to try on, a [`person`](#person) image (the shopper photo, either pre-uploaded or shipped inline), and optional [`externalUserId`](#externaluserid) / [`output`](#output) controls.

## Products

`products` is an **array**. For now it must hold **exactly one** item (multi-product try-on is on the roadmap). Each item works three ways:

* **Reference an existing product** — pass `{ "externalId": "..." }` only. Cheapest call (\~50 bytes on the wire).
* **Describe a product inline** — pass `{ "externalId": "...", "title": ..., "description": ..., "images": [...] }`. Creates the product if it's new; updates it if it's not. Inline-created products live for 15 days from their last use; the server refreshes the timer on every generation, so an actively-used product never expires.
* **One-shot** — omit `externalId`. The server returns a generated ID in the response under `productExternalId` that you can pass back if you want to reference the same product later. One-shot products live for 7 days from their last use.

<ParamField body="products" type="array" required>
  Array of product specs to try on. **Exactly one item for now** — multi-product try-on is on the roadmap.

  <Expandable title="product item fields">
    <ParamField body="externalId" type="string">
      Your product ID. Omit for one-shot generations.
    </ParamField>

    <ParamField body="title" type="string">
      Optional. Helps the AI classify the product category more accurately. Preserved on updates if omitted.
    </ParamField>

    <ParamField body="description" type="string">
      Optional. Helps the AI classify the product category more accurately. Preserved on updates if omitted.
    </ParamField>

    <ParamField body="images" type="array">
      One or more image entries. Each entry has a `source` object holding **exactly one** of `url` (remote URL) or `fileKey` (name of a multipart file field in the same request), plus an optional `classifications` object alongside `source`.

      <Note>The old flat shape — `{ url }` / `{ fileKey, classifications }` with the fields at the top level — is still accepted but **deprecated**. Prefer `{ source: { url | fileKey }, classifications? }`.</Note>
    </ParamField>

    <ParamField body="validForDays" type="integer | null">
      How long the product lives between uses, in days. Pass a number (1–365) to set a custom lifetime; pass `null` to keep it forever; omit to use the default. Updates preserve the existing value when omitted.
    </ParamField>

    <ParamField body="metadata" type="object">
      Free-form JSON returned with the product on `GET /products/:externalId`.
    </ParamField>
  </Expandable>
</ParamField>

## Person

The shopper photo. `person.image.source` holds exactly one of `id`, `url`, or `fileKey`.

<ParamField body="person" type="object" required>
  <Expandable title="person fields">
    <ParamField body="image" type="object" required>
      <Expandable title="image fields">
        <ParamField body="source" type="object" required>
          Exactly one of `id`, `url`, or `fileKey`.

          <Expandable title="source fields">
            <ParamField body="id" type="string">
              Image id from a prior [`POST /images/upload`](/tryon-api/endpoints/upload-image). **Recommended** — decouples the
              upload from the generation so you can retry without re-uploading, and is the only path that controls cropping (set
              `crop=false` on upload).
            </ParamField>

            <ParamField body="url" type="string">
              Remote URL. The server downloads on every call. Always 4:5-cropped.
            </ParamField>

            <ParamField body="fileKey" type="string">
              Name of a multipart file field in the same request. Always 4:5-cropped.
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="externalUserId" type="string">
  Optional opaque identifier of your own end user (e.g. your user ID). **No PII** — it's used purely as an attribution and GDPR key. When set, the generation — and the person image uploaded as part of this call — can be wiped on demand via [`DELETE /customers/:customerId`](/tryon-api/endpoints/delete-customer) (pass the same value as the path segment).
</ParamField>

<ParamField body="output" type="object">
  Optional controls over the generated result.

  <Expandable title="output fields">
    <ParamField body="watermark" type="boolean" default="true">
      Composite your account's watermark logo onto the result. Set `false` to skip for this generation. No-op if your
      account has no logo configured. See [Watermark](/tryon-api/watermark).
    </ParamField>

    <ParamField body="keepForDays" type="integer">
      How long the generation's stored images are kept. One of `1`, `3`, `7`. Defaults to your account's configured retention window. Ignored when `person.image.source.id` references an already-uploaded image (the existing storage path is reused).
    </ParamField>
  </Expandable>
</ParamField>

<Note>
  The previous request shape is still accepted but **deprecated** (this is not a breaking change). The old form used a top-level singular `product` object, a `customer` object (`customer: { source: { id | url | fileKey } }`), a top-level `customerId`, and top-level `useWatermark` / `retentionDays`. Migrate to `products: [...]`, `person.image.source`, `externalUserId`, and `output.watermark` / `output.keepForDays`. See the [Changelog](/tryon-api/changelog#1-4-0-2026-06-17).
</Note>

## Recommended flow

1. Upload the person image once via [`POST /images/upload`](/tryon-api/endpoints/upload-image) — gets you an `imageId` you can reuse.
2. Call `/try-on` with `{ products: [{ externalId }], person: { image: { source: { id: imageId } } } }` for repeat generations against known products, or with a full inline product when introducing a new SKU.

The inline `person.image.source.url` and `person.image.source.fileKey` paths are convenient one-shot conveniences but they re-download/re-upload on every call. Stick to `/images/upload` once you're in steady state.

## Examples

<RequestExample>
  ```bash Reference + uploaded person theme={null}
  curl -X POST "https://api.genlook.app/tryon/v1/try-on" \
    -H "x-api-key: gk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "products": [{ "externalId": "shirt-42" }],
      "person": { "image": { "source": { "id": "ephemeral/customer/ttl-7d/.../20260512-….jpeg" } } }
    }'
  ```

  ```bash Inline upsert + uploaded person theme={null}
  curl -X POST "https://api.genlook.app/tryon/v1/try-on" \
    -H "x-api-key: gk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "products": [{
        "externalId": "shirt-42",
        "title": "Red tee",
        "description": "Soft cotton regular fit",
        "images": [{ "source": { "url": "https://cdn.example/red-tee.jpg" } }]
      }],
      "person": { "image": { "source": { "id": "ephemeral/customer/ttl-7d/.../20260512-….jpeg" } } }
    }'
  ```

  ```bash Anonymous one-shot theme={null}
  curl -X POST "https://api.genlook.app/tryon/v1/try-on" \
    -H "x-api-key: gk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "products": [{
        "title": "Red tee",
        "description": "Soft cotton regular fit",
        "images": [{ "source": { "url": "https://cdn.example/red-tee.jpg" } }]
      }],
      "person": { "image": { "source": { "id": "ephemeral/customer/ttl-7d/.../20260512-….jpeg" } } }
    }'
  ```

  ```bash Person via URL theme={null}
  curl -X POST "https://api.genlook.app/tryon/v1/try-on" \
    -H "x-api-key: gk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "products": [{ "externalId": "shirt-42" }],
      "person": { "image": { "source": { "url": "https://cdn.example/me.jpg" } } }
    }'
  ```

  ```bash Person + product as multipart bytes theme={null}
  curl -X POST "https://api.genlook.app/tryon/v1/try-on" \
    -H "x-api-key: gk_your_api_key" \
    -F 'data={
          "products": [{
            "title": "Red tee",
            "description": "Soft cotton",
            "images": [{ "source": { "fileKey": "primary" } }]
          }],
          "person": { "image": { "source": { "fileKey": "model" } } }
        }' \
    -F "primary=@shirt.jpg" \
    -F "model=@me.jpg"
  ```

  ```bash With externalUserId + output theme={null}
  curl -X POST "https://api.genlook.app/tryon/v1/try-on" \
    -H "x-api-key: gk_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "products": [{ "externalId": "shirt-42" }],
      "person": { "image": { "source": { "id": "ephemeral/customer/ttl-7d/.../20260512-….jpeg" } } },
      "externalUserId": "your-end-user-id",
      "output": { "watermark": true, "keepForDays": 7 }
    }'
  ```

  ```javascript JavaScript (recommended path) theme={null}
  // 1. Upload the person photo
  const uploadForm = new FormData();
  uploadForm.append("file", personFile);
  const { imageId } = await fetch("https://api.genlook.app/tryon/v1/images/upload", {
    method: "POST",
    headers: { "x-api-key": API_KEY },
    body: uploadForm,
  }).then((r) => r.json());

  // 2. Run the try-on (reference an existing product)
  const res = await fetch("https://api.genlook.app/tryon/v1/try-on", {
    method: "POST",
    headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      products: [{ externalId: "shirt-42" }],
      person: { image: { source: { id: imageId } } },
    }),
  }).then((r) => r.json());
  ```

  ```ts Node SDK (reference) theme={null}
  import { Genlook } from "@genlook/api";

  const client = new Genlook({ apiKey: process.env.GENLOOK_API_KEY! });

  // Cheapest path — product already exists, person image already uploaded.
  const { generationId } = await client.tryOn.create({
    products: [{ externalId: "shirt-42" }],
    person: { image: { source: { id: imageId } } },
  });
  ```

  ```ts Node SDK (inline upsert) theme={null}
  import { Genlook, ProductNotFoundError } from "@genlook/api";

  const client = new Genlook({ apiKey: process.env.GENLOOK_API_KEY! });

  // Creates the product on first use, refreshes its TTL on every reuse.
  const { generationId } = await client.tryOn.create({
    products: [{
      externalId: "shirt-42",
      title: "Red tee",
      description: "Soft cotton regular fit",
      images: [{ source: { url: "https://cdn.example.com/red-tee.jpg" } }],
    }],
    person: { image: { source: { id: imageId } } },
  });

  // Ref-first, upsert-on-miss pattern:
  try {
    await client.tryOn.create({
      products: [{ externalId: "shirt-42" }],
      person: { image: { source: { id: imageId } } },
    });
  } catch (err) {
    if (err instanceof ProductNotFoundError) {
      await client.tryOn.create({
        products: [{
          externalId: "shirt-42",
          title: "Red tee",
          description: "Soft cotton regular fit",
          images: [{ source: { url: "https://cdn.example.com/red-tee.jpg" } }],
        }],
        person: { image: { source: { id: imageId } } },
      });
    } else throw err;
  }
  ```
</RequestExample>

## Response

<ResponseField name="generationId" type="string" required>
  Poll [`GET /generations/:id`](/tryon-api/endpoints/generation-status) every 2s until status is `COMPLETED` or
  `FAILED`.
</ResponseField>

<ResponseField name="status" type="string" required>
  Initial status: `PENDING`.
</ResponseField>

<ResponseField name="productExternalId" type="string">
  The product's external ID. Echoes back the value you sent for named calls; on one-shot calls it's a server-generated
  ID you can store and re-use to reference the same product later.
</ResponseField>

<ResponseExample>
  ```json Success theme={null}
  {
    "generationId": "cm8gen456xyz",
    "status": "PENDING",
    "productExternalId": "shirt-42"
  }
  ```

  ```json One-shot theme={null}
  {
    "generationId": "cm8gen789abc",
    "status": "PENDING",
    "productExternalId": "_anon_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
  }
  ```

  ```json Insufficient credits (402) theme={null}
  {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Insufficient credits. Top up the account balance and retry.",
    "status": 402
  }
  ```

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

## Errors

| Code                          | HTTP | Meaning                                                                                            |
| ----------------------------- | ---- | -------------------------------------------------------------------------------------------------- |
| `PRODUCT_NOT_FOUND`           | 404  | The referenced `externalId` doesn't exist (or has expired). Retry with a full inline payload.      |
| `PRODUCT_IMAGES_REQUIRED`     | 400  | One-shot call (no `externalId`) without any image.                                                 |
| `CUSTOMER_IMAGE_REQUIRED`     | 400  | `person` is missing.                                                                               |
| `CUSTOMER_IMAGE_FETCH_FAILED` | 400  | `person.image.source.url` could not be downloaded — DNS fail, host unreachable, or non-2xx status. |
| `MULTIPART_FILE_NOT_FOUND`    | 400  | A `fileKey` from `data` doesn't appear in the multipart payload.                                   |
| `VALIDATION_FAILED`           | 400  | Request body failed validation — see `details[]` for the offending fields.                         |
| `INSUFFICIENT_CREDITS`        | 402  | Account is out of credits.                                                                         |
| `RESERVED_EXTERNAL_ID`        | 409  | `product.externalId` starts with `_anon_`, which is reserved for server-generated IDs.             |

Full catalog at [Errors](/tryon-api/errors). If a reference call returns `PRODUCT_NOT_FOUND`, the product has fallen out of cache — retry with the full inline payload to recreate it. No sync loop required.

## Watermarking

If your account has a logo configured, the result comes back with the logo composited in the bottom-left corner. Pass `output: { watermark: false }` to skip it on a single call. See [Watermark](/tryon-api/watermark) for configuration.
