Skip to main content
@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.
It has zero runtime dependencies and ships as ESM.

Quickstart

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

Creating the client

createTryOnClient(options) returns a ready client. The options you will actually use: 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:
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).
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 and we will set you up.

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. If you must show a consent step before a photo may leave the device, set requireLegalConsent: true and use stagePhoto instead of uploadPhoto:
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

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

Knowing what is allowed: can()

Before rendering your try-on button or upload step, ask the client:
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):
For reacting to moments rather than rendering state, use the typed event bus:
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), 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): Branch your UI on GenerationFailedError.kind: 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:
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:

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

Direct API access

Not on JavaScript? The same channel over raw HTTP, with every endpoint documented.

Custom button on Shopify

Keep Genlook’s widget but design your own button in your theme. One call: Genlook.cabin.open().