Integration Guide
This guide covers multiple integration paths for adding zero-knowledge identity verification to your application, from quick embeds to full custom implementations.
Table of Contents
- 5-Minute Age Verification Embed
- Server-Side Verifier Setup
- Nationality Set-Membership Verification
- OpenID4VP Integration Path
- Mobile SDK Integration
- Configuration Options
- Troubleshooting
5-Minute Age Verification Embed {#5-minute-embed}
The fastest way to add age verification to any website.
Step 1: Install Dependencies
npm install @zk-id/sdk @zk-id/core
See the age-gate-widget example for a complete implementation.
Step 2: Add to Your Page
<button id="verify-age">Enter Site (18+ Required)</button>
<script type="module">
import { ZkIdAgeGateWidget } from '@zk-id/age-gate-widget';
document.getElementById('verify-age').addEventListener('click', () => {
ZkIdAgeGateWidget.init({
verificationEndpoint: 'https://your-verifier.com/auth/request',
minAge: 18,
onVerified: () => {
// User verified! Show restricted content
document.getElementById('restricted-content').classList.remove('hidden');
},
onRejected: (reason) => {
console.log('Verification failed:', reason);
},
});
});
</script>
Step 3: Deploy a Verifier
See Server-Side Setup below for deploying your verification endpoint.
Widget Options
interface ZkIdAgeGateConfig {
// Required
verificationEndpoint: string; // Your OpenID4VP verifier endpoint
minAge: number; // Minimum age (e.g., 18, 21)
onVerified: () => void; // Success callback
// Optional
onRejected?: (reason: string) => void; // Failure callback
issuerEndpoint?: string; // Custom issuer URL
circuitPaths?: {
ageWasm: string; // Defaults to CDN
ageZkey: string;
};
branding?: {
title?: string; // Modal title
primaryColor?: string; // Hex color (e.g., "#667eea")
logo?: string; // Logo URL
};
}
Example: Custom Branding
ZkIdAgeGateWidget.init({
verificationEndpoint: 'https://your-verifier.com/auth/request',
minAge: 21,
onVerified: () => unlockContent(),
branding: {
title: 'Verify Your Age',
primaryColor: '#ff6b6b',
logo: 'https://your-site.com/logo.png',
},
});
Server-Side Verifier Setup {#server-side-setup}
Every integration requires a verifier server to validate proofs.
Quick Start (Express)
npm install @zk-id/sdk express cors
import express from 'express';
import cors from 'cors';
import { ZkIdServer, OpenID4VPVerifier, InMemoryIssuerRegistry } from '@zk-id/sdk';
const app = express();
app.use(cors());
app.use(express.json());
// Initialize ZkIdServer
const zkIdServer = new ZkIdServer({
verificationKeyPath: './verification_key.json',
issuerRegistry: new InMemoryIssuerRegistry([
// Add trusted issuer public keys here
]),
});
// Wrap with OpenID4VP
const verifier = new OpenID4VPVerifier({
zkIdServer,
verifierUrl: 'https://your-verifier.com',
verifierId: 'your-verifier-id',
callbackUrl: 'https://your-verifier.com/openid4vp/callback',
});
// Authorization endpoint
app.get('/auth/request', async (req, res) => {
const minAge = parseInt(req.query.minAge as string) || 18;
const authRequest = verifier.createAgeVerificationRequest(minAge);
res.json({
authRequest,
qrCode: await generateQRCode(authRequest), // Optional
});
});
// Callback endpoint
app.post('/openid4vp/callback', async (req, res) => {
const result = await verifier.verifyPresentation(req.body, req.ip);
if (result.verified) {
res.json({ verified: true, message: 'Age verified' });
} else {
res.status(400).json({ verified: false, error: result.error });
}
});
app.listen(5002, () => {
console.log('Verifier running on http://localhost:5002');
});
Issuer Registry Setup
Add trusted issuer public keys to validate credentials:
import { InMemoryIssuerRegistry } from '@zk-id/sdk';
import { createPublicKey } from 'crypto';
// Load issuer public keys
const issuer1PublicKey = createPublicKey({
key: Buffer.from('base64-encoded-public-key', 'base64'),
type: 'spki',
format: 'der',
});
const issuer2PublicKey = createPublicKey({
key: Buffer.from('base64-encoded-public-key', 'base64'),
type: 'spki',
format: 'der',
});
const issuerRegistry = new InMemoryIssuerRegistry([
{
issuer: 'Government ID Provider',
publicKey: issuer1PublicKey,
status: 'active',
},
{
issuer: 'Bank KYC Service',
publicKey: issuer2PublicKey,
status: 'active',
},
]);
Environment Variables
# .env
VERIFIER_URL=https://your-verifier.com
VERIFIER_ID=your-verifier-id
VERIFICATION_KEY_PATH=./verification_key.json
PORT=5002
Deployment
Railway / Render / Fly.io:
# Install dependencies
npm install
# Build
npm run build
# Start
npm start
Docker:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 5002
CMD ["npm", "start"]
Nationality Set-Membership Verification {#nationality-set-path}
Prove a userβs nationality is in an allowed set (e.g., βis an EU citizenβ) without revealing the specific country. This uses the nationality-set-verify circuit with up to 32 allowed ISO 3166-1 numeric codes.
Quick Example
import { generateNationalitySetProofAuto, verifyNationalitySetProof, REGION_EU } from '@zk-id/core';
import { readFileSync } from 'fs';
// Client: generate proof that user is an EU citizen
const proof = await generateNationalitySetProofAuto(
credential,
REGION_EU, // allowedCodes: all 27 EU member states
'nonce-from-server',
Date.now(),
);
// Server: verify the proof
const verificationKey = JSON.parse(readFileSync('./nationality-set-verify_vkey.json', 'utf-8'));
const isValid = await verifyNationalitySetProof(proof, verificationKey, REGION_EU);
console.log('EU citizen verified:', isValid); // true β nationality is in REGION_EU
Available Region Constants
import {
REGION_EU,
REGION_US,
REGION_US_EU,
REGION_EEA,
NATIONALITY_SET_MAX_SIZE,
} from '@zk-id/core';
// REGION_EU β all 27 EU member states
// REGION_US β [840] (United States only)
// REGION_US_EU β REGION_US + REGION_EU (28 codes)
// REGION_EEA β REGION_EU + Iceland (352), Liechtenstein (438), Norway (578)
// NATIONALITY_SET_MAX_SIZE β 32 (maximum allowed codes per proof)
Server-Side Setup for Set-Membership Verification
import express from 'express';
import { verifyNationalitySetProof, REGION_EU } from '@zk-id/core';
import { readFileSync } from 'fs';
const nationalitySetVKey = JSON.parse(
readFileSync('./nationality-set-verify-verification_key.json', 'utf-8'),
);
app.post('/api/verify-eu-citizenship', async (req, res) => {
const { proof } = req.body;
// Verify the proof matches REGION_EU
const isValid = await verifyNationalitySetProof(proof, nationalitySetVKey, REGION_EU);
if (isValid) {
res.json({ verified: true, message: 'EU citizenship confirmed' });
} else {
res.status(400).json({ verified: false, error: 'Nationality not in EU member states' });
}
});
Custom Allowed Sets
You can specify any set of up to 32 ISO 3166-1 numeric codes:
// UK + EU (post-Brexit cross-border scenario)
const UK_AND_EU = [826, ...REGION_EU]; // 826 = United Kingdom
// Nordic countries only
const NORDIC = [208, 246, 352, 578, 752]; // Denmark, Finland, Iceland, Norway, Sweden
OpenID4VP Integration Path {#openid4vp-path}
Standards-compliant integration for interoperability with other wallets.
Architecture
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Verifier β β Wallet β β Issuer β
β (Your App) β β (User's) β β (Trusted) β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β β β
β 1. Authorization Request β β
β (Presentation Definition) β β
βββββββββββββββββββββββββββββββββ>β β
β β 2. Fetch credential β
β βββββββββββββββββββββββββββββββββ>β
β β 3. Return credential β
β β<βββββββββββββββββββββββββββββββββ€
β β 4. Generate ZK proof β
β β (locally, private) β
β 5. Submit Presentation β β
β<βββββββββββββββββββββββββββββββββ€ β
β 6. Verify proof β β β
Verifier Implementation
import { OpenID4VPVerifier } from '@zk-id/sdk';
const verifier = new OpenID4VPVerifier({
zkIdServer,
verifierUrl: 'https://your-app.com',
verifierId: 'your-app',
callbackUrl: 'https://your-app.com/openid4vp/callback',
});
// Create authorization request
app.get('/auth/request', (req, res) => {
const authRequest = verifier.createAgeVerificationRequest(18);
// For mobile wallets, generate QR code
const qrCode = generateQRCode(
`openid4vp://?${new URLSearchParams({
presentation_definition: JSON.stringify(authRequest.presentation_definition),
response_uri: authRequest.response_uri,
nonce: authRequest.nonce,
client_id: authRequest.client_id,
state: authRequest.state,
})}`,
);
res.json({ authRequest, qrCode });
});
// Handle presentation submission
app.post('/openid4vp/callback', async (req, res) => {
const result = await verifier.verifyPresentation(req.body, req.ip);
res.json({ verified: result.verified });
});
Browser Wallet Implementation
import { OpenID4VPWallet, InMemoryCredentialStore } from '@zk-id/sdk';
const wallet = new OpenID4VPWallet({
credentialStore: new InMemoryCredentialStore(),
circuitPaths: {
ageWasm: 'https://cdn.example.com/age.wasm',
ageZkey: 'https://cdn.example.com/age.zkey',
},
walletId: 'my-wallet',
});
// Fetch authorization request from verifier
const response = await fetch('https://verifier.com/auth/request');
const { authRequest } = await response.json();
// Generate presentation
const presentation = await wallet.generatePresentation(authRequest);
// Submit to callback
await fetch(authRequest.response_uri, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(presentation),
});
Custom Presentation Definitions
Create custom verification requests:
const customRequest = verifier.createAuthorizationRequest({
presentation_definition: {
id: 'nationality-verification',
name: 'Nationality Verification',
purpose: 'Verify citizenship',
input_descriptors: [
{
id: 'nationality-proof',
constraints: {
fields: [
{
path: ['$.credentialSubject.nationality'],
filter: {
type: 'string',
const: 'US',
},
},
],
},
},
],
},
});
Mobile SDK Integration {#mobile-sdk-path}
Native mobile wallet implementation for React Native and Expo.
Installation
npm install @zk-id/mobile @zk-id/core
npm install @react-native-async-storage/async-storage
Basic Setup (React Native)
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MobileWallet, MobileCredentialStore, SecureStorageAdapter } from '@zk-id/mobile';
// Create storage adapter
const storageAdapter: SecureStorageAdapter = {
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
removeItem: (key) => AsyncStorage.removeItem(key),
getAllKeys: () => AsyncStorage.getAllKeys(),
};
// Initialize wallet
const wallet = new MobileWallet({
credentialStore: new MobileCredentialStore(storageAdapter),
circuitPaths: {
ageWasm: 'https://cdn.example.com/age.wasm',
ageZkey: 'https://cdn.example.com/age.zkey',
},
});
Credential Management
// Add credential from issuer
const credential = await fetchCredentialFromIssuer();
await wallet.addCredential(credential);
// List all credentials
const credentials = await wallet.listCredentials();
// Export for backup
const backup = await wallet.exportCredentials();
await saveToCloud(backup);
// Import from backup
const restored = await loadFromCloud();
await wallet.importCredentials(restored);
Proof Generation
// Generate age proof
const proofResponse = await wallet.generateAgeProof(
null, // Auto-select most recent credential
18, // Minimum age
'challenge-nonce-from-verifier',
);
// Submit to verifier
await fetch('https://verifier.com/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(proofResponse),
});
Deep Link Handling
import { Linking } from 'react-native';
import { parseAuthorizationRequest, generatePresentation, submitPresentation } from '@zk-id/mobile';
// Listen for deep links
Linking.addEventListener('url', async (event) => {
const url = event.url; // e.g., openid4vp://?presentation_definition=...
// Parse authorization request
const authRequest = parseAuthorizationRequest(url);
// Generate presentation
const presentation = await generatePresentation(authRequest, wallet);
// Submit to verifier
const httpAdapter = {
post: (url, body, headers) =>
fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }),
get: (url, headers) => fetch(url, { headers }),
};
const result = await submitPresentation(authRequest.response_uri, presentation, httpAdapter);
console.log('Verification result:', result);
});
QR Code Scanning
import { Camera } from 'expo-camera';
import { parseAuthorizationRequest } from '@zk-id/mobile';
const handleBarCodeScanned = async ({ data }) => {
if (data.startsWith('openid4vp://')) {
const authRequest = parseAuthorizationRequest(data);
// Handle authorization request...
}
};
<Camera onBarCodeScanned={handleBarCodeScanned} />
Expo SecureStore Example
import * as SecureStore from 'expo-secure-store';
const expoStorageAdapter: SecureStorageAdapter = {
getItem: (key) => SecureStore.getItemAsync(key),
setItem: (key, value) => SecureStore.setItemAsync(key, value),
removeItem: (key) => SecureStore.deleteItemAsync(key),
getAllKeys: async () => {
// Expo SecureStore doesn't support getAllKeys natively
// Maintain a key index
const index = await SecureStore.getItemAsync('zkid:key-index');
return index ? JSON.parse(index) : [];
},
};
Configuration Options {#configuration}
Circuit Paths
CDN (Recommended for Production):
circuitPaths: {
ageWasm: 'https://cdn.jsdelivr.net/npm/@zk-id/circuits/dist/age.wasm',
ageZkey: 'https://cdn.jsdelivr.net/npm/@zk-id/circuits/dist/age.zkey',
nationalityWasm: 'https://cdn.jsdelivr.net/npm/@zk-id/circuits/dist/nationality.wasm',
nationalityZkey: 'https://cdn.jsdelivr.net/npm/@zk-id/circuits/dist/nationality.zkey',
}
Self-Hosted:
circuitPaths: {
ageWasm: 'https://your-cdn.com/circuits/age.wasm',
ageZkey: 'https://your-cdn.com/circuits/age.zkey',
}
Local Development:
circuitPaths: {
ageWasm: '/circuits/age.wasm',
ageZkey: '/circuits/age.zkey',
}
Issuer Registry
In-Memory (Development):
import { InMemoryIssuerRegistry } from '@zk-id/sdk';
import { createPublicKey } from 'crypto';
const issuerRegistry = new InMemoryIssuerRegistry([
{
issuer: 'Test Issuer',
publicKey: createPublicKey(issuerPublicKeyPem),
status: 'active',
},
]);
Database-Backed (Production):
import { IssuerRegistry, IssuerRecord } from '@zk-id/sdk';
import { createPublicKey } from 'crypto';
class PostgresIssuerRegistry implements IssuerRegistry {
async getIssuer(issuer: string): Promise<IssuerRecord | null> {
const row = await db.query(
'SELECT issuer, public_key_pem, status FROM issuers WHERE issuer = $1',
[issuer],
);
if (!row) return null;
return {
issuer: row.issuer,
publicKey: createPublicKey(row.public_key_pem),
status: row.status,
};
}
}
Credential Storage
Browser (IndexedDB):
import { IndexedDBCredentialStore } from '@zk-id/sdk';
const store = new IndexedDBCredentialStore();
Mobile (Secure Storage):
import { MobileCredentialStore } from '@zk-id/mobile';
import AsyncStorage from '@react-native-async-storage/async-storage';
const store = new MobileCredentialStore({
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
removeItem: (key) => AsyncStorage.removeItem(key),
getAllKeys: () => AsyncStorage.getAllKeys(),
});
Server (In-Memory for Testing):
import { InMemoryCredentialStore } from '@zk-id/sdk';
const store = new InMemoryCredentialStore();
Troubleshooting {#troubleshooting}
Common Issues
Proof Generation Takes Too Long
Problem: First proof takes 45-60 seconds.
Solution:
- This is expected for the initial circuit load (~5MB WASM + zkey)
- Subsequent proofs are faster (~10-15 seconds)
- Consider adding a loading indicator
- Roadmap: Circuit caching and proof reuse (Q2 2026)
// Show loading indicator
setLoading(true);
setLoadingMessage('Loading verification circuit... (~45 seconds)');
const proof = await wallet.generateAgeProof(null, 18, nonce);
setLoading(false);
CORS Errors
Problem: Access-Control-Allow-Origin errors when calling verifier.
Solution: Add CORS headers to your verifier:
import cors from 'cors';
app.use(
cors({
origin: ['https://your-frontend.com', 'http://localhost:5000'],
credentials: true,
}),
);
Circuit Files Not Loading
Problem: 404 errors for .wasm or .zkey files.
Solution:
- Verify circuit paths are correct
- Check CDN availability
- Serve files with correct MIME types:
// Express
app.use(
'/circuits',
express.static('circuits', {
setHeaders: (res, path) => {
if (path.endsWith('.wasm')) {
res.setHeader('Content-Type', 'application/wasm');
}
},
}),
);
Verification Fails with βInvalid Signatureβ
Problem: Proof verification returns βInvalid signatureβ error.
Solution:
- Ensure issuer public key is in the registry
- Verify credential hasnβt expired
- Check that proof timestamp is recent
- Confirm circuit files match (same trusted setup)
// Debug issuer registry
const issuer = await issuerRegistry.getIssuer(credential.issuerPublicKey);
console.log('Issuer found:', issuer);
Mobile Deep Links Not Working
Problem: openid4vp:// URLs donβt trigger the app.
Solution: Configure deep link handling in your app:
iOS (Info.plist):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>openid4vp</string>
</array>
</dict>
</array>
Android (AndroidManifest.xml):
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="openid4vp" />
</intent-filter>
Storage Adapter Errors
Problem: SecureStorageAdapter methods throwing errors.
Solution:
- Ensure all 4 methods are implemented:
getItem,setItem,removeItem,getAllKeys - Handle null returns correctly
- Test with mock adapter first:
const mockAdapter = {
getItem: async (key) => mockStorage.get(key) ?? null,
setItem: async (key, value) => {
mockStorage.set(key, value);
},
removeItem: async (key) => {
mockStorage.delete(key);
},
getAllKeys: async () => Array.from(mockStorage.keys()),
};
Performance Optimization
Cache Circuit Files
// Service Worker (browser)
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('zk-circuits').then((cache) => {
return cache.addAll(['/circuits/age.wasm', '/circuits/age.zkey']);
}),
);
});
Preload Circuits
<!-- HTML head -->
<link rel="preload" href="/circuits/age.wasm" as="fetch" crossorigin />
<link rel="preload" href="/circuits/age.zkey" as="fetch" crossorigin />
Lazy Load Wallet
// Only load wallet when needed
const walletPromise = import('@zk-id/sdk').then((module) => new module.OpenID4VPWallet(config));
button.addEventListener('click', async () => {
const wallet = await walletPromise;
// Use wallet...
});
Security Best Practices
- Always use HTTPS in production
- Validate nonces on the server side
- Check proof timestamps to prevent replay attacks
- Rate limit verification endpoints
- Log verification attempts for audit trails
- Keep issuer registry updated with trusted issuers only
- Use secure storage on mobile (Keychain/EncryptedSharedPreferences)
- Never log user credentials or proofs
Next Steps
-
Try the demos:
-
Read the docs:
-
Join the community:
-
Deploy to production:
Need help? Open an issue or ask in Discussions.