Integration

Webhooks

Receive real-time event notifications from CAIRL using signed HTTP POST requests.

Overview

CAIRL sends outbound webhook events to your configured endpoint as HTTP POST requests. Each delivery is signed with HMAC-SHA256 so you can verify it came from CAIRL.


Setup

  1. Go to Dashboard → API Keys
  2. Find the key you want to configure
  3. Enter your HTTPS endpoint URL in the Webhook Configuration section
  4. Click Save — a signing secret is generated and shown once
  5. Store the secret securely in your environment variables

Live keys require an https:// endpoint. Test keys accept http://localhost for local development.


Verifying signatures

Every request includes an X-CAIRL-Signature header:

X-CAIRL-Signature: sha256=<64-char hex>

Verification (Node.js):

import crypto from 'crypto';

function verifyWebhookSignature(secret, rawBody, receivedSig) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // Timing-safe comparison
  if (expected.length !== receivedSig.length) return false;

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'utf8'),
    Buffer.from(receivedSig, 'utf8'),
  );
}

// Express handler
app.post('/webhooks/cairl', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-cairl-signature'];
  const secret = process.env.CAIRL_WEBHOOK_SECRET;

  if (!verifyWebhookSignature(secret, req.body, sig)) {
    return res.status(400).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  // Handle event...
  res.status(200).send('OK');
});

Important: Verify signatures against the raw request body before parsing JSON. Parsing first may alter whitespace and invalidate the signature.


Retry schedule

CAIRL retries failed deliveries (non-2xx response or network error) on an exponential backoff schedule:

AttemptDelay
1Immediate
2+1 minute
3+5 minutes
Marked failed

After 3 failed attempts, the delivery is marked failed and no further retries occur.

Make your endpoint idempotent. The same event_id may be delivered more than once — use event_id to deduplicate.


Event catalog

verification.session.completed

Fired when a user completes the CAIRL verification flow and an authorization code is issued.

{
  "event": "verification.session.completed",
  "event_id": "evt_018e...",
  "session_id": "hvf_abc...",
  "partner_id": "partner-uuid",
  "user_id": "user-uuid",
  "status": "complete",
  "scopes": ["age_18_plus", "identity_verified"],
  "completed_at": "2026-03-24T10:05:00.000Z"
}

verification.session.failed

Fired when a session ends in failure (user abandoned, document rejected, etc.).

{
  "event": "verification.session.failed",
  "event_id": "evt_018f...",
  "session_id": "hvf_xyz...",
  "partner_id": "partner-uuid",
  "failure_reason": "document_rejected",
  "failed_at": "2026-03-24T10:08:00.000Z"
}

verification.session.expired (coming soon)

This event is planned but not yet available. Sessions expire silently after 30 minutes; poll GET /api/verify/hvf-session/{id} to detect expiry, or handle the session_expired error on the redirect return. See the session docs.

enrollment.created

Fired the first time a user authenticates with your application (new enrollment). Not fired for returning users.

{
  "event": "enrollment.created",
  "event_id": "evt_019b...",
  "enrollment_id": "enrollment-uuid",
  "partner_id": "partner-uuid",
  "user_id": "user-uuid",
  "created_at": "2026-03-24T10:05:10.000Z"
}

vae.resolved

Fired on each Verified Access Event — when your application exchanges an authorization code for a token on a returning user. Billing is recorded at the same time.

{
  "event": "vae.resolved",
  "event_id": "evt_019c...",
  "partner_id": "partner-uuid",
  "user_id": "user-uuid",
  "claims": {
    "age_18_plus": true,
    "identity_verified": true
  },
  "resolved_at": "2026-03-24T11:00:00.000Z"
}

Delivery order

Webhook delivery is always after the billing write and user redirect:

  1. Billing event written to CAIRL database (atomic)
  2. User redirected to your redirect_uri
  3. Webhook delivered to your endpoint (async)

Webhook delivery failure never blocks user redirect or billing.


Secret rotation

Rotate your webhook secret from Dashboard → API Keys → Rotate Secret. The old secret is invalidated immediately — update your environment variable before rotating to avoid a gap in signature validation.


Responding to events

Return a 2xx status to acknowledge delivery. CAIRL does not inspect the response body.

If your handler needs more time than a typical request timeout, acknowledge the event immediately with 200 OK and process it asynchronously.

app.post('/webhooks/cairl', express.raw({ type: 'application/json' }), (req, res) => {
  // Verify first, acknowledge immediately
  if (!verifyWebhookSignature(secret, req.body, req.headers['x-cairl-signature'])) {
    return res.status(400).send('Invalid signature');
  }

  res.status(200).send('OK'); // Acknowledge before processing

  // Process asynchronously
  setImmediate(() => processEvent(JSON.parse(req.body)));
});

On this page