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.
API Categories
- Integrator APIs - For verifying users' existing credentials (challenge/response)
- Credential APIs - For issuing new credentials to users (internal/advanced use)
Integrator APIs
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
}Credential APIs
Advanced Use Only
The Credential APIs are used internally by the Cardless ID system to issue new credentials. Most integrators should use the Integrator APIs above. Only use these endpoints if you're building a custom verification flow or have been approved as a trusted issuer.
POST /api/credentials
Issue a new credential NFT to a user's wallet after successful verification.
POST /api/credentials
Content-Type: application/json
{
"sessionId": "verification_session_id",
"walletAddress": "ALGORAND_WALLET_ADDRESS",
"birthdate": "YYYY-MM-DD",
"demo": false
}{
"success": true,
"assetId": "123456789",
"credentialUrl": "https://cardlessid.com/app/wallet-status/...",
"optInUrl": "https://cardlessid.com/app/optin/...",
"explorerUrl": "https://testnet.explorer.perawallet.app/asset/123456789/"
}Security Notes:
- • Session validation ensures only verified users receive credentials
- • AssetId is stored in the session for transfer validation
- • Each session can only issue one credential (prevents replay attacks)
- • Birthdate is never stored - only used to generate the credential hash
POST /api/credentials/transfer
Transfer and freeze credential NFT after user has opted in to receive it.
POST /api/credentials/transfer
Content-Type: application/json
{
"sessionId": "verification_session_id",
"assetId": 123456789,
"walletAddress": "ALGORAND_WALLET_ADDRESS"
}{
"success": true,
"assetId": "123456789",
"transferTxId": "TRANSFER_TRANSACTION_ID",
"freezeTxId": "FREEZE_TRANSACTION_ID",
"explorerUrls": {
"transfer": "https://testnet.explorer.perawallet.app/tx/...",
"freeze": "https://testnet.explorer.perawallet.app/tx/..."
}
}Important Security Features:
- • Session Validation: Only transfers to the wallet that completed verification
- • Asset Validation: Only transfers the assetId that was issued in this session
- • Single Use: Each session can only transfer once (prevents replay attacks)
- • NFT Freeze: Credential is frozen after transfer to prevent tampering
These security measures were added to prevent unauthorized credential transfers and ensure that credentials can only be issued to verified users.
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)
