← Back to Documentation

Cardless ID Age Verification - Integration Guide

Complete guide to integrating secure, privacy-preserving age verification into your application

Overview

Cardless ID provides zero-knowledge age verification using decentralized identity credentials on the Algorand blockchain. Users prove they meet an age requirement without revealing their actual birthdate.

Key Features

  • Privacy-preserving - Only returns true/false
  • Secure challenge-response flow - Prevents tampering
  • Single-use verification tokens - Cannot be replayed
  • 10-minute expiration - Time-limited challenges
  • Optional webhook callbacks - Real-time notifications

Security Model

The verification flow uses a challenge-response pattern to prevent tampering:

  1. Your backend creates a challenge with your required age
  2. Cardless ID generates a unique, single-use challenge ID
  3. User scans QR code with their wallet
  4. Wallet verifies age requirement and responds to Cardless ID
  5. Your backend polls or receives webhook to confirm verification
  6. Challenge cannot be reused or modified

Why This Is Secure

  • • Challenge ID is cryptographically tied to your minAge requirement
  • • User cannot modify the age requirement (it's stored server-side)
  • • Challenge is single-use and expires after 10 minutes
  • • Only you (with your API key) can verify the challenge result

Credential Verification & Issuer Registry

When verifying a user's credential, the Cardless ID system checks that the credential was issued by a trusted source. An Algorand smart contract maintains a registry of approved issuer addresses. Only credentials from registered issuers are considered valid.

This ensures that all credentials in the ecosystem meet Cardless ID's security and verification standards. If you're building your own verification system, see the Custom Verification Provider Guide for information about the registry approval process.

Getting Started

1. Get Your API Key

Contact Cardless ID to receive your API key for production use.

2. Install the SDK

npm install @cardlessid/verifier

3. Basic Usage

const Cardless ID = require('@cardlessid/verifier');

const verifier = new CardlessID({
  apiKey: process.env.CARDLESSID_API_KEY
});

// Create challenge
const challenge = await verifier.createChallenge({ minAge: 21 });

// Show QR code to user
console.log('Scan this:', challenge.qrCodeUrl);

// Poll for result
const result = await verifier.pollChallenge(challenge.challengeId);

if (result.verified) {
  console.log('User is 21+');
}

Node.js SDK

Constructor

const verifier = new Cardless ID({
  apiKey: 'your_api_key',
  baseUrl: 'https://cardlessid.com' // optional
});

createChallenge(params)

Creates a new age verification challenge.

const challenge = await verifier.createChallenge({
  minAge: 21,
  callbackUrl: 'https://yourapp.com/webhook' // optional
});
Returns:
{
  challengeId: 'chal_1234567890_abc123',
  qrCodeUrl: 'https://cardlessid.com/app/age-verify?challenge=...',
  deepLinkUrl: 'cardlessid://verify?challenge=...',
  createdAt: 1234567890000,
  expiresAt: 1234568490000
}

verifyChallenge(challengeId)

Checks the current status of a challenge.

const result = await verifier.verifyChallenge(challengeId);
Returns:
{
  challengeId: 'chal_1234567890_abc123',
  verified: true,
  status: 'approved', // pending | approved | rejected | expired
  minAge: 21,
  walletAddress: 'ALGORAND_ADDRESS...',
  createdAt: 1234567890000,
  expiresAt: 1234568490000,
  respondedAt: 1234568123000
}

pollChallenge(challengeId, options)

Polls a challenge until completed or expired.

const result = await verifier.pollChallenge(challengeId, {
  interval: 2000,  // Poll every 2 seconds
  timeout: 600000  // 10 minute timeout
});

REST API

Authentication

All API requests require your API key in the X-API-Key header or request body.

POST /api/integrator/challenge/create

Create a new verification challenge.

POST /api/integrator/challenge/create
Content-Type: application/json

{
  "apiKey": "your_api_key",
  "minAge": 21,
  "callbackUrl": "https://yourapp.com/verify-callback"
}
Response:
{
  "challengeId": "chal_1234567890_abc123",
  "qrCodeUrl": "https://cardlessid.com/app/age-verify?challenge=...",
  "deepLinkUrl": "cardlessid://verify?challenge=...",
  "createdAt": 1234567890000,
  "expiresAt": 1234568490000
}

GET /api/integrator/challenge/verify/:challengeId

Verify a challenge status.

GET /api/integrator/challenge/verify/:challengeId
X-API-Key: your_api_key
Response:
{
  "challengeId": "chal_1234567890_abc123",
  "verified": true,
  "status": "approved",
  "minAge": 21,
  "walletAddress": "ALGORAND_ADDRESS...",
  "createdAt": 1234567890000,
  "expiresAt": 1234568490000,
  "respondedAt": 1234568123000
}

Example Integrations

Express.js Example

const express = require('express');
const Cardless ID = require('@cardlessid/verifier');

const app = express();
const verifier = new CardlessID({
  apiKey: process.env.CARDLESSID_API_KEY
});

// Start verification
app.post('/verify-age', async (req, res) => {
  const challenge = await verifier.createChallenge({
    minAge: 21
  });

  res.json({
    qrCodeUrl: challenge.qrCodeUrl,
    challengeId: challenge.challengeId
  });
});

// Check verification status
app.get('/verify-status/:challengeId', async (req, res) => {
  const result = await verifier.verifyChallenge(req.params.challengeId);

  res.json({
    verified: result.verified,
    status: result.status
  });
});

app.listen(3000);

Frontend Integration (React)

async function startAgeVerification() {
  // Create challenge via your backend
  const response = await fetch('/verify-age', {
    method: 'POST'
  });

  const { challengeId, qrCodeUrl } = await response.json();

  // Show QR code to user
  showQRCode(qrCodeUrl);

  // Poll for completion
  const pollInterval = setInterval(async () => {
    const statusRes = await fetch(`/verify-status/${challengeId}`);
    const status = await statusRes.json();

    if (status.status === 'approved') {
      clearInterval(pollInterval);
      onVerificationSuccess();
    } else if (status.status === 'rejected' || status.status === 'expired') {
      clearInterval(pollInterval);
      onVerificationFailed();
    }
  }, 2000);
}

Best Practices

✅ Security

  • • Store API keys securely (use environment variables)
  • • Never commit API keys to version control
  • • Use HTTPS in production
  • • Validate all inputs on your backend

✅ Performance

  • • Use webhooks instead of polling when possible
  • • Set appropriate polling intervals (2-5 seconds)
  • • Handle all status states: pending, approved, rejected, expired
  • • Implement proper timeout handling

✅ Reliability

  • • Log verification events for audit trails
  • • Rate limit verification attempts
  • • Handle network errors gracefully
  • • Use database/Redis for session storage (not in-memory)

Support

  • 📧 Email: me@djscruggs.com
  • 🐛 Issues: GitHub Issues
  • 💬 Community: Discord (coming soon)