Drop-in Checkbox (in development)
Design preview of the CAIRL Checkbox — a privacy-first age and identity verification widget. The script is not yet served; use the OAuth quickstart to integrate today.
⚠️ Not yet available — this is a design preview
https://cairl.app/v1/checkbox.jsis not served yet. The snippets on this page describe the intended integration contract, not a working one: pasting them into a page today loads nothing.Two gate-removal blockers remain — partner-backend-owned PKCE, and the hosted-modal completion emitter — so redirect mode cannot retrieve claims and iframe mode never resolves.
pk_liveissuance is also not yet activated.To integrate identity verification today, use the OAuth quickstart — it is live and supported.
This page stays published so the contract can be reviewed before it ships. It becomes a working guide when the widget is served.
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 do not receive raw identity data. You exchange the code for boolean claims (e.g.
claims.age_21_plus: true) under a pseudonymous, site-scopedsub. 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 browser testing. Live keys (pk_live_*) activate at launch. |
| Registered redirect URI | Registered on /home/f/{slug}/connect. The resolver only accepts registered callback URLs. |
| Sandbox (optional) | If /home/developer/sandbox is available in your environment, it renders a synthetic run with no key. |
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.
Where available, sandbox recipes render the checkbox experience and synthetic
result before you create a pk_* key. They do not create a billable Verified
Access Event, balance debit, enrollment, or production claim.
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 -->
<!-- NOT YET SERVED — this URL 404s today. See the notice at the top of this page. -->
<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 CAIRL microbrand attribution. |
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, site-scoped identifier — not a global user id.
// The claims a key may request are bound to the key (scope `age:21+`
// returns `claims.age_21_plus`).
if (claims.age_21_plus === 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>
<!-- NOT YET SERVED — this URL 404s today. See the notice at the top of this page. -->
<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.)
Confirm on your server. The
cairl:completeevent is a convenient UX signal — it carries no proof and must not 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 widget's global (part of the same not-yet-served script):
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 do not call a real
biometric provider, create a user, or 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).
- Add funds at
/home/f/{slug}/billing— $50 minimum load. - Issue live API keys on
/home/f/{slug}/keysand swappk_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 site-scoped pseudonym designed to reduce cross-site linkability; it is not the user's global CAIRL identifier and must still be treated as personal data. - Raw material stays out of the result. The Checkbox response does not send identity documents or biometric captures to your application. CAIRL handles source material under its applicable retention and deletion controls.
- Hosted capture boundary. The CAIRL-hosted flow returns a derived result rather than biometric bytes. Your business remains responsible for its integration choices and applicable privacy and biometric-law obligations.
- You avoid holding raw identity material. You receive derived booleans rather than raw documents, names, or biometric material. Those assertions are still personal data and must be protected and retained appropriately.
See the live demo to watch the widget run, and Errors for the full error-code reference.