← Back to Documentation

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/verify
import { CardlessIDVerify } from '@cardlessid/verify';

const verify = new CardlessIDVerify({
  minAge: 21,
  onVerified({ meetsRequirement, walletAddress }) {
    if (meetsRequirement) {
      // grant access
    }
  },
});

verify.mount('#age-gate');

JS Snippet API

OptionTypeRequiredDescription
minAgenumberYesMinimum age to verify (1–150)
onVerifiedfunctionNoCalled with { meetsRequirement, walletAddress, proof } — the simplified result
onResultfunctionNoCalled with the raw SignedProof — use to send proof to your backend for re-verification
siteIdstringNoPublic site identifier for usage analytics (not a secret)
No API key needed. The JS snippet is public. Security is provided by the wallet's ed25519 signature and the on-chain credential — not by a secret embedded in the page.

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
Response:
{
  "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"
  }
}
Response:
{ "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...
Response (200 when found):
{
  "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
}
Response:
{
  "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"
}
Response:
{
  "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.timestamp is recent (within 5 minutes)
  • • Confirm proof.payload.minAge matches 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 siteId to the snippet to track usage per site in the Cardless ID dashboard
  • • The siteId is public — it is not a secret and does not provide access

Related Documentation

Support