CAIRLDocs
Integration

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:

  1. Render a verification checkbox where you drop a <div>.
  2. Hand the visitor to CAIRL when they tap it (full redirect, or an in-page iframe overlay).
  3. 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-partner sub. No name, no document, no selfie — the document and the biometrics stay on CAIRL’s side of the consent boundary.


Prerequisites

ItemHow 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 URIYour 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

AttributeRequiredDescription
data-cairl-checkboxyesMarker — its presence opts the element in.
data-cairl-keyyesYour publishable key (pk_sandbox_* / pk_live_*).
data-cairl-redirectyesYour callback URL. Must be a registered redirect URI.
data-cairl-profilenoCopy 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-modenoredirect (default) or iframe.
data-cairl-trust-badgenofalse 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_verifier and its S256 code_challenge) and a random state, and keeps the verifier server-side keyed to that state. 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:complete event 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

  1. Create a business facet (no sales call).
  2. Fund your prepaid wallet — $50 minimum reload.
  3. Issue live API keys and swap pk_sandbox_* for pk_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 sub you 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.

On this page