@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.
Quickstart
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: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 skipdimensions, the check is skipped.uploadSource:"gallery"or"mirror"(camera).
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, setrequireLegalConsent: 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
generateagain 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
contextobject 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):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 acontext 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 canawaitit and navigate away;tryon:data_erasedfires only when the server confirmed the erasure.
Handling errors
The SDK throws three error classes. Match them byerr.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:- 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.
- Flush on background. Call
genlook.flushEvents({ beacon: true })when the app is backgrounded so pending analytics are not lost.
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 callgenlook.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().
