> ## 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 Custom Flow

> Build a fully custom virtual try-on experience on Shopify using the Genlook widget API: upload, generate, poll, and display results in your own UI.

Choose this approach to build a completely bespoke try-on experience. You own the upload screens, loading states, and result display; Genlook's backend handles the AI generation.

<Warning>The **Genlook Try-On** app embed must be enabled in your theme. It provides authentication context and, if you use it, the `Genlook.cabin.http.fetch` helper.</Warning>

## Architecture

All requests go through your store's Shopify app proxy, so authentication is handled for you and no keys ever touch the browser:

```
Base URL: https://your-store.myshopify.com/apps/proxy_genlook-x/public
```

```mermaid theme={null}
sequenceDiagram
    participant User
    participant YourUI as Your Custom UI
    participant Proxy as Shopify App Proxy
    participant Genlook as Genlook Backend

    User->>YourUI: Clicks try-on
    YourUI->>Proxy: GET /check-credits
    Proxy->>Genlook: Forward
    Genlook-->>YourUI: { allowed: true }

    User->>YourUI: Uploads photo
    YourUI->>Proxy: POST /prepare-upload
    Genlook-->>YourUI: { uploadUrl, uploadKey }
    YourUI->>Genlook: PUT file to uploadUrl
    YourUI->>Proxy: POST /upload-complete
    Genlook-->>YourUI: { fileId }

    YourUI->>Proxy: POST /fitting-room
    Genlook-->>YourUI: { jobId }

    loop Poll every 2s
        YourUI->>Proxy: GET /generation/:jobId
        Genlook-->>YourUI: { status, resultImageUrl }
    end

    User->>YourUI: Views result
    YourUI->>Proxy: POST /rate-generation (optional)
    YourUI->>Proxy: POST /share-generation (optional)
    YourUI->>Proxy: POST /collect-email (optional)
```

## End-to-end example

A minimal vanilla JavaScript implementation of the core flow. Note the ID formats: `productId` is a Shopify product GID, and the `fileId` returned by `upload-complete` is an opaque string you pass back unchanged.

```javascript theme={null}
async function tryOn(photoFile, productId) {
  // productId: "gid://shopify/Product/1234567890"
  const base = "/apps/proxy_genlook-x/public";

  // 1. Check the store can generate right now
  const credits = await fetch(`${base}/check-credits`).then((r) => r.json());
  if (!credits.allowed) throw new Error("No generation credits available");

  // 2. Upload the shopper's photo (3-step signed-URL flow).
  //    Passing productId here is optional but lets Genlook warm the
  //    product images while the shopper's photo uploads.
  const { uploadUrl, uploadKey } = await fetch(`${base}/prepare-upload`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ productId }),
  }).then((r) => r.json());

  await fetch(uploadUrl, {
    method: "PUT",
    headers: { "Content-Type": "application/octet-stream" },
    body: photoFile,
  });

  const { fileId } = await fetch(`${base}/upload-complete`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ uploadKey }),
  }).then((r) => r.json());
  // fileId looks like "customer-media/acc_123/file_abc123xyz.jpg"

  // 3. Start the AI generation
  const { jobId, code, message } = await fetch(`${base}/fitting-room`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ userImageId: fileId, productId }),
  }).then((r) => r.json());
  if (!jobId) throw new Error(`${code}: ${message}`);

  // 4. Poll for the result (most try-ons finish in 10 to 20 seconds)
  for (let i = 0; i < 30; i++) {
    const status = await fetch(`${base}/generation/${jobId}`).then((r) => r.json());

    if (status.status === "COMPLETED") return status.resultImageUrl;
    if (status.status === "FAILED") throw new Error(status.errorMessage);

    await new Promise((resolve) => setTimeout(resolve, 2000));
  }

  throw new Error("The generation request timed out");
}
```

<Note>
  If the Genlook SDK is on the page, `Genlook.cabin.http.fetch("/check-credits")` saves you from hardcoding the proxy path. See the [custom button guide](/docs/virtual-tryon/custom-button#genlook-cabin-http-fetch-path-options) for details.
</Note>

## Endpoint reference

<CardGroup cols={2}>
  <Card title="Upload Image" icon="upload" href="/docs/virtual-tryon/endpoints/upload-image">
    The 3-step signed-URL flow for shopper photos.
  </Card>

  <Card title="Create Generation" icon="wand-magic-sparkles" href="/docs/virtual-tryon/endpoints/create-generation">
    Start a try-on generation job.
  </Card>

  <Card title="Generation Status" icon="clock" href="/docs/virtual-tryon/endpoints/generation-status">
    Poll a generation until it completes.
  </Card>

  <Card title="Rate Generation" icon="thumbs-up" href="/docs/virtual-tryon/endpoints/rate-generation">
    Record shopper feedback on a result.
  </Card>

  <Card title="Check Credits" icon="coins" href="/docs/virtual-tryon/endpoints/check-credits">
    Verify the store can generate before starting.
  </Card>

  <Card title="Collect Email" icon="envelope" href="/docs/virtual-tryon/endpoints/collect-email">
    Send collected shopper emails to Genlook.
  </Card>

  <Card title="Share Generation" icon="share" href="/docs/virtual-tryon/endpoints/share-generation">
    Create public share links for results.
  </Card>

  <Card title="Download Image" icon="download" href="/docs/virtual-tryon/endpoints/download-image">
    Download results without CORS issues.
  </Card>

  <Card title="Tracking Events" icon="chart-line" href="/docs/virtual-tryon/endpoints/tracking-events">
    Report funnel events so your dashboard analytics stay accurate.
  </Card>
</CardGroup>

## Error codes

Failed `fitting-room` calls return a `message` and a `code`:

| Code                  | Meaning                                               |
| --------------------- | ----------------------------------------------------- |
| `QUOTA_EXCEEDED`      | The store reached its monthly generation limit.       |
| `RATE_LIMIT_EXCEEDED` | Too many requests in a short time.                    |
| `BILLING_NOT_ALLOWED` | The store's plan expired or there is a billing issue. |
| `CREATION_FAILED`     | The generation job failed to initialize.              |
