Drop-in Checkbox in 15 Minutes
Add a privacy-first age and identity verification checkbox to any website with one script tag. Redirect and iframe modes, no account, no stored ID.
What you'll build
The CAIRL Checkbox is a copy-paste age and identity verification widget for any website — WordPress, Shopify, or plain HTML. No npm, no React, no account.
By the end of this guide your page will:
- Render a verification checkbox where you drop a
<div>. - Hand the visitor to CAIRL when they tap it (full redirect, or an in-page iframe overlay).
- Receive an OAuth authorization code on your callback and exchange it, server-side, for boolean claims.
You never receive raw identity data. You exchange the code for boolean claims (e.g.
age.over_21.v1.estimate: true) under a pseudonymous, per-partnersub. No name, no document, no selfie — the document and the biometrics stay on CAIRL’s side of the consent boundary.
Prerequisites
| Item | How to get it |
|---|---|
Publishable key (pk_*) | A pk_sandbox_* key for testing — safe to put in browser code. Live keys (pk_live_*) activate at launch. |
| Registered redirect URI | Your callback URL, pre-registered on your CAIRL account. The resolver only accepts registered origins. |
The publishable key is browser-safe by design — it carries no policy of its
own and resolves server-side to your account's claim authorization. It is the
only credential the snippet needs. Never put a cairl_* secret key in a page.
Step 1 — Add the checkbox (redirect mode)
Drop these two lines anywhere on your page. Swap pk_sandbox_your_key_here for
your key and data-cairl-redirect for your callback URL.
<!-- 1. The checkbox renders here -->
<div
data-cairl-checkbox
data-cairl-key="pk_sandbox_your_key_here"
data-cairl-redirect="https://yourstore.example/age-callback"
data-cairl-profile="age-gate-21"
data-cairl-mode="redirect"
></div>
<!-- 2. Load the widget once -->
<script src="https://cairl.app/v1/checkbox.js" async></script>That's the whole front-end. The script finds every [data-cairl-checkbox]
element, renders the checkbox, and starts the flow on click.
Attributes
| Attribute | Required | Description |
|---|---|---|
data-cairl-checkbox | yes | Marker — its presence opts the element in. |
data-cairl-key | yes | Your publishable key (pk_sandbox_* / pk_live_*). |
data-cairl-redirect | yes | Your callback URL. Must be a registered redirect URI. |
data-cairl-profile | no | Copy hint, e.g. age-gate-21, age-gate-18, trust-gate-standard, liveness-only. The actual claims are bound to your key, not chosen in the browser. |
data-cairl-mode | no | redirect (default) or iframe. |
data-cairl-trust-badge | no | false to hide the Powered by CAIRL mark. |
Step 2 — Handle the callback
When the flow completes, CAIRL redirects the visitor back to your
data-cairl-redirect URL with an OAuth 2.0 authorization code and your
state — exactly the Authorization Code + PKCE callback CAIRL's hosted flow
uses everywhere:
https://yourstore.example/age-callback?code=<code>&state=<state>There is no token in the URL. The code is a short-lived, single-use handle;
you exchange it server-to-server for the verified claims. Do this on your
backend — the client_secret and PKCE code_verifier never touch the
browser.
PKCE is your backend's job. Before the flow starts, your backend mints a PKCE pair (a random
code_verifierand its S256code_challenge) and a randomstate, and keeps theverifierserver-side keyed to thatstate. The challenge is bound to the authorization request; the verifier proves, at exchange time, that the same backend that started the flow is finishing it.
// 1. Validate state (CSRF) against the value you stored when the flow started,
// then look up the matching PKCE code_verifier for this state.
export async function handleAgeCallback(req, res) {
const { code, state } = req.query;
const pending = await consumePendingFlow(state); // your store; null if unknown
if (!pending) {
return res.status(400).send("Invalid or expired state");
}
// 2. Exchange the code (+ the PKCE verifier) for an access token.
const tokenRes = await fetch("https://cairl.app/api/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://yourstore.example/age-callback", // must match exactly
client_id: process.env.CAIRL_CLIENT_ID,
client_secret: process.env.CAIRL_CLIENT_SECRET, // server-side only
code_verifier: pending.codeVerifier,
}),
});
if (!tokenRes.ok) {
return res.status(400).send("Token exchange failed");
}
const { access_token } = await tokenRes.json();
// 3. Read the verified claims from userinfo with the access token.
const claimsRes = await fetch("https://cairl.app/api/oauth/userinfo", {
headers: { Authorization: `Bearer ${access_token}` },
});
const { sub, claims } = await claimsRes.json();
// sub is a pseudonymous, per-partner identifier — not a global user id.
if (claims["age.over_21.v1.estimate"] === true) {
// allow
} else {
// block or offer an alternative
}
}Claim values are true (passed), false (failed), or null (inconclusive —
the visitor could retry, you decide the fallback). The authorization code is
single-use and short-lived, so the exchange itself is the enforcement point;
see OAuth and OIDC for the full token and
userinfo contract and Verification sessions
for session introspection.
Step 3 (optional) — Iframe mode
Iframe mode keeps the visitor on your page: the verification opens in a CAIRL-hosted overlay. Your page receives the outcome via DOM events instead of a redirect.
<div
data-cairl-checkbox
data-cairl-key="pk_sandbox_your_key_here"
data-cairl-redirect="https://yourstore.example/age-callback"
data-cairl-profile="age-gate-21"
data-cairl-mode="iframe"
></div>
<script src="https://cairl.app/v1/checkbox.js" async></script>
<script>
document.addEventListener("cairl:complete", function (e) {
// e.detail = { canonicalClaimIds, sessionRef, modeType, profileLabel }
console.log("verified", e.detail);
});
document.addEventListener("cairl:error", function (e) {
console.warn("verification error", e.detail);
});
</script>The overlay is a sandboxed, CAIRL-origin iframe. CAIRL still owns the consent
and capture surface end-to-end — the overlay cannot be re-skinned to obscure
the consent disclosures, and the result channel is bound per-launch (message
source + a per-launch nonce + state, with version/surface/type discriminators)
so a result can't be forged or replayed from the host page. (Origin is
intentionally not part of the binding: a sandboxed frame presents an opaque
origin, so the parent gates on event.source reference-equality plus the
nonce/state echo instead.)
Always confirm on your server. The
cairl:completeevent is a convenient UX signal — it carries no proof and must never gate access on its own. Confirm server-side before granting access: either the code exchange (Step 2) or a webhook keyed to the session is the source of truth.
Programmatic API
If you render checkboxes dynamically, skip the auto-scan and call the SDK:
window.Cairl.checkbox({
el: document.querySelector("#age-gate"),
key: "pk_sandbox_your_key_here",
redirect: "https://yourstore.example/age-callback",
mode: "iframe",
profile: "age-gate-21",
onComplete: (detail) => console.log("verified", detail),
onError: (detail) => console.warn(detail),
});
// Re-scan after injecting new [data-cairl-checkbox] nodes:
window.Cairl.mount();Sandbox → live
pk_sandbox_* keys return deterministic fixtures — they never call a real
biometric provider, never create a user, and never bill. They are safe in
public docs, demos, and automated tests. Build and ship your entire integration
against sandbox with zero human in the loop.
Live publishable keys (pk_live_*) issue when production verification opens.
The integration contract does not change — you swap the key prefix and your
checkbox is live.
Go live self-serve
- Create a business facet (no sales call).
- Fund your prepaid wallet — $50 minimum reload.
- Issue live API keys and swap
pk_sandbox_*forpk_live_*.
See metered pricing for per-check costs (~$0.10 typical age check — a fraction of typical document-upload identity verification).
Privacy & compliance notes
- Pseudonymous by construction. The
subyou receive is a per-partner pseudonym — it cannot be correlated to the same person across other partners, and it is never the user's global CAIRL identifier. - No stored ID. Biometric capture is processed in memory and purged; CAIRL persists only non-reconstructible audit records, never images or video.
- CAIRL is the sole collector. Your site never handles biometric data, which keeps a single entity responsible under BIPA and state biometric law.
- You hold no PII. You receive booleans, so there is nothing to breach.
See the live demo to watch the widget run, and Errors for the full error-code reference.