Oyster Widget Web SDK
Embed the skin scan on any site yourself. Two modes: float (a launcher button) and inline (mounted into a container you provide). Use the playground to generate copyable integration code for your exact settings. On WordPress the WooCommerce plugin does all of this for you, and in a native app see the mobile guide.
Inline is embedded
Inline mode mounts the widget into your page via container. No floating toggle button is used in inline mode.
autoOpen
autoOpen controls whether the widget surfaces on init. In float mode the launcher is always shown — autoOpen: false just keeps the widget closed until the user clicks the launcher (or you call open()). In inline mode, false defers mounting entirely.
User is optional
If you don’t have user context (public page), omit user. The widget can prompt for an email when needed.
Overview
The Oyster widget is embedded by loading a UMD bundle that exposes a global OysterskinWidget. You create a widget instance using one of the factory methods:
- OysterskinWidget.createScanWidget(options)
- OysterskinWidget.createChatWidget(options)
- OysterskinWidget.createUnifiedWidget(options)
- OysterskinWidget.createConsultationWidget(options)
- OysterskinWidget.createVoiceAgentWidget(options)
Each factory returns an instance with open(), minimize(), and close(). The unified widget instance additionally exposes openScan(), openConsultation(), openChat(), and openVoiceAgent() for driving the widget to a specific view from host code.
In v1, minimize() only affects float mode (it returns the widget to the launcher). In inline mode, minimize() is a no-op.
Quickstart
Minimal standalone examples: float mode and inline mode.
Playground
This snippet reflects your current playground settings. Your public key is never inlined; replace YOUR_PUBLIC_KEY.
Loading the bundle
<script src="https://sandbox.widget-lib.oysterskin.com/v1/oysterskin-vendor-widget-web.umd.js"></script>
Modes (float vs inline)
Float mode
Create the widget with mode: "float". The floating launcher appears automatically on every embed. autoOpen: true (default) also opens the widget itself immediately; autoOpen: false leaves just the launcher, and the widget opens on click or when you call open().
Inline mode
Create the widget with mode: "inline" and provide a container. Inline mode mounts into your page and does not use a floating launcher.
Widget types
- Scan: createScanWidget
- Chat: createChatWidget
- Unified: createUnifiedWidget
- Consultation: createConsultationWidget
- Voice Agent: createVoiceAgentWidget
Unified widget: programmatic navigation
On a unified widget instance you can jump directly to any tile from host code. Each method opens the widget (if minimized) and navigates it to that view.
const widget = OysterskinWidget.createUnifiedWidget({ /* ... */ });
widget.openScan();
widget.openConsultation();
widget.openChat();
widget.openVoiceAgent();
These methods are present on every widget instance, but on a restricted widget (e.g. createScanWidget) the widget silently ignores navigation to disallowed views.
Unified widget: restricting the gallery
Pass widgetTypes to show only specific tiles in the home gallery. The router also blocks deep-linked routes outside the listed types.
const widget = OysterskinWidget.createUnifiedWidget({
publicKey: "YOUR_PUBLIC_KEY",
widgetTypes: ["scan", "consultation"], // hide chat + voice-agent tiles
callback: (message) => console.log(message),
});
Vendors can also configure this list centrally from the Oyster widget manager — no host code change required. When widgetTypes is passed from code it overrides the vendor-managed value.
Where config comes from
Most appearance and copy options can be set centrally from the Oyster Widget Manager in the vendor dashboard and are fetched at runtime. The recommended workflow:
- Set your branding, copy, and budget tiers once in the Widget Manager — no deploy required, every embed picks them up.
- From code, pass only what's page-specific or what you need to override for a particular widget instance. Anything you pass from code wins; anything you omit falls back to the manager-saved value.
Manager-controlled options
Set these in the Widget Manager; override per-instance from code only when needed.
- primaryColor, primaryTextColor
- displayLogo, logoUrl
- introMessage, messageBody
- budgetRanges, budgetCurrency, minBudget/maxBudget
- conversationStarters (chat / unified)
- widgetTypes (unified — restrict the home gallery)
- launcherPosition (float — which corner the launcher sits in, and how far from the edges)
- zIndex (float: the stacking order the widget paints at)
- Allowed domains
Code-only options
No manager equivalent — these are per-page or per-instance decisions.
- publicKey, callback — required on every instance
- mode, container, inlineWidth, inlineHeight — page-specific layout
- autoOpen, displayHoverPopup, displayAutoPopup — per-instance UX behaviour
- user — per-end-user context
In short: configure once in the Widget Manager; reach for code overrides only when an instance needs to differ.
Options reference
- key (string, required): vendor key.
- callback (function, required): called with widget events.
- onCollectPayment (function, optional): collect a scan payment on your own gateway. Only called when your account is set up to collect scan payments on your own gateway — see Collecting on your own gateway.
- mode ("float" | "inline", optional): defaults to "float".
- autoOpen (boolean, optional): defaults to true. Auto-opens the widget on init. In float mode the launcher is always shown — autoOpen only controls whether the widget also surfaces immediately. Set to false to keep only the launcher visible until the user clicks it (or you call open()).
- user (object, optional): { id, name?, email? }. If omitted, the widget can prompt for email when needed.
- container (HTMLElement | string, inline only): where the widget mounts.
- inlineWidth/inlineHeight (number | string, inline only): number = px.
- primaryColor (string, optional): widget theme color. Defaults vary by widget type.
- displayLogo (boolean, optional): whether to show the Oysterskin logo. Defaults to true.
- logoUrl (string, optional): custom logo image URL. When provided with displayLogo: true, replaces the default SVG logo with your custom image.
- introMessage (string, optional): launcher tooltip title text.
- messageBody (string, optional): launcher tooltip body text.
- displayHoverPopup (boolean, optional, float only): defaults to true. Show the launcher tooltip when the user hovers the launcher.
- displayAutoPopup (boolean, optional, float only): defaults to true. Auto-show the launcher tooltip (with a close button) once per browsing session per publicKey, after the host page finishes loading. Suppressed when autoOpen: true or once the user has opened the widget at least once.
- minBudget/maxBudget (number, optional): fallback budget boundaries. Defaults to 100 and 100000.
- budgetCurrency (string, optional): preferred currency code used for budget range rendering.
- budgetRanges (object, optional): config-driven range presets. Supports { low, mid, premium } or { NGN: { low, mid, premium } }.
- conversationStarters (string[], chat/unified only): optional suggestion prompts.
- widgetTypes (("scan" | "chat" | "consultation" | "voice-agent")[], unified only): restrict which tiles appear in the home gallery. Omit (or pass empty) to show all. The value set in your Oyster dashboard is used when this option is not provided.
- launcherPosition (object, optional, float only): where the launcher sits — { corner?, offsetX?, offsetY? }. See Moving the launcher.
- zIndex (number, optional, float only): the stacking order the widget paints at. See Putting your own dialogs above the widget.
Moving the launcher
By default the launcher sits in the bottom-right corner, 28px from each edge (16px on phones). Set launcherPosition to put it somewhere else — useful when your own chat bubble, cookie banner or back-to-top button already owns that corner.
- corner ("bottom-right" | "bottom-left" | "top-right" | "top-left"): defaults to "bottom-right".
- offsetX (number): distance in px from the left or right edge, whichever the corner names.
- offsetY (number): distance in px from the top or bottom edge.
The offsets are distances from the corner, not viewport coordinates, so the launcher keeps its corner on every screen size. Omit either one to keep the default margin for that axis. The panel the launcher opens follows it, growing out of the same corner. An offset large enough to push the launcher off a narrow screen is pinned to the edge instead, so it stays reachable on a phone.
const instance = OysterskinWidget.createScanWidget({
publicKey: "YOUR_PUBLIC_KEY",
launcherPosition: {
corner: "bottom-left",
offsetX: 24,
offsetY: 96,
},
callback: (message) => console.log(message),
})
You can also set the position in your Oyster dashboard, under Widget Manager, and leave it out of your code. Passing it here wins over the saved value, field by field: pass only corner and the saved offsets still apply.
Putting your own dialogs above the widget
By default the widget paints at the very top of the page's stacking order, so it stays visible over any storefront. If your page has a dialog of its own that has to come first, such as a newsletter or sign-up modal that opens on load, an age gate or a consent banner, set zIndex to a number below your dialog's own. The shopper finishes with your dialog, and the widget is still there behind it.
- zIndex (number): the widget's topmost layer, which is the panel the scan runs in. The launcher and its tooltip sit one below. Defaults to 2147483647; the lowest accepted value is 1.
So a page whose modal sits at 9999 wants a widget at 9000, not at 9999: equal layers leave the browser to break the tie by document order. Nothing else about the widget changes: the launcher stays in its corner and the panel still opens out of it.
const instance = OysterskinWidget.createScanWidget({
publicKey: "YOUR_PUBLIC_KEY",
// Our own sign-up modal is at 9999, so keep the widget under it.
zIndex: 9000,
callback: (message) => console.log(message),
})
You can also set this in your Oyster dashboard, under Widget Manager, and leave it out of your code. Passing it here wins over the saved value.
Budget ranges example
const instance = OysterskinWidget.createScanWidget({
publicKey: "YOUR_PUBLIC_KEY",
budgetCurrency: "NGN",
budgetRanges: {
NGN: {
low: { min: 30000, max: 60000, label: "Low Budget" },
mid: { min: 60000, max: 100000, label: "Mid Budget" },
premium: { min: 100000, max: 200000, label: "Premium" }
}
},
callback: (message) => console.log(message),
});
Custom logo example
Use logoUrl to display your own logo instead of the default Oysterskin logo:
const instance = OysterskinWidget.createScanWidget({
publicKey: "YOUR_PUBLIC_KEY",
mode: "float",
displayLogo: true,
logoUrl: "https://your-domain.com/logo.png",
callback: (message) => console.log(message),
});
Events and callback
Your callback receives { event, data }. Events are emitted by the widget via postMessage, and forwarded by this library after origin/source checks.
Supported callback events
- closed: user closed the widget UI. data: {}
- minimized: user minimized the widget UI (float mode only). data: {}
- checkout: user clicked checkout from within the widget. data: CheckoutPayload
- cartItemAdded: an item was added or its quantity was increased. data: CartItemChangedPayload
- cartItemRemoved: an item was removed or its quantity was decreased. data: CartItemChangedPayload
- scanCompleted: scan completed and produced a batch id. data: { batch_id }
Recommended handler pattern
function onWidgetMessage(message) {
switch (message.event) {
case 'checkout':
// message.data: { total_quantity, total_amount, currency, checkout_items, user? }
// `user` is { id?, name?, firstName?, lastName?, email? } when the widget
// has a profile on hand (collected before scan or supplied via config.user),
// otherwise omitted entirely.
break;
case 'cartItemAdded':
case 'cartItemRemoved':
// message.data: { total_quantity, changed_item, checkout_items }
break;
case 'scanCompleted':
// message.data: { batch_id }
break;
case 'minimized':
case 'closed':
break;
default:
// Future-proofing: ignore unknown events
break;
}
}
Example payloads
checkout
{
"event": "checkout",
"data": {
"total_quantity": 2,
"total_amount": 12800,
"currency": "NGN",
"checkout_items": [
{
"product_id": 123,
"product_name": "Name",
"quantity": 2,
"brand_name": "Brand",
"sku": "SKU-123",
"image_url": "https://example.com/image.jpg",
"stock_level": 10
}
],
"user": {
"id": "user-123",
"firstName": "Jane",
"lastName": "Doe",
"email": "[email protected]"
}
}
}
user is included when the widget has a profile on hand — either collected before the scan (contact step) or supplied via config.user. It is omitted entirely when nothing is known, so you can distinguish "no profile" from "anonymous". All fields inside user are optional.
cartItemAdded (payload shape is identical for cartItemRemoved)
Tip: checkout_items can be an empty array if the widget doesn’t yet have inventory-backed product IDs for the items in the cart.
{
"event": "cartItemAdded",
"data": {
"total_quantity": 1,
"changed_item": {
"product_id": 123,
"product_name": "Name",
"quantity": 1,
"brand_name": "Brand",
"sku": null,
"image_url": null,
"stock_level": null
},
"checkout_items": []
}
}
scanCompleted
{
"event": "scanCompleted",
"data": {
"batch_id": "batch_abc123"
}
}
Collecting on your own gateway
By default, when a scan has to be paid for, the widget opens an Oyster checkout and Oyster receives the money. Your account can instead be set up to collect on your own gateway: the customer pays you, through your checkout, and Oyster invoices you for the scans. Contact support to have it enabled.
Nothing changes about who is asked to pay. That is still your configured scan payer.
The flow
- The widget asks Oyster to open a payment and gets back a reference.
- It calls your onCollectPayment handler with that reference.
- You collect however you like.
- Your server confirms to Oyster.
- The widget sees the confirmation and the scan runs.
Registering a handler
const widget = OysterskinWidget.createScanWidget({
publicKey: "YOUR_PUBLIC_KEY",
callback: handleMessage,
onCollectPayment: async ({ reference, amount, currency, email, batchId }) => {
// Create a checkout on your own gateway. Keep `reference` — your
// server needs it to confirm.
const res = await fetch("/my-store/scan-checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reference, amount, currency, email, batchId }),
});
const { checkoutUrl } = await res.json();
return { status: "redirect", checkoutUrl };
},
});
Without a handler, customers of a vendor on this mode cannot pay — the widget logs an error and tells them collection is unavailable. Register it before asking for this to be enabled.
What you can return
- { status: "redirect", checkoutUrl }: send the customer to your checkout. The widget reuses the window it already opened on their click, so pop-up blockers do not interfere.
- { status: "processing" }: you are collecting another way — your own modal, a card terminal, an account balance. The widget waits for your server.
- { status: "cancelled", reason? }: the customer backed out.
- { status: "failed", reason? }: collection could not start.
Send cancelled or failed rather than staying silent. A handler that never resolves leaves the customer watching a spinner until the widget gives up.
Only your server can complete a payment
Nothing you return from onCollectPayment settles anything. It only decides what the customer sees. The scan is admitted when your backend calls:
POST https://api.oysterskin.com/api/v1/scan-payments/{reference}/external/confirm
Authorization: Bearer <your Oyster API key>
{ "status": "success", "external_reference": "your-order-id" }
Send { "status": "failed" } the same way when a charge declines or is abandoned, so the customer is told promptly instead of waiting out the timeout.
Retries are safe: confirming an already-settled payment returns replayed: true, changes nothing, and should be treated as success. One collection can never become two billable scans.
The amount you charge is yours to decide. Oyster records what it would have charged for reconciliation and never validates yours against it.
Framework patterns
Recommended: load the UMD script once, keep one widget instance, and reuse it. In inline mode, mount into a stable container.
Embedding inside an iOS or Android app works the same way, with a host page of your own in a WebView. See Embedding the widget in a mobile app.
On WooCommerce there is nothing to load by hand: the plugin handles the keys, the allowed domains and the placement for you. See Setting up Oyster for WooCommerce.
Advanced patterns
For inline mode, use autoOpen: false + call open() from your CTA to defer the widget entirely and improve page performance. (In float mode the launcher is always shown, so autoOpen: false just keeps the widget itself closed — it doesn't avoid loading the launcher.)
Versioning
Use the major channel, /v1/, in production. You receive fixes and patches without chasing every release, and no breaking change lands inside a major.
// Production (major channel)
<script src="https://widget-lib.oysterskin.com/v1/oysterskin-vendor-widget-web.umd.js"></script>
// Sandbox (major channel)
<script src="https://sandbox.widget-lib.oysterskin.com/v1/oysterskin-vendor-widget-web.umd.js"></script>
/latest/ is for testing only: it can cross a major and take a breaking change with it. If your release process needs a frozen artifact rather than a moving channel, talk to us before you build around one.
CSP / security headers
Allow the widget script + widget origins for the environment you use.
Troubleshooting
If OysterskinWidget is undefined, verify your script URL and CSP.
Launch checklist
- Keys: no keys committed; loaded at runtime from your config/secret system.
- Callback: events are handled and logged in staging.
- CSP: script-src and frame-src updated for the widget domains.
- Inline container: ensure your container exists before creating the widget.