Custom Verification Provider Guide
Build custom identity verification flows for Cardless ID credentials
Overview
A verification provider is a module that handles the identity verification process and returns verified identity data. Cardless ID then uses this data to issue a W3C Verifiable Credential on the Algorand blockchain.
Production Deployment & Issuer Registry
Cardless ID uses a smart contract as a registry of allowed issuers. Only credentials issued by addresses in this registry will be recognized as valid by verifiers.
For production deployment: You must complete a security audit before we add your issuer address to the on-chain registry. This ensures the integrity of the Cardless ID ecosystem.
Contact us to request addition to the registry
Key Responsibilities
- ✓ Verify user identity through your chosen method
- ✓ Return standardized identity data
- ✓ Provide verification quality metrics
- ✓ Handle errors and edge cases
Provider Types
1. Full Verification Provider
A full verification provider implements a complete identity verification flow, similar to the default custom verification flow.
Typical Components:
- Document capture - Photo of government ID
- OCR/Data extraction - Extract name, DOB, document number
- Fraud detection - Check for fake or altered documents
- Biometric verification - Selfie capture and face matching
- Liveness detection - Ensure real person, not a photo
Use Cases:
- Cloud-based verification (e.g., Stripe Identity, Persona, Onfido)
- Custom ML-based verification
- Hardware-based verification (NFC passport readers)
- Manual review workflows
2. Delegated Verification Provider
A delegated verification provider issues credentials based on existing verification from a trusted authority. The provider trusts that verification has already occurred and simply signs the credential.
Use Cases:
- Banks issuing credentials for their customers
- Government agencies (DMV, Social Security Administration)
- Universities issuing student credentials
- Employers issuing employee credentials
- Healthcare providers issuing patient credentials
→ See the Delegated Verification Guide for detailed implementation instructions.
Architecture Overview
Provider Interface
All verification providers must implement the VerificationProvider interface:
interface VerificationProvider {
name: string;
// Create a new verification session
createSession(sessionId: string): Promise<{
authToken: string;
providerSessionId: string;
}>;
// Process verification results
processVerification(
sessionId: string,
providerData: any
): Promise<VerifiedIdentity>;
// Optional: Handle webhooks from provider
handleWebhook?(payload: any): Promise<void>;
}
interface VerifiedIdentity {
firstName: string;
lastName: string;
dateOfBirth: string; // YYYY-MM-DD
documentNumber?: string;
documentType?: 'drivers_license' | 'passport' | 'government_id';
issuingCountry?: string;
issuingState?: string;
compositeHash: string; // Unique identifier
// Verification quality metrics
evidence: {
fraudDetection?: {
performed: boolean;
passed: boolean;
signals: string[];
};
documentAnalysis?: {
bothSidesAnalyzed: boolean;
lowConfidenceFields: string[];
qualityLevel: 'high' | 'medium' | 'low';
};
biometricVerification?: {
performed: boolean;
faceMatch: boolean;
faceMatchConfidence?: number;
liveness: boolean;
livenessConfidence?: number;
};
};
}Directory Structure
app/
├── utils/
│ └── verification-providers/
│ ├── index.ts # Provider registry
│ ├── base-provider.ts # Base class (optional)
│ ├── mock-provider.ts # Development/testing
│ ├── custom-provider.ts # Default custom verification
│ ├── stripe-identity-provider.ts # Example: Stripe Identity
│ └── delegated-provider.ts # Example: Delegated auth
└── routes/
└── api/
├── verification/
│ ├── start.ts # Create session
│ ├── webhook.ts # Handle provider webhooks
│ └── status.$id.ts # Check verification status
└── custom-verification/
├── upload-id.ts # Custom flow: ID upload
└── upload-selfie.ts # Custom flow: SelfieBuilding a Full Verification Provider
Step 1: Create Provider File
Create a new file in app/utils/verification-providers/your-provider.ts:
import type { VerificationProvider, VerifiedIdentity } from '~/types/verification';
import { generateCompositeHash } from '~/utils/composite-hash.server';
export class YourVerificationProvider implements VerificationProvider {
name = 'your-provider';
private apiKey: string;
constructor() {
this.apiKey = process.env.YOUR_PROVIDER_API_KEY || '';
if (!this.apiKey) {
console.warn('[YourProvider] API key not configured');
}
}
async createSession(sessionId: string): Promise<{
authToken: string;
providerSessionId: string;
}> {
// Call your provider's API to create a verification session
const response = await fetch('https://api.yourprovider.com/verifications', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
callback_url: `${process.env.BASE_URL}/api/verification/webhook`,
metadata: { cardless_session_id: sessionId }
})
});
const data = await response.json();
return {
authToken: data.client_secret,
providerSessionId: data.id
};
}
async processVerification(
sessionId: string,
providerData: any
): Promise<VerifiedIdentity> {
// Fetch verification results from your provider
const response = await fetch(
`https://api.yourprovider.com/verifications/${providerData.verification_id}`,
{
headers: { 'Authorization': `Bearer ${this.apiKey}` }
}
);
const verification = await response.json();
// Map provider data to Cardless ID format
const identity: VerifiedIdentity = {
firstName: verification.user.first_name,
lastName: verification.user.last_name,
dateOfBirth: verification.user.date_of_birth,
documentNumber: verification.document.number,
documentType: this.mapDocumentType(verification.document.type),
issuingCountry: verification.document.country,
compositeHash: generateCompositeHash(
verification.user.first_name,
verification.user.last_name,
verification.user.date_of_birth
),
evidence: {
fraudDetection: {
performed: true,
passed: verification.fraud_check.passed,
signals: verification.fraud_check.signals || []
},
documentAnalysis: {
bothSidesAnalyzed: verification.document.sides_captured === 2,
lowConfidenceFields: [],
qualityLevel: verification.document.quality
},
biometricVerification: {
performed: true,
faceMatch: verification.biometric.match,
faceMatchConfidence: verification.biometric.confidence,
liveness: verification.biometric.liveness_passed,
livenessConfidence: verification.biometric.liveness_confidence
}
}
};
return identity;
}
private mapDocumentType(providerType: string): 'drivers_license' | 'passport' | 'government_id' {
const mapping: Record<string, any> = {
'driving_license': 'drivers_license',
'passport': 'passport',
'id_card': 'government_id'
};
return mapping[providerType] || 'government_id';
}
}Step 2: Register Provider
Add your provider to app/utils/verification-providers/index.ts:
import { YourVerificationProvider } from './your-provider';
const providers = {
mock: new MockProvider(),
custom: new CustomProvider(),
'your-provider': new YourVerificationProvider(),
};
export function getProvider(name?: string): VerificationProvider {
const providerName = name || 'mock';
const provider = providers[providerName];
if (!provider) {
console.warn(`Provider "${providerName}" not found, using mock`);
return providers.mock;
}
return provider;
}Step 3: Configure Environment
YOUR_PROVIDER_API_KEY=sk_live_xxxxxxxxxxxxxStep 4: Use Provider
// Start verification with your provider
const response = await fetch('/api/verification/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: 'your-provider' })
});
const { authToken, sessionId } = await response.json();
// Use authToken with your provider's SDKAPI Reference
POST /api/verification/start
Create a new verification session.
Request:
{
"provider": "your-provider" // optional, defaults to "mock"
}Response:
{
"sessionId": "session_1234567890_abc123",
"authToken": "client_secret_xxx",
"expiresAt": "2025-01-15T10:30:00Z",
"provider": "your-provider"
}GET /api/verification/status/:sessionId
Check verification session status.
Response:
{
"sessionId": "session_1234567890_abc123",
"status": "pending" | "completed" | "failed",
"provider": "your-provider",
"verifiedData": { /* VerifiedIdentity if completed */ }
}Security Considerations
🚨 Mock Provider Production Protection
The mock verification provider is automatically blocked in production environments.
- • Mock provider throws an error if
NODE_ENV=production - • In production, the default provider is
cardlessid(Google Document AI + AWS) - • Set
VERIFICATION_PROVIDERenvironment variable to override the default - • Valid production values:
cardlessid,custom - • To temporarily allow mock in production for testing (NOT RECOMMENDED), set
ALLOW_MOCK_VERIFICATION=true
API Key Management
- • Store API keys in environment variables
- • Rotate keys regularly
- • Use separate keys for dev/staging/production
Webhook Verification
- • Verify webhook signatures from providers
- • Use HTTPS for all webhook endpoints
- • Implement replay protection
Data Retention
- • Delete ID photos after verification
- • Store only minimal PII
- • Comply with GDPR/CCPA requirements
Fraud Prevention
- • Implement rate limiting
- • Monitor for duplicate composite hashes
- • Log all verification attempts
Related Documentation
Support
- 📧 Email: me@djscruggs.com
- 🐛 Issues: GitHub Issues
- 💬 Community: Discord (coming soon)
