Oyster Widget Developer Docs

Integrate the Oyster (Oysterskin) widget by loading one UMD script and creating a widget instance. Use the snippets below as-is, then customize options and handle events in your callback.

What you ship

One script tag plus a small init call. The widget opens as an embedded overlay and can be opened, minimized, or closed.

Two display modes

Float shows a floating launcher automatically. Inline lets you control when to call open().

Events

Your callback receives checkout, minimized, and closed events.

Overview

What it is, how it behaves

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)

Each factory returns an instance with open(), minimize(), and close().

Permissions: the widget is embedded using an iframe with allow="camera *; geolocation *" (this is not configurable by the integrator). If camera access is needed, your site must be served over HTTPS (browsers block camera on insecure origins).

Quickstart

Copy/paste baseline integration

Replace placeholders like YOUR_VENDOR_KEY and YOUR_END_USER_ID. Your callback is required and receives widget events.

Minimal standalone examples: float mode and inline mode.

Use the playground below to validate your key + event plumbing quickly (no code changes required).

Playground

Load, create, open, minimize, close
Important Your vendor key is never stored by this page (other non-secret fields may be remembered locally).
Pick the environment you want to test against. Most integrations start on sandbox, then switch to production.
Chat and Unified can accept conversation starters.
Float shows a launcher immediately. Inline requires calling open().
This doc does not store your key
Used for launcher/spinner styling and applied inside the widget UI.
Stable identifier you control (not an email address).
Used by the launcher hover popup text.
If starters are provided, chat/unified auto-open on create.
Event log (callback payloads)

This snippet reflects your current playground settings. Your vendor key is never inlined; replace YOUR_VENDOR_KEY.

Loading the bundle

UMD + global API

The widget is distributed as a UMD script that exposes a global OysterskinWidget. There are two environment bundles (prod + sandbox). Load exactly one of them:

<script src="https://sandbox.widget-lib.oysterskin.com/v0.2.0/oysterskin-vendor-widget-web.umd.js"></script>
<script src="https://widget-lib.oysterskin.com/v0.2.0/oysterskin-vendor-widget-web.umd.js"></script>

Hosting recommendations

  • Prefer loading from the official CDN for cache hit rate and updates.
  • If you self-host, pin a specific version in the URL and validate it in staging before promoting to production.
  • Load the script late (after your critical rendering path) unless you want the float launcher to appear immediately.

Modes (float vs inline)

Decide how the widget appears

Float mode

Create the widget with mode: "float". A floating launcher appears immediately; users open the widget by clicking it.

Inline mode

Create the widget with mode: "inline". No launcher is shown automatically. You decide when to call instance.open().

Both modes support minimize() which hides the widget and shows the floating launcher again.

Widget types

Which factory method to call
  • Scan: createScanWidget (widgetMode = scan)
  • Chat: createChatWidget (widgetMode = chat; supports conversation starters; routes to /chat)
  • Unified: createUnifiedWidget (widgetMode = all; supports conversation starters; routes to /)
  • Consultation: createConsultationWidget (widgetMode = consultation; routes to /consultation/booking)

All widget types share the same instance contract and most options.

Options reference

What you can configure

The widget factories accept an options object with the following fields:

  • key (string, required): vendor key used to sign the widget config.
  • user (object, required): { id, name?, email? }.
  • callback (function, required): called with widget events.
  • mode ("float" | "inline", optional): defaults to "float".
  • primaryColor (string, optional): defaults vary by widget type; affects launcher/spinner and the widget UI theme.
  • displayLogo (boolean, optional): defaults to true.
  • introMessage (string, optional): launcher popup title.
  • messageBody (string, optional): launcher popup body text.
  • minBudget/maxBudget (number, optional): used by the widget; defaults to 100 and 100000.
  • conversationStarters (string[], chat/unified only): pre-seeds suggestions; when provided, chat/unified auto-open.

Note: the type definition includes a couple fields not currently used by the runtime (primaryTextColor, skipVendorFetch). If you need them, confirm support in the bundle version you are shipping.

Events and callback

How to listen and react

Your callback(message) receives objects of the shape: { event: string, data: any }.

  • closed: widget requested close; the widget is removed from view.
  • minimized: widget requested minimize; the widget is hidden and the floating launcher is shown.
  • checkout: user completed a checkout flow; the widget minimizes and passes a payload in data.
function onWidgetMessage(message) {
  switch (message.event) {
    case "checkout":
      // message.data: checkout payload
      // Example: route to your checkout confirmation page
      break;
    case "minimized":
      // Widget minimized to launcher
      break;
    case "closed":
      // Widget closed
      break;
    default:
      // Future-proof: ignore unknown events
      break;
  }
}

Framework patterns

React/Next.js, SPAs, tag managers

React (client-side) pattern

Keep a single widget instance. Load the UMD script once, create on demand, and always clean up on unmount.

import { useEffect, useRef } from "react";

function loadScriptOnce(src) {
  return new Promise((resolve, reject) => {
    const existing = document.querySelector(`script[data-oyster-umd="1"]`);
    if (existing) return resolve();
    const s = document.createElement("script");
    s.src = src;
    s.async = true;
    s.setAttribute("data-oyster-umd", "1");
    s.onload = () => resolve();
    s.onerror = () => reject(new Error("Failed to load Oyster widget script"));
    document.head.appendChild(s);
  });
}

export function OysterWidgetButton({ vendorKey, user }) {
  const instanceRef = useRef(null);

  useEffect(() => {
    return () => {
      // Ensure the widget is closed if component unmounts
      try { instanceRef.current?.close?.(); } catch {}
      instanceRef.current = null;
    };
  }, []);

  async function open() {
    await loadScriptOnce("https://sandbox.widget-lib.oysterskin.com/v0.2.0/oysterskin-vendor-widget-web.umd.js");
    const api = window.OysterskinWidget;
    if (!instanceRef.current) {
      instanceRef.current = api.createScanWidget({
        key: vendorKey,
        user,
        mode: "inline",
        callback: (m) => console.log("Oyster widget message", m),
      });
    }
    instanceRef.current.open();
  }

  return <button onClick={open}>Open skin analysis</button>;
}

Next.js note

  • Call the widget only in a client component (add "use client").
  • Do not create the widget during SSR.

GTM / Tag Manager (Custom HTML tag)

This is useful when you can’t deploy code changes but can add a tag. You still must provide your vendor key at runtime.

<script>
(function () {
  var SCRIPT = "https://sandbox.widget-lib.oysterskin.com/v0.2.0/oysterskin-vendor-widget-web.umd.js";
  var s = document.createElement("script");
  s.src = SCRIPT;
  s.async = true;
  s.onload = function () {
    // Replace these with variables or dataLayer values
    var vendorKey = "{{OYSTER_VENDOR_KEY}}";
    var userId = "{{USER_ID}}";

    window.__oysterInstance = window.OysterskinWidget.createScanWidget({
      key: vendorKey,
      user: { id: userId },
      mode: "float",
      callback: function (m) { console.log("Oyster widget message", m); }
    });
  };
  document.head.appendChild(s);
})();
</script>

Single-page apps (route changes)

  • If your SPA fully replaces document.body content, keep the widget instance in an app-level singleton to avoid duplicates.
  • On user logout/account switch, call instance.close() and recreate with the new user identity.

Advanced patterns

Lazy load, singleton, CTA-driven open

Singleton instance (recommended)

Keep one instance per page and reuse it.

let oysterInstance = null;

export async function getOysterInstance({ scriptUrl, vendorKey, user, callback }) {
  if (!window.OysterskinWidget) {
    await new Promise((resolve, reject) => {
      const s = document.createElement("script");
      s.src = scriptUrl;
      s.async = true;
      s.onload = resolve;
      s.onerror = () => reject(new Error("Failed to load Oyster widget script"));
      document.head.appendChild(s);
    });
  }

  if (!oysterInstance) {
    oysterInstance = window.OysterskinWidget.createScanWidget({
      key: vendorKey,
      user,
      mode: "inline",
      callback,
    });
  }
  return oysterInstance;
}

CTA-driven open (inline mode)

Initialize only after user intent to reduce page work.

document.getElementById("open-skin-analysis").addEventListener("click", async () => {
  const instance = await getOysterInstance({
    scriptUrl: "https://sandbox.widget-lib.oysterskin.com/v0.2.0/oysterskin-vendor-widget-web.umd.js",
    vendorKey: window.APP_CONFIG.oysterVendorKey,
    user: { id: window.CURRENT_USER_ID },
    callback: (m) => console.log("Oyster widget message", m),
  });

  instance.open();
});

Versioning

Pin versions and promote safely
  • Pin a version in your script URL (example: /v0.2.0/) to avoid surprise behavior changes.
  • Promote through environments: sandbox/staging first, then production.
  • Monitor: log callback events + errors so you can correlate failures to version changes.
// Example: centralize configuration (no hardcoded keys in source)
window.APP_CONFIG = {
  oyster: {
  scriptUrl: "https://sandbox.widget-lib.oysterskin.com/v0.2.0/oysterskin-vendor-widget-web.umd.js",
    vendorKey: window.ENV?.OYSTER_VENDOR_KEY, // injected at deploy/runtime
  }
};

CSP / security headers

Common integration blocker

If you use a Content Security Policy, you must allow the widget script origin and the widget iframe origin. The exact origins depend on the environment/version you use.

Start with a staging-only relaxed policy, validate, then tighten. Prefer nonces/hashes for your own inline scripts.

# Example (adjust domains for your environment)
Content-Security-Policy:
  script-src 'self' https://widget-lib.oysterskin.com https://sandbox.widget-lib.oysterskin.com;
  frame-src https://widget-lib.oysterskin.com https://sandbox.widget-lib.oysterskin.com;
  connect-src 'self' https://widget-lib.oysterskin.com https://sandbox.widget-lib.oysterskin.com;
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';

Performance

Load timing + UX
  • Defer initialization in inline mode until the user clicks a CTA (best for page speed).
  • Preload the script on pages where the widget is frequently used (best for responsiveness).
  • Avoid multiple instances at once; keep a single instance and reuse it.

Troubleshooting

Fast fixes

OysterskinWidget is undefined

  • Verify your script URL is reachable (open it directly in a browser).
  • Confirm the script tag is not blocked by CSP or an ad blocker.
  • Load the script before you call OysterskinWidget.create*.

The widget opens but is blank

  • Confirm your CSP allows frame-src for the widget origin.
  • Check browser console for iframe errors.
  • Confirm the key/user payload is valid for the environment.

Floating button is behind other UI

  • The launcher uses a very high z-index. If your site uses extreme z-index values, reduce them or adjust your stacking contexts.
  • Make sure no parent element creates a stacking context that clips fixed-position children unexpectedly.

Launch checklist

Before you ship
  • Keys: no keys committed; loaded at runtime from your config/secret system.
  • Callback: checkout/minimized/closed events are handled and logged in staging.
  • CSP: script-src and frame-src updated for the widget domains.
  • UX: inline mode opens from your CTA; float mode launcher placement doesn’t conflict with your chat/support widgets.
  • Mobile: verify the widget and launcher are reachable and not blocked by safe-area insets.