stile
SDKs

Widget SDK

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

Installation

<script src="https://cdn.stile.dev/v1/stile.js"></script>
npm install @stile/widget

Quick start: <stile-button>

The simplest integration — a drop-in web component that handles the entire verification flow. Works with any framework or plain HTML.

<stile-button
  publishable-key="pk_test_..."
  email-selector="#checkout-email"
  products="alcohol_delivery"
></stile-button>

Attributes

ParameterTypeDescription
publishable-keyrequiredstringYour publishable key (pk_test_ or pk_live_).
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.
productsstringComma-separated product types (e.g. "alcohol_delivery,tobacco_nicotine"). The widget automatically calls the Compliance API to resolve the required age tier and check for prohibited products — no server code needed.
age-tierstringInternal. Automatically resolved from products via the Compliance API. Do not set manually.
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 to draw attention 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.
labelstring= "Verify Age"Button text.
verified-labelstring= "Age 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, e.detail.status, 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
  publishable-key="pk_live_..."
  products="alcohol_delivery"
  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
    publishable-key="pk_live_..."
    email-selector="[name=email]"
    products="alcohol_delivery"
    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. If the user clicks the submit button before verifying, the button shakes to draw their attention.

Use form-field-name to customize the hidden input name if your backend expects a different field.

verify() — JavaScript API

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

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

const result = await verify({
  publishableKey: "pk_test_...",
  email: "user@example.com",
  products: ["alcohol_delivery"],
});

console.log(result.sessionId);
ParameterTypeDescription
publishableKeyrequiredstringYour publishable key.
emailrequiredstringThe user's email address.
productsstring[]Product types being sold. The SDK calls the Compliance API automatically to resolve the required age tier, check for prohibited products, and filter verification methods by jurisdiction. E.g. ["alcohol_delivery", "tobacco_nicotine"].
ageTierstringExplicit age tier override. Ignored when products is set.
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. This is useful when you create sessions on your own backend and pass the client_secret to the frontend.

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

const widget = create({
  clientSecret: session.client_secret,  // from your backend
  container: document.getElementById("verify-container"),
  onComplete: (session) => {
    console.log("Verified!", session.id);
  },
  onError: (error) => {
    console.error("Failed:", error.message);
  },
  onCancel: () => {
    console.log("User cancelled");
  },
});

Framework examples

The web component works natively in any framework:

export function Checkout() {
  return (
    <stile-button
      publishable-key="pk_test_..."
      email="user@example.com"
    />
  );
}
<template>
  <stile-button
    publishable-key="pk_test_..."
    :email="userEmail"
  />
</template>
<stile-button
  publishable-key="pk_test_..."
  email={userEmail}
/>
<stile-button
  publishable-key="pk_test_..."
  email-selector="#email"
></stile-button>

Web components are supported in all modern browsers. The <stile-button> element auto-registers when the script loads — no setup required.

On this page