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/widgetQuick 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
| Parameter | Type | Description |
|---|---|---|
publishable-keyrequired | string | Your publishable key (pk_test_ or pk_live_). |
email-selector | string | CSS selector for an email input on the page. The button reads its value automatically. |
email | string | Pass the email directly instead of using a selector. |
products | string | Comma-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-tier | string | Internal. Automatically resolved from products via the Compliance API. Do not set manually. |
success-url | string | URL to redirect to after successful verification. The session ID is appended as a query parameter (e.g. "/done?session_id=vks_..."). |
cancel-url | string | URL to redirect to if the user cancels verification. |
required | boolean | When 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-name | string= "stile_session_id" | Name for the hidden input injected into the parent form after verification. |
label | string= "Verify Age" | Button text. |
verified-label | string= "Age Verified" | Text shown after successful verification. |
disabled | boolean | Disable the button. |
Events
Listen for custom events on the <stile-button> element:
| Event | Detail | Description |
|---|---|---|
stile:verified | VerifyResult | Verification succeeded. |
stile:error | { message: string } | Verification failed. |
stile:cancel | — | User 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);| Parameter | Type | Description |
|---|---|---|
publishableKeyrequired | string | Your publishable key. |
emailrequired | string | The user's email address. |
products | string[] | 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"]. |
ageTier | string | Explicit age tier override. Ignored when products is set. |
jurisdiction | string | Override 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.