Integration

Get a Verified User in 10 Minutes

Integrate CAIRL identity verification into your application using OAuth 2.0 with PKCE.

What you'll build

CAIRL uses the OAuth 2.0 Authorization Code flow with PKCE. If you've integrated Stripe or Plaid, this will feel familiar.

By the end of this guide, your application will:

  1. Redirect a user to CAIRL for identity verification
  2. Receive a callback with an authorization code
  3. Exchange that code for an access token
  4. Pull verified claims (e.g., age_18_plus) from the token

You never receive raw identity data. CAIRL returns only verified claims (e.g., age_18_plus: true). You do not store documents, names, or PII. No data liability, no breach exposure.


Prerequisites

Before you start, you need:

ItemHow to get it
client_idProvided by CAIRL when your account is activated
client_secretProvided alongside client_id — store it server-side only, never expose to browsers
Registered callback URLYour redirect URI, pre-registered with CAIRL. During beta, contact your CAIRL onboarding contact to register it.
Funded walletYour CAIRL wallet must have a balance before live verification events are billed. Minimum load: $50. You can build and test the full integration flow without a funded wallet — token exchange will return 402 Payment Required until funded.

Step 1 — Redirect the user to CAIRL

When a user needs to be verified, redirect them to CAIRL's hosted verification flow.

Copy-paste canonical URL:

https://cairl.app/verify/start?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/auth/cairl/callback&state=YOUR_STATE&scope=age_18_plus&code_challenge=YOUR_CODE_CHALLENGE&code_challenge_method=S256

Most integrations start with scope=age_18_plus. Add additional claims only when your use case requires them.

Full parameter reference:

ParameterRequiredDescription
client_idYesYour CAIRL client identifier
redirect_uriYesMust exactly match your registered callback URL
stateYesRandom string you generate. Minimum 16 characters. Returned to you unchanged — use it to verify the callback is genuine (CSRF protection).
scopeYesSpace-delimited list of claims you need. See claim reference below.
code_challengeYesPKCE S256 challenge derived from your code_verifier
code_challenge_methodYesMust be S256

Generate PKCE values (Node.js):

import crypto from 'crypto';

// Generate once per authorization request, store server-side
const codeVerifier = crypto.randomBytes(32).toString('base64url');

const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

Generate state:

const state = crypto.randomBytes(16).toString('base64url');
// Store in session: req.session.cairl_state = state
// Store verifier in session: req.session.cairl_code_verifier = codeVerifier

Full redirect (Node.js / Express):

app.get('/auth/cairl', (req, res) => {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');
  const state = crypto.randomBytes(16).toString('base64url');

  req.session.cairl_code_verifier = codeVerifier;
  req.session.cairl_state = state;

  const params = new URLSearchParams({
    client_id: process.env.CAIRL_CLIENT_ID,
    redirect_uri: 'https://yourapp.com/auth/cairl/callback',
    state,
    scope: 'age_18_plus',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });

  res.redirect(`https://cairl.app/verify/start?${params}`);
});

What CAIRL does next

CAIRL owns the entire user experience after the redirect:

  • Account creation or login (inline — no separate signup page)
  • Email verification (6-digit code, entered inline)
  • Identity verification (document upload + face match)
  • Consent screen showing your app name and requested claims

If the user has already verified with CAIRL, identity verification is skipped automatically.

First-time users: ~3–5 minutes. Returning verified users: ~30 seconds.


Step 2 — Handle the callback

After verification, CAIRL redirects the user back to your redirect_uri:

https://yourapp.com/auth/cairl/callback?code=AUTH_CODE&state=YOUR_STATE_VALUE

Validate the state first. If state does not match what you stored in Step 1, reject the request — it may be a CSRF attempt.

app.get('/auth/cairl/callback', async (req, res) => {
  const { code, state, error } = req.query;

  // Handle user denial or verification failure
  if (error) {
    return res.redirect('/verification-failed');
  }

  // Validate state
  if (state !== req.session.cairl_state) {
    return res.status(400).send('Invalid state');
  }

  // Proceed to Step 3
  const token = await exchangeCode(code, req.session.cairl_code_verifier);
  // ...
});

Error callbacks — if verification fails or the user cancels, CAIRL redirects with an error parameter instead of code:

error valueMeaning
access_deniedUser declined consent
verification_failedUser abandoned verification
session_expiredUser took too long (30 min limit)

Retry behavior: If verification fails or the user cancels, restart the flow by redirecting to /verify/start with a fresh state and new PKCE values. Nothing is broken — the user simply starts a new session.


Step 3 — Exchange the code for a token

Exchange the authorization code for an access token. This is a server-to-server request — never make it from the browser.

cURL:

curl -X POST https://cairl.app/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "redirect_uri=https://yourapp.com/auth/cairl/callback" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

Node.js:

async function exchangeCode(code, codeVerifier) {
  const response = 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,
      client_id: process.env.CAIRL_CLIENT_ID,
      client_secret: process.env.CAIRL_CLIENT_SECRET,
      redirect_uri: 'https://yourapp.com/auth/cairl/callback',
      code_verifier: codeVerifier,
    }),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(err.error_description ?? err.error);
  }

  return response.json();
}

Response:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Billing note: Your wallet is charged at this step, not during verification. You are charged only when the user completes verification and you successfully exchange the authorization code. Abandoned flows, failed verifications, and failed token exchanges are not billed. If your wallet balance is insufficient, the token exchange returns 402 Payment Required and no token is issued.


Step 4 — Pull verified claims

Use the access token to retrieve the verified claims you requested.

cURL:

curl https://cairl.app/api/oauth/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"

Node.js:

async function getVerifiedClaims(accessToken) {
  const response = await fetch('https://cairl.app/api/oauth/userinfo', {
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  if (!response.ok) throw new Error('Failed to retrieve claims');
  return response.json();
}

Response:

{
  "sub": "user_abc123",
  "age_18_plus": true,
  "identity_verified": true
}

sub is a stable, per-partner pseudonymous user identifier. It is the same for returning users and different across partners — your application cannot use it to track a user across other CAIRL-integrated platforms.


Available claims

ClaimDescriptionRisk level
age_18_plusUser is 18 or olderLow
age_21_plusUser is 21 or olderLow
identity_verifiedUser has completed identity verificationLow
unique_per_partnerUser has no other account with your applicationLow
document_activeUser's ID document is not expiredLow
freshness_currentUser meets your configured freshness windowLow
name_matchSubmitted name matches verified nameMedium
dob_matchSubmitted date of birth matches verified recordMedium
address_matchSubmitted address matches verified recordMedium

Request only the claims your application needs. Users see exactly what you request on the consent screen.


Billing reference

EventPrice
Enrollment (first verification per user per partner)$0.50
Authorization event (subsequent claim checks)$0.05
Additional claims beyond 5 included$0.01 each

Charges are deducted from your prepaid wallet at token exchange. Wallet minimum load: $50. If your wallet balance is insufficient, the token exchange returns 402 Payment Required and no token is issued.


Common errors

ErrorHTTP statusCauseFix
invalid_client401Wrong client_id or client_secretCheck credentials
invalid_grant400Code expired, already used, or code_verifier mismatchGenerate a new authorization request
invalid_redirect_uri400redirect_uri doesn't match registered valueUse exact registered URL — no trailing slash differences
invalid_request400Missing or malformed parameterCheck all required fields are present
invalid_scope400Requested scope not authorized for your clientContact CAIRL to enable additional claims
payment_required402Wallet balance insufficientFund your wallet at cairl.app/home/billing
client_inactive403Your account is not yet activatedContact CAIRL to complete activation
access_denied— (redirect)User declined consentPresent user with option to try again

Security checklist

Before going live, confirm:

  • client_secret is stored server-side only — never in browser code, mobile apps, or public repos
  • state is validated on every callback before processing the code
  • code_verifier is generated fresh for every authorization request
  • Token exchange happens server-to-server, not from the client
  • redirect_uri in token exchange exactly matches the value used in the authorization request

Next steps

  • Add more claims — request identity_verified or age_21_plus by updating your scope parameter
  • Handle returning users — users who have already verified with CAIRL skip re-verification automatically; you pay only the authorization event rate ($0.05) on subsequent checks
  • Configure freshness — contact CAIRL to set a verification freshness window for your application (e.g., require re-verification every 90 days for high-trust actions)
  • OAH-001 — self-serve callback URL management is coming; for now, contact your onboarding contact to add or update registered URIs

Full working example

If you want to copy-paste a working integration, start here. This is a complete Express.js server that handles all four steps.

import express from 'express';
import crypto from 'crypto';
import session from 'express-session';

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));

// Step 1: Redirect to CAIRL
app.get('/auth/cairl', (req, res) => {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
  const state = crypto.randomBytes(16).toString('base64url');

  req.session.cairl_code_verifier = codeVerifier;
  req.session.cairl_state = state;

  const params = new URLSearchParams({
    client_id: process.env.CAIRL_CLIENT_ID,
    redirect_uri: 'https://yourapp.com/auth/cairl/callback',
    state,
    scope: 'age_18_plus identity_verified',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });

  res.redirect(`https://cairl.app/verify/start?${params}`);
});

// Step 2 + 3 + 4: Handle callback, exchange code, pull claims
app.get('/auth/cairl/callback', async (req, res) => {
  const { code, state, error } = req.query;

  if (error) return res.redirect('/verification-failed');
  if (state !== req.session.cairl_state) return res.status(400).send('Invalid state');

  // Step 3: Exchange code
  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,
      client_id: process.env.CAIRL_CLIENT_ID,
      client_secret: process.env.CAIRL_CLIENT_SECRET,
      redirect_uri: 'https://yourapp.com/auth/cairl/callback',
      code_verifier: req.session.cairl_code_verifier,
    }),
  });

  if (!tokenRes.ok) return res.redirect('/verification-failed');
  const { access_token } = await tokenRes.json();

  // Step 4: Pull claims
  const claimsRes = await fetch('https://cairl.app/api/oauth/userinfo', {
    headers: { Authorization: `Bearer ${access_token}` },
  });

  const claims = await claimsRes.json();

  // claims.age_18_plus === true — user is verified and of age
  if (claims.age_18_plus) {
    req.session.verified = true;
    return res.redirect('/dashboard');
  }

  res.redirect('/verification-failed');
});

app.listen(3000);

On this page