Cardless ID Age Verification — Integration Guide
Add age verification to any web page with a single script tag. No backend required. The signed proof is verified against the Algorand blockchain — no API key, no database writes per verification.
Overview
Cardless ID provides privacy-preserving age verification backed by Algorand. Users prove they meet an age requirement without revealing their actual birthdate. The integrator receives a cryptographically signed proof that can be verified independently against the blockchain.
Key Properties
- ✅ No backend required — drop in a script tag, done
- ✅ No API key in the browser — security comes from the wallet's cryptographic signature
- ✅ Privacy-preserving — proof contains only a boolean and a wallet address, never a birthdate
- ✅ Replay-proof — nonces are signed and expire in 5 minutes
- ✅ Blockchain-verifiable — integrator can confirm the wallet holds a valid credential on Algorand
Issuer Registry
When verifying a proof, the on-chain check confirms the wallet's credential was issued by an address registered in the Cardless ID issuer registry smart contract. Only credentials from registered issuers are valid. Smart Contract Architecture →
Quick Start
CDN embed (simplest)
Add one script tag and an element to mount to. No build step, no API key.
<script src="https://cdn.cardlessid.org/verify/latest/cardlessid-verify.js"></script>
<div id="age-gate"></div>
<script>
const verify = new CardlessIDVerify({
minAge: 21,
onVerified: function({ meetsRequirement }) {
if (meetsRequirement) {
// Age verified — grant access
document.getElementById('protected-content').hidden = false;
}
}
});
verify.mount('#age-gate');
</script>npm / ES module
npm install @cardlessid/verifyimport { CardlessIDVerify } from '@cardlessid/verify';
const verify = new CardlessIDVerify({
minAge: 21,
onVerified({ meetsRequirement, walletAddress }) {
if (meetsRequirement) {
// grant access
}
},
});
verify.mount('#age-gate');JS Snippet API
| Option | Type | Required | Description |
|---|---|---|---|
| minAge | number | Yes | Minimum age to verify (1–150) |
| onVerified | function | No | Called with { meetsRequirement, walletAddress, proof } — the simplified result |
| onResult | function | No | Called with the raw SignedProof — use to send proof to your backend for re-verification |
| siteId | string | No | Public site identifier for usage analytics (not a secret) |
Verifying the Proof
The onResult callback receives a signed proof. At minimum, check proof.payload.meetsRequirement. For higher assurance, verify the Algorand signature and the on-chain credential.
Client-side verification
import { verifyProof } from '@cardlessid/verify';
const result = verifyProof(proof);
if (result.valid && result.payload.meetsRequirement) {
// Age verified — signature and timestamp checked
}Server-side verification (Node.js)
import algosdk from 'algosdk';
function verifyProof(proof: SignedProof): boolean {
const message = Buffer.from(JSON.stringify(proof.payload));
const sigBytes = Buffer.from(proof.signature, 'base64url');
// Verify ed25519 signature — algosdk.verifyBytes prepends the Algorand signing prefix
return algosdk.verifyBytes(message, sigBytes, proof.payload.walletAddress);
}Proof structure
{
"payload": {
"nonce": "eyJhbGciOiJIUzI1NiJ9...",
"walletAddress": "XYZAQ5E3EQFWMH3MQVK2BX...",
"minAge": 21,
"meetsRequirement": true,
"timestamp": 1744829470000
},
"signature": "base64url-encoded-ed25519-signature"
}REST API
The JS snippet uses these endpoints internally. You only need to call them directly if you're building a custom wallet or integration.
GET /api/v/nonce
Issues a signed, expiring nonce. No database write. No authentication required.
GET /api/v/nonce?minAge=21&siteId=my-site{
"nonce": "eyJqdGkiOiJ...",
"expiresIn": 300
}POST /api/v/submit
Called by the wallet after signing a proof. Verifies the nonce and stores the proof in a 60-second TTL cache.
POST /api/v/submit
Content-Type: application/json
{
"nonce": "eyJqdGkiOiJ...",
"signedProof": {
"payload": {
"nonce": "eyJqdGkiOiJ...",
"walletAddress": "XYZAQ5E3...",
"minAge": 21,
"meetsRequirement": true,
"timestamp": 1744829470000
},
"signature": "base64url-encoded-ed25519-signature"
}
}{ "success": true }GET /api/v/result/:nonce
Polls for a submitted proof. Returns 404 until the wallet submits. The snippet polls this every 1–2 seconds.
GET /api/v/result/eyJqdGkiOiJ...{
"proof": {
"payload": { ... },
"signature": "..."
}
}Credential APIs
Advanced / Internal Use
The Credential APIs are used by the Cardless ID system during KYC to issue credentials to user wallets. Most integrators never call these directly — they only receive the signed proof via the JS snippet. Only use these if you are an approved trusted issuer.
POST /api/credentials
Issue a credential NFT to a user's wallet after successful identity 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/...",
"explorerUrl": "https://testnet.explorer.perawallet.app/asset/123456789/"
}POST /api/credentials/transfer
Transfer and freeze a credential NFT after the user has opted in.
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"
}Best Practices
✅ Proof Verification
- • Always verify the Algorand signature server-side for sensitive decisions
- • Use
verifyProof(proof)from@cardlessid/verify— it checks the ed25519 signature and timestamp - • Check
proof.payload.timestampis recent (within 5 minutes) - • Confirm
proof.payload.minAgematches what you required
✅ UX
- • Show a loading state while the snippet polls for the result
- • Handle the case where the user closes the wallet without completing
- • Display a clear "scan with Cardless ID wallet" instruction near the QR code
✅ Analytics
- • Pass a
siteIdto the snippet to track usage per site in the Cardless ID dashboard - • The
siteIdis public — it is not a secret and does not provide access
Related Documentation
Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Community: Discord (coming soon)
