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

# Direct API Access

> Run try-ons over raw HTTP with a publishable key: the channel behind the @genlook/storefront SDK, for native apps, servers, and everything that is not JavaScript.

This page documents the raw HTTP channel behind Genlook try-ons. On JavaScript or TypeScript, use the [`@genlook/storefront` SDK](/docs/virtual-tryon/sdk) instead: it is the same channel with upload, polling, quota, and errors already handled. Come here when you are not on JavaScript, or when you would rather not take a dependency. Everything the SDK does is built on these endpoints, and nothing is reserved.

## Why not go through the storefront

The storefront path leans on things a browser on your shop's domain gets for free: cookies, a familiar user agent, and a session your platform already trusts. Code running anywhere else carries none of that, so it tends to be treated as a scraper. Mobile apps hit this hardest:

* **Bot protection** challenges requests that arrive without browser fingerprints, whether it comes from your platform's edge or a firewall you configured yourself.
* **Redirects** between your `myshopify.com` domain and your primary domain drop request bodies in several common HTTP clients.
* **Shared mobile IPs** are the norm. Carriers put thousands of subscribers behind a single address, so any protection that counts per IP will eventually block real customers.

None of this is a bug you can fix from inside your own code. The answer is to stop going through the storefront.

## Choosing your path

<CardGroup cols={2}>
  <Card title="Call Genlook directly" icon="key">
    **Shopify and SHOPLINE.** Your code calls Genlook directly with a publishable key. Nothing sits in between.
  </Card>

  <Card title="Through your site" icon="server">
    **WooCommerce and PrestaShop.** Your code calls the Genlook proxy that the plugin already runs on your site.
  </Card>
</CardGroup>

The split exists because on Shopify and SHOPLINE, Genlook reads your catalog from the platform, so a product id is all you send. On WooCommerce and PrestaShop the product details come from your site, so your site stays in the request path.

## Get your publishable key

In your Shopify admin, open the **Genlook Try-On** app, go to **Settings**, and press **Create key** in the **Publishable key** section. The key stays on that screen, so you can come back and copy it whenever you need it. On SHOPLINE, email [support@genlook.app](mailto:support@genlook.app) and we will issue the key for your store.

It is safe to embed in your code: it can only do what a shopper on your storefront could already do. It cannot read your catalog, reach the merchant dashboard, or touch any admin data.

<Warning>
  This is not your store API key. The store API key is an admin credential and must never leave a server you control. If a key does not start with `pk_`, it does not belong in anything a shopper can read.
</Warning>

Two more things the merchant can do from that screen:

* **Replace** the key if it leaked. The new key works immediately and the old one stops, so anything still shipping the old key breaks until you update it.
* **Turn off** the key, which closes this channel right away. The website widget is unaffected either way.

## Base URL and authentication

```
https://api.genlook.app/storefront/v1
```

Every request carries your publishable key:

```
Authorization: Bearer pk_your_key_here
```

## Identifying the shopper

| Header                     | Required | Purpose                                                                         |
| -------------------------- | -------- | ------------------------------------------------------------------------------- |
| `X-Genlook-Anonymous-Id`   | Yes      | A stable per-shopper id you generate once and persist. Must start with `anon_`. |
| `X-Genlook-Customer-Id`    | No       | Your platform's customer id, when the shopper is signed in.                     |
| `X-Genlook-Customer-Email` | No       | The signed-in shopper's email.                                                  |

The anonymous id is what ties a shopper to their try-on history and their quota, so generate it once and persist it: device storage in an app, a first-party cookie or local storage in a browser. A new id on every session means the shopper loses their history and gets a fresh quota, which is not the experience you want.

Customer identity is optional, and worth sending when you have it: it links try-ons to the shopper across their devices and feeds the merchant's analytics.

## The flow

```mermaid theme={null}
sequenceDiagram
    participant App as Your client
    participant API as api.genlook.app

    App->>API: GET /availability
    API-->>App: { allowed: true }

    App->>API: POST /uploads
    API-->>App: { uploadUrl, uploadKey }
    App->>API: PUT photo to uploadUrl
    App->>API: POST /uploads/:uploadKey/complete
    API-->>App: { fileId }

    App->>API: POST /try-ons
    API-->>App: { jobId }

    loop Every 2s
        App->>API: GET /try-ons/:jobId
        API-->>App: { status, resultImageUrl }
    end
```

```javascript theme={null}
const BASE = "https://api.genlook.app/storefront/v1";

const headers = {
  Authorization: `Bearer ${PUBLISHABLE_KEY}`,
  "X-Genlook-Anonymous-Id": deviceId, // "anon_..." persisted on device
  "Content-Type": "application/json",
};

async function tryOn(photo, productId) {
  // 1. Check the store can run a try-on right now
  const availability = await fetch(`${BASE}/availability`, { headers }).then((r) => r.json());
  if (!availability.allowed) throw new Error("No try-ons available");

  // 2. Upload the shopper's photo
  const { uploadUrl, uploadKey } = await fetch(`${BASE}/uploads`, {
    method: "POST",
    headers,
    body: JSON.stringify({ productId }),
  }).then((r) => r.json());

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

  const { fileId } = await fetch(`${BASE}/uploads/${encodeURIComponent(uploadKey)}/complete`, {
    method: "POST",
    headers,
  }).then((r) => r.json());

  // 3. Start the try-on. Only the product id is needed: the server reads
  //    the title and images from your platform.
  const { jobId, code, message } = await fetch(`${BASE}/try-ons`, {
    method: "POST",
    headers,
    body: JSON.stringify({ userImageId: fileId, productId }),
  }).then((r) => r.json());
  if (!jobId) throw new Error(`${code}: ${message}`);

  // 4. Poll. Most try-ons finish in 10 to 20 seconds.
  for (let i = 0; i < 30; i++) {
    const status = await fetch(`${BASE}/try-ons/${jobId}`, { headers }).then((r) => r.json());
    if (status.status === "COMPLETED") return status.resultImageUrl;
    if (status.status === "FAILED") throw new Error(status.errorMessage);
    await new Promise((r) => setTimeout(r, 2000));
  }

  throw new Error("The try-on request timed out");
}
```

## Things worth knowing

**Photo requirements.** Photos can be JPEG, PNG, WebP, HEIC, or HEIF, up to 20 MB, and at least 500 × 625 px. Validate on your side before uploading for the best experience.

**You send a product id, nothing more.** The server reads the title and images from your platform. Product details sent by the caller are ignored.

**Handled errors arrive as a 200.** A try-on that cannot start returns a 200 with a `code` in the body, so branch on the payload rather than the status. Authentication problems use real status codes: `401` for an unknown or replaced key, `403` for a blocked request.

**Several limits can refuse a try-on.** The store's plan, the per-shopper quota, and the ceilings the merchant sets on the key itself (a weekly total and a weekly per-IP allowance) all surface as `QUOTA_EXCEEDED` or `FITTING_ROOM_WEEKLY_LIMIT_EXCEEDED` on the try-on call. `GET /availability` reflects the store's plan, so check it before showing your button, but treat a refusal on the try-on itself as a state your UI handles too. One note for **server-side** integrations: all your requests come from one address, so ask the merchant to raise or clear the per-IP ceiling in their key settings.

**Try-ons count against the merchant's plan.** Same allowance as the storefront widget, not a separate subscription; a shopper's quota is shared between your app and the website.

**"Logged-in customers only" is not available on this channel.** If the merchant has that setting on, try-ons return `403 LOGIN_REQUIRED` even when you send a customer id. Ask them to turn it off and rely on your own sign-in instead.

**The store's settings are readable.** `GET /settings` returns the merchant's per-shopper quota and related widget settings, so your client can match the widget's behavior. The SDK does this for you.

**Report funnel events if you can.** `POST /events` feeds the merchant's analytics with the same funnel the widget reports; see the [Events endpoint](/docs/virtual-tryon/endpoints/events).

**Shoppers can erase their data.** [`DELETE /shopper`](/docs/virtual-tryon/endpoints/delete-my-data) removes the calling shopper's photos, try-on images, and identity. On this channel it is scoped to the calling device, never another shopper's data.

## Through your site

WooCommerce and PrestaShop stores already run a Genlook proxy as part of the plugin. Your app or server can call it exactly as your storefront does, and it will attach your product details and credentials on the server side.

This path avoids the problems described at the top of this page, because the infrastructure in the request path is yours. Any bot protection in front of it is protection you control and can allowlist.

On WordPress, the plugin serves this same API under:

```
https://yourshop.com/wp-json/genlook/v1/public
```

Same paths, same bodies as documented on this page, with two differences: no `Authorization` header (the proxy attaches your site's credentials server-side), and on the try-on call the proxy resolves the product's title and images from your catalog, so you still send only a product id. On JavaScript, [the SDK targets this proxy with one option](/docs/virtual-tryon/sdk#woocommerce-and-prestashop).

On PrestaShop, the module's proxy is protected by a storefront session token, so it is callable from pages your shop serves, using the proxy URL the widget on the page already uses.

## Endpoint reference

Every endpoint from the widget API is available on this channel, with the base URL and authentication header described above.

<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 Try-On" icon="wand-magic-sparkles" href="/docs/virtual-tryon/endpoints/create-try-on">
    Start a try-on job.
  </Card>

  <Card title="Try-On Status" icon="clock" href="/docs/virtual-tryon/endpoints/try-on-status">
    Poll a try-on until it completes.
  </Card>

  <Card title="Availability" icon="coins" href="/docs/virtual-tryon/endpoints/availability">
    Verify the store can run try-ons before starting.
  </Card>

  <Card title="Share Try-On" icon="share" href="/docs/virtual-tryon/endpoints/share-try-on">
    Create public share links for results.
  </Card>

  <Card title="Collect Email" icon="envelope" href="/docs/virtual-tryon/endpoints/collect-email">
    Record the shopper's email for the merchant's email collection step.
  </Card>

  <Card title="Delete My Data" icon="trash" href="/docs/virtual-tryon/endpoints/delete-my-data">
    Erase the shopper's photos, try-ons, and identity on request.
  </Card>

  <Card title="Events" icon="chart-simple" href="/docs/virtual-tryon/endpoints/events">
    Report funnel events that power the merchant's analytics.
  </Card>
</CardGroup>

## Error codes

A failed `try-ons` call returns a `message` and a `code`:

| Code                                 | Meaning                                                                                                   |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `QUOTA_EXCEEDED`                     | The store reached its plan's try-on limit, or a key ceiling was hit.                                      |
| `FITTING_ROOM_WEEKLY_LIMIT_EXCEEDED` | The shopper hit a weekly allowance.                                                                       |
| `RATE_LIMIT_EXCEEDED`                | Too many requests in a short time.                                                                        |
| `BILLING_NOT_ALLOWED`                | The store's plan expired or there is a billing issue.                                                     |
| `PRODUCT_BLOCKED`                    | Try-on is disabled for this product.                                                                      |
| `UPLOAD_FAILED`                      | The photo upload did not complete; re-upload and retry.                                                   |
| `CREATION_FAILED`                    | The try-on could not be started. Also covers a product that could not be resolved or has no usable image. |

The `message` is for your logs, not your UI; it is not localized and can change. Authentication problems are different: an unknown or replaced key returns a `401` before any of this, and a store that requires shopper login returns a `403` with `LOGIN_REQUIRED` (see the notes above).
