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

# Full Example (Python)

> Recommended end-to-end Python flow: ref-first, upsert-on-miss, with the customer image pre-uploaded.

A runnable Python script implementing the **recommended workflow**: upload the customer photo once, reference products by `externalId`, fall back to a full inline upsert if the product expired. No catalog sync loop, no health checks, no surprises.

## Prerequisites

```bash theme={null}
pip install requests
```

## The script

```python theme={null}
import os, sys, time, requests

BASE = os.environ.get("GENLOOK_BASE_URL", "https://api.genlook.app/tryon/v1")
API_KEY = os.environ["GENLOOK_API_KEY"]  # required

session = requests.Session()
session.headers["x-api-key"] = API_KEY


# ── 1. Upload the customer photo once ───────────────────────

def upload_customer(file_path: str, *, crop: bool = True) -> str:
    """Returns an imageId you can reuse across many generations."""
    with open(file_path, "rb") as f:
        r = session.post(
            f"{BASE}/images/upload",
            files={"file": (os.path.basename(file_path), f, "image/jpeg")},
            data={"crop": "true" if crop else "false"},
        )
    r.raise_for_status()
    return r.json()["imageId"]


# ── 2. Try-on with ref-first, upsert-on-miss ────────────────

def try_on(image_id: str, external_id: str, full_payload: dict) -> str:
    """Reference the product if it's cached; full inline upsert if it's not.

    `full_payload` is the dict you'd pass as the single `products[]` item to upsert: at
    minimum `{title, description, images: [{source: {url|fileKey}}, ...]}`. The function
    adds `externalId` for you.
    """
    # First attempt: pure reference (cheap)
    r = session.post(
        f"{BASE}/try-on",
        json={
            "products": [{"externalId": external_id}],
            "person": {"image": {"source": {"id": image_id}}},
        },
    )
    if r.status_code == 404 and r.json().get("code") == "PRODUCT_NOT_FOUND":
        # TTL expired (or first time we've seen this externalId). Upsert + retry.
        r = session.post(
            f"{BASE}/try-on",
            json={
                "products": [{**full_payload, "externalId": external_id}],
                "person": {"image": {"source": {"id": image_id}}},
            },
        )
    r.raise_for_status()
    body = r.json()
    print(f"Generation queued: {body['generationId']}  product={body['productExternalId']}")
    return body["generationId"]


# ── 3. Poll until done ──────────────────────────────────────

def poll(generation_id: str, *, timeout_s: int = 180) -> dict:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        r = session.get(f"{BASE}/generations/{generation_id}")
        r.raise_for_status()
        data = r.json()
        status = data["status"]
        print(f"  {int(time.time() - (deadline - timeout_s)):3d}s  {status}")
        if status == "COMPLETED":
            return data
        if status == "FAILED":
            sys.exit(f"Generation failed: {data.get('errorMessage')}")
        time.sleep(2)
    sys.exit("Timed out")


# ── Run it ──────────────────────────────────────────────────

if __name__ == "__main__":
    CUSTOMER_PHOTO = "./customer.jpg"
    PRODUCT = {
        "title": "Red tee",
        "description": "Soft cotton regular fit",
        "images": [{"source": {"url": "https://cdn.example.com/red-tee.jpg"}}],
    }
    EXTERNAL_ID = "shirt-42"

    image_id = upload_customer(CUSTOMER_PHOTO)
    print(f"Customer uploaded: {image_id}")

    # Try a generation. Works on a cold start (will upsert) and on subsequent
    # calls (will just reference). Idempotent either way.
    gen_id = try_on(image_id, EXTERNAL_ID, PRODUCT)
    result = poll(gen_id)
    print(f"\n→ {result['resultImageUrl']}")
```

## Run it

```bash theme={null}
export GENLOOK_API_KEY=gk_your_api_key
python tryon_demo.py
```

Expected output (cold start, 1st run):

```
Customer uploaded: ephemeral/customer/ttl-7d/acc_abc/20260512-9f86d.jpeg
Generation queued: cm8gen456xyz  product=shirt-42
  0s  PENDING
  2s  PROCESSING
 14s  COMPLETED

→ https://storage.googleapis.com/...
```

Subsequent runs against the same `externalId` skip the upsert — the reference call succeeds on the first try, payload is \~50 bytes, no images re-uploaded server-side.

## Key points

* **Upload the customer photo once, reuse the `imageId`.** Cheapest path. Auto-deleted after your account's retention window (default 7 days).
* **Reference by `externalId` for repeat calls.** Skips re-shipping the product payload and keeps the server's image analysis warm.
* **Upsert again only when the product changes** or when `PRODUCT_NOT_FOUND` tells you the product has expired. No proactive sync needed.
* **URLs vs byte uploads**: same URL is served from cache — no re-download. Byte uploads re-upload every call, so pin them to the first upsert and reference by `externalId` afterwards.
* **One-shot generations**: drop the `externalId` entirely. The server returns a `productExternalId` you can stash and re-use if you want, or ignore — one-shot products clean themselves up after 7 days of inactivity.

## Where to go next

* [TypeScript SDK](/tryon-api/sdk) — the same workflow in TypeScript via `@genlook/api`, with typed errors and a built-in poller; see also the runnable [Next.js example app](https://github.com/GenlookLabs/virtual-try-on-api-example).
* [Create Try-On reference](/tryon-api/endpoints/create-try-on) — every option on the `/try-on` body, including inline customer images via URL or multipart.
* [Upsert Product reference](/tryon-api/endpoints/create-product) — the explicit catalog path (lifetime by default, listed in `GET /products`).
* [Changelog](/tryon-api/changelog) — release notes for the current major (v1).
* [Migrating from the alpha](/tryon-api/breaking-changes) — recipes for upgrading earlier integrations.
