Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Age Verification

Verify a user's age through Portal's browser-based verification service. The flow uses Cashu tokens as cryptographic proof — tokens have no monetary value, they serve as tamper-proof verification tickets.

How it works

  1. Your backend creates a verification session → gets a session_url
  2. Redirect the user to the session_url in their browser
  3. The user completes identity verification
  4. Portal mints a Cashu verification token and returns it via the event stream
  5. Your backend receives the token — verification complete ✅

The entire flow is handled by a single SDK call (createVerificationSession), which creates the session and automatically starts listening for the token.

Prerequisites

  • A PortalHub account at hub.getportal.cc — create your verification API key and manage your dashboard from there
  • [verification] api_key configured in portal-rest

Configuration

  1. Sign up / log in at hub.getportal.cc
  2. Create a verification API key from the dashboard
  3. Add it to your config.toml:
[verification]
api_key = "your-api-key"

Or via environment variable:

PORTAL__VERIFICATION__API_KEY=your-api-key

Creating a verification session

HTTP
# Create session (relays are optional — defaults to [nostr] config)
curl -s -X POST $BASE_URL/verification/sessions \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
# → {
#   "session_id": "abc-123",
#   "session_url": "https://verify.getportal.cc/?id=abc-123",
#   "ephemeral_npub": "npub1...",
#   "expires_at": 1234567890,
#   "stream_id": "def-456"
# }

# Poll for the verification token
curl -s "$BASE_URL/events/def-456" \
  -H "Authorization: Bearer $AUTH_TOKEN"
# → cashu_response event with the token when verification completes
JavaScript
import { PortalClient } from 'portal-sdk';

const client = new PortalClient({
  baseUrl: 'http://localhost:3000',
  authToken: 'your-token',
});

// Single call — creates session + listens for token
const session = await client.createVerificationSession();

console.log(`Redirect user to: ${session.session_url}`);

// Wait for the user to complete verification
const result = await client.poll(session, {
  intervalMs: 1000,
  timeoutMs: 5 * 60 * 1000,
});

if (result.status === 'success') {
  console.log('Verified!', result.token);
} else {
  console.log('Failed:', result);
}

Custom relays

By default, the session uses the relays from your [nostr] config. Override per-request:

HTTP
curl -s -X POST $BASE_URL/verification/sessions \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "relays": ["wss://relay.damus.io"] }'
JavaScript
const session = await client.createVerificationSession([
  'wss://relay.damus.io',
]);

Requesting a token from a verified user

If a user already holds a verification token (e.g. verified through the mobile app), you can request it directly:

HTTP
curl -s -X POST $BASE_URL/verification/token \
  -H "Authorization: Bearer $AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient_key": "USER_PUBKEY_HEX",
    "subkeys": []
  }'
# → { "stream_id": "..." }
# Poll events for cashu_response
JavaScript
const op = await client.requestVerificationToken(userPubkeyHex, []);
const result = await client.poll(op, { intervalMs: 1000, timeoutMs: 60_000 });

Token lifecycle

  • Web verification tokens have an amount of 1 (single-use ticket)
  • Mobile app tokens have an amount of 500 (reusable across services)
  • Tokens use Portal's mint (https://mint.getportal.cc) with unit multi
  • Cashu is used purely as a protocol — tokens carry no monetary value
  • To prevent replay attacks, burn the token after receiving it (see Cashu Tokens guide)

Verification statuses

StatusDescription
successVerification passed. token field contains the Cashu token.
rejectedVerification failed. reason may contain details.
insufficient_fundsMint could not issue the token.