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:
- Your backend creates a challenge with your required age
- Cardless ID generates a unique, single-use challenge ID
- User scans QR code with their wallet
- Wallet verifies age requirement and responds to Cardless ID
- Your backend polls or receives webhook to confirm verification
- 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/verifier3. 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
});{
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);{
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"
}{
"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{
"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)
