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