Skip to main content

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.

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

pip install requests

The script

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 under `product` to upsert: at minimum
    `{title, description, images: [{url}|{fileKey}, ...]}`. The function adds
    `externalId` for you.
    """
    # First attempt: pure reference (cheap)
    r = session.post(
        f"{BASE}/try-on",
        json={
            "product": {"externalId": external_id},
            "customer": {"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={
                "product": {**full_payload, "externalId": external_id},
                "customer": {"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": [{"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

export GENLOOK_API_KEY=gk_your_api_key
python tryon_demo.py
Expected output (cold start, 1st run):
Customer uploaded: retention-7d/acc_abc/customer/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