Quickstart
Get age verification working in your app in under 5 minutes.
Prerequisites
You need a stile account and a publishable key. Get both from the dashboard.
1. Add the widget
Include the stile widget on your page. It's a standard web component — no build step required.
<script src="https://cdn.stile.dev/v1/stile.js"></script>npm install @stile/widget2. Drop in the button
Add <stile-button> to your page with your publishable key and the products you sell. The widget handles everything — compliance checks, session creation, the verification modal, and result callbacks.
<stile-button
publishable-key="pk_test_..."
email-selector="#email"
products="alcohol_delivery"
></stile-button>The products attribute tells stile what you're selling. The widget automatically:
- Calls the Compliance API to determine the required age tier for the user's jurisdiction
- Filters verification methods to only those allowed in that jurisdiction
- Creates a verification session with the correct settings
- Opens the verification modal
export function Checkout() {
const handleVerified = (e: CustomEvent) => {
console.log("Verified!", e.detail);
};
return (
<div>
<input id="email" type="email" placeholder="you@example.com" />
<stile-button
publishable-key="pk_test_..."
email-selector="#email"
products="alcohol_delivery"
ref={(el) => el?.addEventListener("stile:verified", handleVerified)}
/>
</div>
);
}<script src="https://cdn.stile.dev/v1/stile.js"></script>
<input id="email" type="email" placeholder="you@example.com" />
<stile-button
publishable-key="pk_test_..."
email-selector="#email"
products="alcohol_delivery"
></stile-button>
<script>
document.querySelector("stile-button")
.addEventListener("stile:verified", (e) => {
console.log("Verified!", e.detail);
});
</script>No JavaScript required. The button blocks form submission until verified, then injects a hidden stile_session_id field automatically.
<script src="https://cdn.stile.dev/v1/stile.js"></script>
<form action="/api/place-order" method="POST">
<input name="email" type="email" placeholder="you@example.com" />
<stile-button
publishable-key="pk_test_..."
email-selector="[name=email]"
products="alcohol_delivery"
required
></stile-button>
<button type="submit">Place Order</button>
</form>Your server receives stile_session_id and stile_verified in the form body. Call GET /v1/verification_sessions/:id with your secret key to confirm the result.
import { verify } from "@stile/widget";
const result = await verify({
publishableKey: "pk_test_...",
email: "user@example.com",
products: ["alcohol_delivery"],
});
console.log("Verified!", result.sessionId);Available product types
| Product | Value |
|---|---|
| Alcohol delivery | alcohol_delivery |
| Alcohol in-person | alcohol_in_person |
| Cannabis dispensary | cannabis_dispensary |
| Cannabis delivery | cannabis_delivery |
| Tobacco / nicotine | tobacco_nicotine |
| Gambling (online) | gambling_online |
| Gambling (in-person) | gambling_in_person |
| Adult content | adult_content |
| Social media | social_media |
| Firearms | firearms |
For mixed carts, pass multiple products: products="alcohol_delivery,tobacco_nicotine". The widget resolves the most restrictive age tier automatically.
3. Confirm verification on your server (required)
The widget fires a client-side event when verification completes, but client-side events can be spoofed. You must set up a server-side webhook to confirm results before granting access.
Step 1: Go to Webhooks in the dashboard and add your server's URL. Save the webhook secret.
Step 2: Create a webhook handler that verifies the signature and processes the event:
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.STILE_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const rawBody = await req.text();
const sig = req.headers.get("stile-signature") ?? "";
// Parse t= and v1= from the header
const parts = sig.split(",");
const ts = parts.find((p) => p.startsWith("t="))?.slice(2);
const v1 = parts.find((p) => p.startsWith("v1="))?.slice(3);
if (!ts || !v1) return new Response("Bad signature", { status: 400 });
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300)
return new Response("Expired", { status: 400 });
const expected = createHmac("sha256", SECRET)
.update(`${ts}.${rawBody}`)
.digest("hex");
if (!timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex")))
return new Response("Invalid", { status: 400 });
const event = JSON.parse(rawBody);
if (event.type === "verification_session.verified") {
// Grant access, update your database, etc.
}
return Response.json({ received: true });
}import hmac, hashlib, time, json, os
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = os.environ["STILE_WEBHOOK_SECRET"]
@app.route("/webhooks", methods=["POST"])
def handle_webhook():
raw_body = request.get_data(as_text=True)
sig = request.headers.get("stile-signature", "")
parts = dict(p.split("=", 1) for p in sig.split(",") if "=" in p)
ts, v1 = parts.get("t"), parts.get("v1")
if not ts or not v1:
return "Bad signature", 400
if abs(time.time() - int(ts)) > 300:
return "Expired", 400
expected = hmac.new(SECRET.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(v1, expected):
return "Invalid", 400
event = json.loads(raw_body)
if event["type"] == "verification_session.verified":
pass # Grant access
return jsonify(received=True)npm install @stile/nodeimport Stile from "@stile/node";
const stile = new Stile(process.env.STILE_API_KEY!);
export async function POST(req: Request) {
const event = await stile.webhooks.fromRequest(
req,
process.env.STILE_WEBHOOK_SECRET!,
);
if (event.type === "verification_session.verified") {
// Grant access, update your database, etc.
}
return Response.json({ received: true });
}That's it — you have age verification working. The widget handles compliance and the verification flow, your server confirms the result via signed webhook. See the full verification guide for Go, Ruby, PHP, and more.
Two different keys
Your publishable key (pk_test_ / pk_live_) goes in the frontend widget. Your secret API key (vk_test_ / vk_live_) stays on your server. See Authentication for details.
Webhooks are required in live mode
Live mode keys (pk_live_ / vk_live_) require at least one active webhook endpoint before sessions can be created. Test keys work without webhooks for development. This ensures verification results are always confirmed server-side in production.
Next steps
- Integration Guide — handling returning users, prohibited products, mixed carts, and VPN detection
- Widget SDK — all attributes, events, and the JavaScript API
- HTTP API — direct API access from any language
- Webhook Verification — copy-paste handlers for Node.js, Python, Go, Ruby, and PHP
- Node.js SDK — typed convenience wrapper for JS/TS developers