stile
SDKs

Widget SDK

Framework-agnostic web components and JavaScript API for embedding verification in any frontend.

The Stile widget is a drop-in verification UI for any frontend: two framework-agnostic web components plus a programmatic JavaScript API. Load <stile-frame> from the CDN with a single script tag, or install @stile/widget from npm for the full kit — both speak the same auth modes and emit the same events.

At a glance

SurfaceDistributionIsolationBest for
<stile-frame>CDN (~7 KB launcher) or npmiframe — verification UI runs on Stile's hosted pageTight JS budgets, cross-origin isolation, no build step
<stile-button>npm (@stile/widget)In-page Shadow DOM modal — no iframeBundler-based apps; instant-open modal via session prefetch
verify()npm (@stile/widget)Shadow DOM modalPromise-based control — open the flow from your own code
create()npm (@stile/widget)Shadow DOM, mounts into your containerFully custom UIs that manage the widget lifecycle themselves

Installation

<script src="https://js.stile.id/v1/stile.js"></script>

The CDN script is a ~7 KB (gzipped) launcher that registers the <stile-frame> web component. The verification UI itself runs inside an iframe on Stile's hosted page, so the heavy dependencies (camera, barcode scanning, face detection) never load in your page.

npm install @stile/widget

The npm package is the full kit for bundler-based apps: <stile-button> (in-page Shadow DOM verification), <stile-frame>, and the verify() / create() JavaScript APIs.

Choose an auth mode

Both components support the same three auth modes. Pick one:

The three widget auth modes: backend session via session-url (recommended), pre-minted session via client-secret and session-id, and the legacy publishable-key mode

ModeAttributesWhen to use
Backend session (recommended)session-urlYour backend mints the session with your secret key. The widget POSTs to your endpoint and opens the modal with the result. No publishable key needed in the page.
Pre-minted sessionclient-secret + session-idYou already created the session server-side (e.g. as part of an existing API call) and pass the result down to the page.
Publishable key (legacy)publishable-key + workflow-idThe widget creates the session directly from the browser. Fine for prototypes and sandbox testing; being phased out for production — responses carry a Stile-Deprecation header.

Every mode needs a workflow. Workflows are authored and published in the dashboard and carry the use case, target jurisdictions, verification methods, and preferences — compliance is resolved server-side from the workflow, so you never pass products or age tiers from the client.

The session-url contract

In backend-session mode, the widget sends your endpoint a POST with a JSON body and expects the fields from POST /v1/verification_sessions back:

app/api/start-verification/route.ts
import Stile from "@stile/node";

const stile = new Stile(process.env.STILE_API_KEY!);

export async function POST(req: Request) {
  // Widget sends: { workflowId?, email?, jurisdiction? }
  const { workflowId, email, jurisdiction } = await req.json();

  const session = await stile.verificationSessions.create({
    type: "age",
    workflow_id: workflowId ?? "wf_YOUR_WORKFLOW_ID",
    email,
    jurisdiction,
  });

  // Widget expects: session_id + client_secret (methods / age_tier
  // let it render the full step rail instead of a generic fallback)
  return Response.json({
    session_id: session.id,
    client_secret: session.client_secret,
    methods: session.methods,
    age_tier: session.age_tier,
  });
}

<stile-button> POSTs to session-url on mount in the background, so the session is usually ready before the user clicks — the modal opens instantly. If the email or workflow changes after prefetch, the stale session is discarded and re-minted at click time.

<stile-frame> — iframe embed

Available via the CDN script or npm. The verification UI runs on Stile's hosted page inside an iframe — your page only loads the ~7 KB launcher. Choose this when your JS budget is tight, your infosec team requires cross-origin isolation for third-party code, or you want camera permissions granted once to Stile's origin instead of per merchant.

<script src="https://js.stile.id/v1/stile.js"></script>

<!-- "modal" renders a trigger button that opens the iframe in an overlay -->
<stile-frame
  mode="modal"
  session-url="/api/start-verification"
  workflow-id="wf_YOUR_WORKFLOW_ID"
></stile-frame>

Attributes

ParameterTypeDescription
session-urlstringBackend-session mode (recommended). URL the frame POSTs to for session creation. See the session-url contract above.
client-secretstringPre-minted-session mode. The session's client secret. Pair with session-id.
session-idstringPre-minted-session mode. The vks_... session ID. Pair with client-secret.
publishable-keystringPublishable-key (legacy) mode. Your stile_pk_ key. Pair with workflow-id.
workflow-idstringID of the published workflow (wf_...). Required in session-url and publishable-key modes.
methodsstringPre-minted mode only: comma-separated methods from the session creation response, so the iframe renders the workflow's full step rail.
age-tierstringPre-minted mode only: the age_tier from the session creation response.
emailstringPre-fills the user's email and powers returning-user lookup.
jurisdictionstringOverride IP-based jurisdiction detection (e.g. "US-OR").
mode"inline" | "modal"= "inline"inline embeds the iframe in place; modal renders a trigger button that opens the iframe in a full-screen overlay.
labelstring= "Verify"Trigger button text (modal mode).
verified-labelstring= "Verified"Trigger button text after successful verification (modal mode).
min-heightnumber= 400Initial iframe height in px, before the first resize message.
requiredbooleanWhen present inside a <form>, blocks form submission until verified.
form-field-namestring= "stile_session_id"Name of the hidden input injected into the parent form after verification.
confirmation-urlstringYour endpoint to poll for server-side confirmation. When set, stile:server-confirmed fires once your webhook has confirmed the session.

Events

EventDetailDescription
stile:verified{ sessionId, vpToken? }Client-side success signal. Wait for the webhook (or stile:server-confirmed) before granting access.
stile:server-confirmedFired after the poll against your confirmation-url reports the webhook landed.
stile:error{ message: string }Verification failed.
stile:cancelUser closed the flow.

<stile-button> — in-page button

Available via npm (@stile/widget). Renders the verification UI directly in your page inside a Shadow DOM modal — no iframe. Same three auth modes and the same event names as <stile-frame>.

<stile-button
  session-url="/api/start-verification"
  workflow-id="wf_YOUR_WORKFLOW_ID"
  email-selector="#checkout-email"
></stile-button>

Attributes

ParameterTypeDescription
session-urlstringBackend-session mode (recommended). URL the button POSTs to for session creation. Prefetched on mount for an instant click.
client-secretstringPre-minted-session mode. Pair with session-id.
session-idstringPre-minted-session mode. Pair with client-secret.
publishable-keystringPublishable-key (legacy) mode. Pair with workflow-id.
workflow-idstringID of the published workflow (wf_...). Required in session-url and publishable-key modes.
email-selectorstringCSS selector for an email input on the page. The button reads its value automatically.
emailstringPass the email directly instead of using a selector.
jurisdictionstringOverride IP-based jurisdiction detection (e.g. "US-OR").
success-urlstringURL to redirect to after successful verification. The session ID is appended as a query parameter (e.g. "/done?session_id=vks_...").
cancel-urlstringURL to redirect to if the user cancels verification.
requiredbooleanWhen present inside a <form>, blocks form submission until verification completes. The button shakes if the user tries to submit early.
form-field-namestring= "stile_session_id"Name for the hidden input injected into the parent form after verification.
confirmation-urlstringYour endpoint to poll for server-side confirmation after the client-side success signal.
labelstring= "Verify"Button text.
verified-labelstring= "Verified"Text shown after successful verification.
disabledbooleanDisable the button.

Events

Listen for custom events on the <stile-button> element:

EventDetailDescription
stile:verifiedVerifyResultVerification succeeded.
stile:error{ message: string }Verification failed.
stile:cancelUser closed the modal.
const btn = document.querySelector("stile-button");

btn.addEventListener("stile:verified", (e) => {
  console.log("Verified!", e.detail);
  // e.detail.sessionId, etc.
});

btn.addEventListener("stile:error", (e) => {
  console.error("Failed:", e.detail.message);
});

btn.addEventListener("stile:cancel", () => {
  console.log("User cancelled");
});

Zero-JS redirect flow

For the simplest possible integration, use success-url and cancel-url to handle verification results without writing any JavaScript:

<stile-button
  session-url="/api/start-verification"
  workflow-id="wf_YOUR_WORKFLOW_ID"
  success-url="/checkout/complete"
  cancel-url="/cart"
></stile-button>

After verification, the user is redirected to /checkout/complete?session_id=vks_.... Your server should call GET /v1/verification_sessions/:id with your secret key to confirm the result before granting access.

Events (stile:verified, stile:cancel) still fire before the redirect, so you can combine redirect URLs with JavaScript event listeners if needed.

Form integration

When placed inside a <form>, the button automatically injects hidden inputs after verification:

<form action="/api/place-order" method="POST">
  <input name="email" type="email" />
  <input name="address" type="text" />

  <stile-button
    session-url="/api/start-verification"
    workflow-id="wf_YOUR_WORKFLOW_ID"
    email-selector="[name=email]"
    required
  ></stile-button>

  <button type="submit">Place Order</button>
</form>

After verification, the form will include these hidden fields automatically:

<input type="hidden" name="stile_session_id" value="vks_..." />
<input type="hidden" name="stile_verified" value="true" />

The required attribute prevents form submission until the user completes verification. Use form-field-name to customize the hidden input name if your backend expects a different field. <stile-frame> supports the same form gating.

verify() — JavaScript API

For more control, use the verify() function from the npm package. It opens a modal, handles the entire flow, and returns a promise.

import { verify } from "@stile/widget";

const result = await verify({
  publishableKey: "stile_pk_...",
  workflowId: "wf_YOUR_WORKFLOW_ID",
  email: "user@example.com",
});

console.log(result.sessionId);
ParameterTypeDescription
publishableKeystringYour publishable key. The widget creates the session from the browser (legacy mode).
workflowIdstringID of the published workflow. Required when publishableKey is set — the workflow carries the use case, jurisdictions, and method preferences.
clientSecretstringPre-created session mode: the client_secret from your backend. Pair with sessionId; publishableKey and workflowId are not needed.
sessionIdstringPre-created session mode: the vks_... session ID. Pair with clientSecret.
methodsstring[]Pre-created session mode: pass the methods array from the session creation response so the widget renders the workflow's full step rail.
ageTierstringPre-created session mode: the age_tier from the session creation response (e.g. "min_age_21").
emailstringThe user's email address.
jurisdictionstringOverride the auto-detected jurisdiction.

The promise rejects with a VerifyError if the user cancels (error.reason === "cancelled") or if verification fails.

create() — Low-level API

For fully custom UIs, use create() to manage the widget lifecycle yourself. Create the session on your backend, then mount the widget with the returned client_secret and session ID:

import { create } from "@stile/widget";

const widget = create({
  clientSecret: session.client_secret, // from your backend
  sessionId: session.id,
  methods: session.methods, // from the same response
  ageTier: session.age_tier,
  onSuccess: (result) => {
    console.log("Verified!", result);
  },
  onError: (error) => {
    console.error("Failed:", error.message);
  },
  onExpired: () => {
    console.log("Session expired");
  },
});

widget.mount("#verify-container");
// later: widget.destroy()

Framework examples

The web components work natively in any framework:

export function Checkout() {
  return (
    <stile-frame
      mode="modal"
      session-url="/api/start-verification"
      workflow-id="wf_YOUR_WORKFLOW_ID"
    />
  );
}
<template>
  <stile-frame
    mode="modal"
    session-url="/api/start-verification"
    workflow-id="wf_YOUR_WORKFLOW_ID"
  />
</template>
<stile-frame
  mode="modal"
  session-url="/api/start-verification"
  workflow-id="wf_YOUR_WORKFLOW_ID"
/>
<stile-frame
  mode="modal"
  session-url="/api/start-verification"
  workflow-id="wf_YOUR_WORKFLOW_ID"
></stile-frame>

Web components are supported in all modern browsers. The elements auto-register when the script loads — no setup required.

Next steps

On this page