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
| Surface | Distribution | Isolation | Best for |
|---|---|---|---|
<stile-frame> | CDN (~7 KB launcher) or npm | iframe — verification UI runs on Stile's hosted page | Tight JS budgets, cross-origin isolation, no build step |
<stile-button> | npm (@stile/widget) | In-page Shadow DOM modal — no iframe | Bundler-based apps; instant-open modal via session prefetch |
verify() | npm (@stile/widget) | Shadow DOM modal | Promise-based control — open the flow from your own code |
create() | npm (@stile/widget) | Shadow DOM, mounts into your container | Fully 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/widgetThe 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:
| Mode | Attributes | When to use |
|---|---|---|
| Backend session (recommended) | session-url | Your 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 session | client-secret + session-id | You 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-id | The 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:
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
| Parameter | Type | Description |
|---|---|---|
session-url | string | Backend-session mode (recommended). URL the frame POSTs to for session creation. See the session-url contract above. |
client-secret | string | Pre-minted-session mode. The session's client secret. Pair with session-id. |
session-id | string | Pre-minted-session mode. The vks_... session ID. Pair with client-secret. |
publishable-key | string | Publishable-key (legacy) mode. Your stile_pk_ key. Pair with workflow-id. |
workflow-id | string | ID of the published workflow (wf_...). Required in session-url and publishable-key modes. |
methods | string | Pre-minted mode only: comma-separated methods from the session creation response, so the iframe renders the workflow's full step rail. |
age-tier | string | Pre-minted mode only: the age_tier from the session creation response. |
email | string | Pre-fills the user's email and powers returning-user lookup. |
jurisdiction | string | Override 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. |
label | string= "Verify" | Trigger button text (modal mode). |
verified-label | string= "Verified" | Trigger button text after successful verification (modal mode). |
min-height | number= 400 | Initial iframe height in px, before the first resize message. |
required | boolean | When present inside a <form>, blocks form submission until verified. |
form-field-name | string= "stile_session_id" | Name of the hidden input injected into the parent form after verification. |
confirmation-url | string | Your endpoint to poll for server-side confirmation. When set, stile:server-confirmed fires once your webhook has confirmed the session. |
Events
| Event | Detail | Description |
|---|---|---|
stile:verified | { sessionId, vpToken? } | Client-side success signal. Wait for the webhook (or stile:server-confirmed) before granting access. |
stile:server-confirmed | — | Fired after the poll against your confirmation-url reports the webhook landed. |
stile:error | { message: string } | Verification failed. |
stile:cancel | — | User 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
| Parameter | Type | Description |
|---|---|---|
session-url | string | Backend-session mode (recommended). URL the button POSTs to for session creation. Prefetched on mount for an instant click. |
client-secret | string | Pre-minted-session mode. Pair with session-id. |
session-id | string | Pre-minted-session mode. Pair with client-secret. |
publishable-key | string | Publishable-key (legacy) mode. Pair with workflow-id. |
workflow-id | string | ID of the published workflow (wf_...). Required in session-url and publishable-key modes. |
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. |
jurisdiction | string | Override IP-based jurisdiction detection (e.g. "US-OR"). |
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 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. |
confirmation-url | string | Your endpoint to poll for server-side confirmation after the client-side success signal. |
label | string= "Verify" | Button text. |
verified-label | string= "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, 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);| Parameter | Type | Description |
|---|---|---|
publishableKey | string | Your publishable key. The widget creates the session from the browser (legacy mode). |
workflowId | string | ID of the published workflow. Required when publishableKey is set — the workflow carries the use case, jurisdictions, and method preferences. |
clientSecret | string | Pre-created session mode: the client_secret from your backend. Pair with sessionId; publishableKey and workflowId are not needed. |
sessionId | string | Pre-created session mode: the vks_... session ID. Pair with clientSecret. |
methods | string[] | Pre-created session mode: pass the methods array from the session creation response so the widget renders the workflow's full step rail. |
ageTier | string | Pre-created session mode: the age_tier from the session creation response (e.g. "min_age_21"). |
email | string | The user's email address. |
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. 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
Quickstart
Go from zero to your first verified session in minutes.
Node SDK
Mint sessions behind your session-url endpoint and verify webhooks server-side.
Webhooks
Treat webhook delivery as the source of truth before granting access.
Integration guide
The full end-to-end flow: session creation, widget, webhook, unlock.