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

# JavaScript SDK

> Build virtual try-on into any app with the @genlook/storefront SDK: custom UIs, native apps, headless storefronts. Uploads, generation, quotas, events, and errors handled for you.

`@genlook/storefront` is the official Genlook SDK, and the recommended way to build on Genlook from JavaScript or TypeScript. It is the same core our own widget runs on: it handles photo upload and validation, try-on generation, the shopper's identity and quota, consent, errors, and the analytics that power the merchant dashboard. The interface is entirely yours; the SDK is headless and has no UI and no DOM dependency, so it runs in browsers, React Native, and Node alike.

```bash theme={null}
npm install @genlook/storefront
```

It has zero runtime dependencies and ships as ESM.

## Quickstart

```javascript theme={null}
import { createTryOnClient } from "@genlook/storefront";

const genlook = createTryOnClient({
  publishableKey: "pk_your_key_here",
  storeId: "your-store.myshopify.com",
});

// The shopper picked a photo
const { fileId } = await genlook.uploadPhoto(file, {
  fileSize: file.size,
  mimeType: file.type,
  fileName: file.name,
  uploadSource: "gallery",
  skipClientDimensionCheck: false,
});

// Run the try-on. Resolves when the image is ready.
const { imageUrl } = await genlook.generate({
  userImageId: fileId,
  productId: "gid://shopify/Product/1234567890",
});
```

That is the whole flow. On Shopify and SHOPLINE you send only a product id; Genlook reads the title and images from your platform. On WooCommerce and PrestaShop the same SDK runs through your site's plugin proxy; see [WooCommerce and PrestaShop](#woocommerce-and-prestashop).

<Note>
  The `pk_` publishable key is safe to embed in client code: it can only do what a shopper on your storefront could already do. Merchants create it in the Genlook app under **Settings → Publishable key**. It is not the store API key; if a key does not start with `pk_`, it does not belong in anything a shopper can read.
</Note>

## Creating the client

`createTryOnClient(options)` returns a ready client. The options you will actually use:

| Option                        | Default                   | Purpose                                                                                                                                                            |
| ----------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `publishableKey`              | required                  | Your `pk_` key.                                                                                                                                                    |
| `storeId`                     | none                      | Your store domain. Scopes settings, quota, and history per store.                                                                                                  |
| `customerId`, `customerEmail` | `null`                    | The signed-in shopper, when you know them. Links try-ons across devices and feeds the merchant's analytics.                                                        |
| `locale`                      | none                      | Shopper locale, for localized behavior.                                                                                                                            |
| `storage`                     | `localStorage`            | Key-value storage for identity, history, and quota. **Required on React Native** (see below). Falls back to in-memory storage where `localStorage` is unavailable. |
| `tracking`                    | `"granted"`               | Set `"denied"` to switch off analytics reporting entirely.                                                                                                         |
| `requireLegalConsent`         | `false`                   | Gate photo uploads behind an explicit consent step (see [consent](#staged-uploads-and-consent)).                                                                   |
| `limits`                      | from store settings       | Override the per-shopper quota (`maxGenerations`, `period`, `emailCollectionStep`) instead of inheriting the merchant's settings.                                  |
| `baseUrl`                     | `https://api.genlook.app` | Only for testing against a different environment.                                                                                                                  |

On creation, the client mints and persists a stable anonymous id for the shopper and fetches the store's settings (quota, email collection step, and so on), cached for five minutes. Options you set explicitly always win over store settings.

## WooCommerce and PrestaShop

On WooCommerce and PrestaShop, your site's Genlook plugin already runs a proxy that speaks this same API, with your site's credentials attached server-side. The SDK drives it by swapping the transport; no publishable key is needed:

```javascript theme={null}
import { createTryOnClient, createFetchTransport } from "@genlook/storefront";

const genlook = createTryOnClient({
  transport: createFetchTransport({
    baseUrl: "https://yourshop.com",
    apiPath: "wp-json/genlook/v1/public", // the Genlook WordPress plugin's proxy
  }),
  storeId: "yourshop.com",
});
```

Everything else on this page works the same, and you still send only a product id: on the try-on call, the proxy resolves the product's title and images from your catalog server-side. WooCommerce product ids can be bare numeric ids or Genlook GIDs (`gid://genlook/WooCommerce/Product/123`).

<Note>
  On PrestaShop, the module's proxy is protected by a storefront session token, so this route works from pages your shop serves: pass a custom `transport` that calls the module's proxy URL the same way the widget on the page does. Building a native app on PrestaShop? Email [support@genlook.app](mailto:support@genlook.app) and we will set you up.
</Note>

## Uploading photos

`uploadPhoto(file, meta)` runs the full three-step upload (request a signed URL, upload the bytes, confirm) and resolves with a `fileId` you pass to `generate`.

Pass real metadata so the SDK can validate before anything hits the network:

* `fileSize`, `mimeType`, `fileName`: from the picked file.
* `dimensions`: the image's width and height if you can read them. Uploads below **500 × 625 px** are rejected early with a clear reason instead of producing a bad try-on. If you skip `dimensions`, the check is skipped.
* `uploadSource`: `"gallery"` or `"mirror"` (camera).

Validation limits are exported so your UI can pre-check: `MAX_UPLOAD_BYTES` (20 MB) and `ALLOWED_UPLOAD_MIME_TYPES` (JPEG, PNG, WebP, HEIC, HEIF). An invalid file rejects with an `UploadRejectedError` whose `reason` is one of `invalid_image_type`, `file_too_large`, or `invalid_dimensions`.

### Staged uploads and consent

If you must show a consent step before a photo may leave the device, set `requireLegalConsent: true` and use `stagePhoto` instead of `uploadPhoto`:

```javascript theme={null}
genlook.stagePhoto(file, meta);          // held in memory, nothing on the wire yet
genlook.acceptLegalConsent("2026-01");   // releases the upload immediately
```

`stagePhoto` uploads immediately when consent is already on record, and otherwise holds the bytes until `acceptLegalConsent` is called. Staging a new photo supersedes the previous one, and `clearPendingUpload()` discards everything held or in flight. To generate with whatever photo the shopper picked last, pass `userImage: "latest"` to `generate` instead of a `fileId`.

## Running try-ons

```javascript theme={null}
const { imageUrl, generationId } = await genlook.generate({
  userImageId: fileId,
  productId: "gid://shopify/Product/1234567890",
  variantId: "gid://shopify/ProductVariant/9876543210", // optional
});
```

`generate` resolves when the try-on image is ready, usually in 10 to 20 seconds; polling is handled for you. Useful behavior you get for free:

* **Duplicate protection**: calling `generate` again with the same photo, product, and variant within 60 seconds returns the same in-flight promise instead of burning another try-on.
* **Fair quota counting**: the shopper's quota is counted when a try-on is accepted, and refunded if it fails.
* **History**: pass a `context` object and the result is appended to the client's local history (see [results](#results-history-and-sharing)).

## Knowing what is allowed: `can()`

Before rendering your try-on button or upload step, ask the client:

```javascript theme={null}
const verdict = genlook.can("generate"); // or "upload"
if (!verdict.ok) {
  // verdict.blocked tells you what to render
}
```

| `blocked` reason   | Meaning                                                   | What your UI does                                                         |
| ------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------- |
| `quota-exceeded`   | The shopper hit the merchant's per-shopper limit.         | Show when they can try again.                                             |
| `email-required`   | The merchant's email collection step is due.              | Collect an email, then call `collectEmail({ email })`.                    |
| `consent-required` | `requireLegalConsent` is on and not yet accepted.         | Show your consent step, then `acceptLegalConsent(version)`.               |
| `login-required`   | The merchant restricted try-on to logged-in customers.    | This channel cannot satisfy it; ask the merchant to turn the setting off. |
| `credits-expired`  | The store cannot run try-ons right now (plan or billing). | Hide or disable the button.                                               |
| `concurrency`      | A configured max of parallel generations is reached.      | Wait for the current one.                                                 |

`generate` and `uploadPhoto` run the same checks and reject with a `PolicyBlockedError` carrying the same `reason`, so you can gate up front with `can()` or handle the rejection, whichever fits your UI.

## State and events

The client holds observable state (current upload, pending consent, quota usage, history):

```javascript theme={null}
const unsubscribe = genlook.subscribe(() => render(genlook.getState()));
```

For reacting to moments rather than rendering state, use the typed event bus:

```javascript theme={null}
genlook.on("tryon:generation_succeeded", (e) => {
  console.log(`done in ${e.duration_ms}ms`);
});
genlook.onAny((e) => console.log(e.type));
```

Events cover the whole funnel: `tryon:photo_submitted`, `tryon:photo_upload_started` / `_succeeded` / `_failed` / `_rejected`, `tryon:photo_discarded`, `tryon:legal_consent_accepted`, `tryon:generation_started` / `_succeeded` / `_failed` / `_blocked`, `tryon:share_link_created`, `tryon:email_collected`, and `tryon:data_erased`. Event payloads never contain personal data.

## Results, history, and sharing

Results generated with a `context` are kept in local history on the shopper's device, scoped to your store:

* `getHistory()` returns everything; `recentResults()` filters to the store's photo-retention window.
* `getShareUrl(entryId)` creates a public share link for a result.
* `deleteMyData()` is the shopper's "delete my data" action: it erases the shopper's photos, try-on images, and identity server-side ([details](/docs/virtual-tryon/endpoints/delete-my-data)), then wipes local history and any pending upload. It never rejects, so your UI can `await` it and navigate away; `tryon:data_erased` fires only when the server confirmed the erasure.

## Handling errors

The SDK throws three error classes. Match them by `err.name`, not `instanceof` (safer across bundlers):

| `err.name`              | When                                 | Key fields                                                              |
| ----------------------- | ------------------------------------ | ----------------------------------------------------------------------- |
| `PolicyBlockedError`    | The action is not allowed right now. | `reason`: same values as [`can()`](#knowing-what-is-allowed-can).       |
| `UploadRejectedError`   | The photo failed validation.         | `reason`: `invalid_image_type`, `file_too_large`, `invalid_dimensions`. |
| `GenerationFailedError` | The try-on could not run or failed.  | `kind` (below) and the raw backend `code`.                              |

Branch your UI on `GenerationFailedError.kind`:

| `kind`            | Meaning                                                                    |
| ----------------- | -------------------------------------------------------------------------- |
| `quota`           | The store reached its plan limit or has a billing issue.                   |
| `weekly_limit`    | The shopper hit a weekly allowance.                                        |
| `rate_limited`    | Too many requests in a short time; retry after a pause.                    |
| `overloaded`      | Genlook is at capacity; retry shortly.                                     |
| `upload_failed`   | The photo upload did not complete.                                         |
| `creation_failed` | The try-on could not start (for example, the product has no usable image). |
| `failed`          | Anything else; show a generic retry.                                       |

The backend `message` string is for your logs, not your UI; it is not localized and can change.

## React Native

The client keeps state in synchronous storage, which React Native cannot provide directly. Hydrate an async store once at startup, before constructing the client:

```javascript theme={null}
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createHydratedStorage, createTryOnClient } from "@genlook/storefront";

const storage = await createHydratedStorage(AsyncStorage);
const genlook = createTryOnClient({ publishableKey, storeId, storage });
```

Two rules keep it correct:

1. **Await hydration before creating the client.** An unhydrated store makes the client mint a fresh anonymous id, which resets the shopper's history and quota.
2. **Flush on background.** Call `genlook.flushEvents({ beacon: true })` when the app is backgrounded so pending analytics are not lost.

Everything else is identical to the web.

## Tracking and privacy

The SDK reports the try-on funnel events that power the merchant's analytics. This is on by default, carries no personal data, and switches off with one option:

```javascript theme={null}
createTryOnClient({ publishableKey, storeId, tracking: "denied" });
```

## Lifecycle

Long-lived apps can call `genlook.dispose()` on teardown; it flushes pending events and stops the client's background timer. `flushEvents()` can be called anytime and never rejects.

## Going lower level

<CardGroup cols={2}>
  <Card title="Direct API access" icon="terminal" href="/docs/virtual-tryon/direct-api-access">
    Not on JavaScript? The same channel over raw HTTP, with every endpoint documented.
  </Card>

  <Card title="Custom button on Shopify" icon="code" href="/docs/virtual-tryon/custom-button">
    Keep Genlook's widget but design your own button in your theme. One call: `Genlook.cabin.open()`.
  </Card>
</CardGroup>
