ID Verification Flow
Build a complete KYC verification system with document upload, OCR, face matching, and compliance checks.
What You'll Build
- ✅ Document upload (ID, passport)
- ✅ OCR data extraction
- ✅ Face matching & liveness
- ✅ National ID verification
- ✅ SIM swap detection
- ✅ Compliance reporting
Estimated Time: 30 minutes
Prerequisites
- ✅ AfriRoute Account with KYC API
- ✅ Node.js 18+ or Python 3.8+
- ✅ File storage (S3 or similar)
Verification Flow
1. Upload ID Document
↓
2. OCR Extraction
↓
3. Face Photo Upload
↓
4. Face Matching
↓
5. National ID Check
↓
6. SIM Swap Detection
↓
7. Verification Complete
Step 1: Document Upload (Node.js)
kyc-handler.js
const express = require('express');
const multer = require('multer');
const { AfriRoute } = require('afriroute-sdk');
require('dotenv').config();
const app = express();
const upload = multer({ dest: 'uploads/' });
const client = new AfriRoute({
apiKey: process.env.AFRIROUTE_API_KEY,
apiSecret: process.env.AFRIROUTE_API_SECRET
});
// Step 1: Upload ID document
app.post('/kyc/upload-id', upload.single('document'), async (req, res) => {
try {
const { country, documentType } = req.body;
const file = req.file;
console.log(`🆔 Uploading ${documentType} for ${country}`);
// Extract data using OCR
const ocrResult = await client.kyc.extractDocument({
document: file.path,
country: country,
document_type: documentType // 'national_id', 'passport', 'drivers_license'
});
console.log('✅ OCR extraction complete');
res.json({
success: true,
verification_id: ocrResult.verification_id,
extracted_data: {
full_name: ocrResult.full_name,
id_number: ocrResult.id_number,
date_of_birth: ocrResult.date_of_birth,
expiry_date: ocrResult.expiry_date,
nationality: ocrResult.nationality,
document_number: ocrResult.document_number
},
confidence_score: ocrResult.confidence,
next_step: 'upload_selfie'
});
} catch (error) {
console.error('Upload failed:', error);
res.status(400).json({ error: error.message });
}
});
// Step 2: Upload selfie for face matching
app.post('/kyc/upload-selfie', upload.single('selfie'), async (req, res) => {
try {
const { verification_id } = req.body;
const selfie = req.file;
console.log('🧑 Uploading selfie for face match');
// Perform liveness detection
const livenessResult = await client.kyc.livenessCheck({
image: selfie.path
});
if (!livenessResult.is_live) {
return res.status(400).json({
error: 'Liveness check failed',
message: 'Please take a live photo'
});
}
// Compare with ID photo
const faceMatch = await client.kyc.faceMatch({
verification_id: verification_id,
selfie: selfie.path
});
console.log(`✅ Face match: ${faceMatch.confidence}%`);
res.json({
success: true,
face_match: faceMatch.match,
confidence: faceMatch.confidence,
liveness_passed: true,
next_step: 'verify_identity'
});
} catch (error) {
console.error('Selfie upload failed:', error);
res.status(400).json({ error: error.message });
}
});
// Step 3: Verify identity with government database
app.post('/kyc/verify-identity', async (req, res) => {
try {
const { verification_id, id_number, country } = req.body;
console.log(`🔍 Verifying ID: ${id_number}`);
// Check national ID database
const idVerification = await client.kyc.verifyNationalId({
id_number: id_number,
country: country,
verification_id: verification_id
});
// Check for SIM swap (fraud detection)
const simSwap = await client.kyc.checkSimSwap({
phone_number: req.body.phone_number,
country: country
});
console.log(`✅ ID verification: ${idVerification.status}`);
res.json({
success: true,
verification_status: idVerification.status,
id_valid: idVerification.is_valid,
sim_swap_detected: simSwap.swapped_recently,
risk_score: calculateRiskScore(idVerification, simSwap),
next_step: 'complete'
});
} catch (error) {
console.error('Verification failed:', error);
res.status(400).json({ error: error.message });
}
});
function calculateRiskScore(idVerification, simSwap) {
let score = 0;
if (!idVerification.is_valid) score += 50;
if (simSwap.swapped_recently) score += 30;
if (idVerification.document_expired) score += 20;
return score;
}
// Get verification status
app.get('/kyc/status/:verification_id', async (req, res) => {
try {
const status = await client.kyc.getStatus({
verification_id: req.params.verification_id
});
res.json(status);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('🆔 KYC server running on port 3000');
});
Step 2: Python Implementation
kyc_verification.py
from flask import Flask, request, jsonify
from afriroute import AfriRoute
import os
app = Flask(__name__)
client = AfriRoute(
api_key=os.getenv('AFRIROUTE_API_KEY'),
api_secret=os.getenv('AFRIROUTE_API_SECRET')
)
@app.route('/kyc/upload-id', methods=['POST'])
def upload_id():
try:
file = request.files['document']
country = request.form['country']
doc_type = request.form['documentType']
print(f"🆔 Processing {doc_type}")
# OCR extraction
result = client.kyc.extract_document(
document=file,
country=country,
document_type=doc_type
)
return jsonify({
'success': True,
'verification_id': result.verification_id,
'extracted_data': {
'full_name': result.full_name,
'id_number': result.id_number,
'date_of_birth': result.date_of_birth
},
'confidence': result.confidence
})
except Exception as e:
return jsonify({'error': str(e)}), 400
@app.route('/kyc/upload-selfie', methods=['POST'])
def upload_selfie():
try:
selfie = request.files['selfie']
verification_id = request.form['verification_id']
# Liveness check
liveness = client.kyc.liveness_check(image=selfie)
if not liveness.is_live:
return jsonify({'error': 'Liveness check failed'}), 400
# Face matching
match = client.kyc.face_match(
verification_id=verification_id,
selfie=selfie
)
return jsonify({
'success': True,
'face_match': match.match,
'confidence': match.confidence
})
except Exception as e:
return jsonify({'error': str(e)}), 400
@app.route('/kyc/verify-identity', methods=['POST'])
def verify_identity():
try:
data = request.json
# National ID verification
id_check = client.kyc.verify_national_id(
id_number=data['id_number'],
country=data['country']
)
# SIM swap check
sim_check = client.kyc.check_sim_swap(
phone_number=data['phone_number'],
country=data['country']
)
return jsonify({
'success': True,
'id_valid': id_check.is_valid,
'sim_swap_detected': sim_check.swapped_recently,
'risk_score': calculate_risk(id_check, sim_check)
})
except Exception as e:
return jsonify({'error': str(e)}), 400
def calculate_risk(id_check, sim_check):
score = 0
if not id_check.is_valid: score += 50
if sim_check.swapped_recently: score += 30
return score
if __name__ == '__main__':
app.run(port=3000, debug=True)
Step 3: Verification Frontend
kyc-form.html
<!DOCTYPE html>
<html>
<head>
<title>ID Verification</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 50px auto; }
.step { display: none; padding: 20px; border: 1px solid #ddd; border-radius: 10px; }
.step.active { display: block; }
button { background: #003366; color: white; padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; }
input[type="file"] { margin: 10px 0; }
.success { color: green; }
.error { color: red; }
</style>
</head>
<body>
<h1>🆔 Identity Verification</h1>
<!-- Step 1: Upload ID -->
<div class="step active" id="step1">
<h2>Step 1: Upload ID Document</h2>
<select id="country">
<option>Kenya</option>
<option>Ethiopia</option>
<option>Uganda</option>
</select>
<select id="docType">
<option value="national_id">National ID</option>
<option value="passport">Passport</option>
<option value="drivers_license">Driver's License</option>
</select>
<input type="file" id="idDoc" accept="image/*">
<button onclick="uploadID()">Upload →</button>
</div>
<!-- Step 2: Upload Selfie -->
<div class="step" id="step2">
<h2>Step 2: Take a Selfie</h2>
<p>Please take a live photo for face matching</p>
<input type="file" id="selfie" accept="image/*" capture="user">
<button onclick="uploadSelfie()">Upload →</button>
</div>
<!-- Step 3: Phone Verification -->
<div class="step" id="step3">
<h2>Step 3: Verify Phone</h2>
<input type="tel" id="phone" placeholder="+254700123456">
<input type="text" id="idNumber" placeholder="ID Number">
<button onclick="verifyIdentity()">Verify →</button>
</div>
<!-- Step 4: Complete -->
<div class="step" id="step4">
<h2 class="success">✅ Verification Complete!</h2>
<p>Your identity has been successfully verified.</p>
<div id="results"></div>
</div>
<div id="message"></div>
<script>
let verificationId = null;
async function uploadID() {
const formData = new FormData();
formData.append('document', document.getElementById('idDoc').files[0]);
formData.append('country', document.getElementById('country').value);
formData.append('documentType', document.getElementById('docType').value);
const res = await fetch('/kyc/upload-id', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
verificationId = data.verification_id;
showStep(2);
showMessage('ID uploaded successfully!', 'success');
} else {
showMessage(data.error, 'error');
}
}
async function uploadSelfie() {
const formData = new FormData();
formData.append('selfie', document.getElementById('selfie').files[0]);
formData.append('verification_id', verificationId);
const res = await fetch('/kyc/upload-selfie', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success && data.face_match) {
showStep(3);
showMessage(`Face match: ${data.confidence}%`, 'success');
} else {
showMessage('Face match failed. Please try again.', 'error');
}
}
async function verifyIdentity() {
const res = await fetch('/kyc/verify-identity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verification_id: verificationId,
phone_number: document.getElementById('phone').value,
id_number: document.getElementById('idNumber').value,
country: document.getElementById('country').value
})
});
const data = await res.json();
if (data.success && data.id_valid) {
document.getElementById('results').innerHTML = `
`;
showStep(4);
} else {
showMessage('Verification failed', 'error');
}
}
function showStep(step) {
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
document.getElementById(`step${step}`).classList.add('active');
}
function showMessage(text, type) {
const msg = document.getElementById('message');
msg.textContent = text;
msg.className = type;
}
</script>
</body>
</html>
Supported Countries
- 🇰🇪 Kenya (National ID, Passport)
- 🇪🇹 Ethiopia (National ID, Passport)
- 🇺🇬 Uganda (National ID)
- 🇹🇿 Tanzania (NIDA)
- 🇳🇬 Nigeria (NIN, BVN)
Compliance
- GDPR: Store data encrypted, allow data deletion
- KYC/AML: Verify against government databases
- Data Retention: Follow local regulations
Best Practices
- Store documents encrypted
- Implement liveness detection
- Check SIM swap for fraud prevention
- Log all verification attempts
- Implement rate limiting
Troubleshooting
- Low OCR confidence: Use better quality images
- Face match fails: Ensure good lighting
- ID not found: Verify ID number format
Next Steps
- 📱 OTP Auth
- 🔒 Security
- 📊 Compliance
Pro Tip
Always implement liveness detection to prevent photo spoofing!