KYC Integration
Learn how to verify customer identities using Zentra’s KYC (Know Your Customer) APIs. Support BVN, NIN, and document verification.
Identity is a reviewed but tenant-gated namespace in the current route manifest. Use these flows only when your environment explicitly enables the identity routes and verify the exact shape against your approved tenant contract.
What You’ll Learn
Verify BVN (Bank Verification Number)
Verify NIN (National Identification Number)
Perform liveness checks
Handle verification results
Build a complete onboarding flow
Prerequisites
Zentra API account with KYC enabled
Customer information to verify
Webhook endpoint for async results
KYC Flow Overview
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Customer │────▶│ Your Server │────▶│ Zentra API │
│ (Submit) │ │ (Verify) │ │ (Check) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
│ 1. Enter │ 2. Submit │
│ Details │ for KYC │
│ │ ──────────────▶ │
│ │ │ 3. Verify with
│ │ │ NIBSS/NIMC
│ │ ◀────────────── │
│ 4. Return │ 5. Result │
│ Result │ (sync/async) │
│ ◀────────────────│ │
Step 1: Verify BVN
BVN verification is the primary KYC method in Nigeria:
const Zentra = require ( '@zentra/sdk' );
const client = new Zentra . Client ({
apiKey: process . env . ZENTRA_SECRET_KEY
});
async function verifyBVN ( customerData ) {
const result = await client . identity . verifyBVN ({
bvn: customerData . bvn ,
first_name: customerData . firstName ,
last_name: customerData . lastName ,
date_of_birth: customerData . dateOfBirth , // YYYY-MM-DD
phone: customerData . phone // Optional, for additional matching
});
return {
verified: result . verified ,
match_score: result . match_score ,
details: {
first_name_match: result . first_name_match ,
last_name_match: result . last_name_match ,
dob_match: result . dob_match ,
phone_match: result . phone_match
}
};
}
BVN Verification Response
{
"verified" : true ,
"match_score" : 95 ,
"first_name_match" : true ,
"last_name_match" : true ,
"dob_match" : true ,
"phone_match" : true ,
"bvn_data" : {
"first_name" : "JOHN" ,
"last_name" : "DOE" ,
"middle_name" : "JAMES" ,
"date_of_birth" : "1990-05-15" ,
"phone" : "08012345678" ,
"gender" : "Male" ,
"registration_date" : "2015-03-20"
}
}
BVN data is returned in uppercase. Normalize before comparing with your records.
Step 2: Verify NIN
Verify National Identification Number:
async function verifyNIN ( customerData ) {
const result = await client . identity . verifyNIN ({
nin: customerData . nin ,
first_name: customerData . firstName ,
last_name: customerData . lastName ,
date_of_birth: customerData . dateOfBirth
});
return {
verified: result . verified ,
match_score: result . match_score ,
photo: result . photo , // Base64 encoded photo
details: result . nin_data
};
}
NIN verification returns the person’s photo. Only store and display with proper consent.
Step 3: Liveness Check
Verify the person is real and matches their documents:
async function performLivenessCheck ( customerId , selfieData ) {
const result = await client . identity . verifySelfie ({
customer_id: customerId ,
selfie: selfieData . imageBase64 ,
reference_image: selfieData . referenceImage , // From BVN/NIN
liveness_check: true
});
return {
is_live: result . liveness_passed ,
face_match: result . face_match_score > 80 ,
face_match_score: result . face_match_score ,
fraud_signals: result . fraud_signals
};
}
Liveness Response
{
"liveness_passed" : true ,
"face_match_score" : 92 ,
"fraud_signals" : [],
"quality_score" : 85 ,
"details" : {
"eyes_open" : true ,
"face_centered" : true ,
"good_lighting" : true
}
}
Step 4: Complete Onboarding Flow
Build a full KYC onboarding:
async function onboardCustomer ( userData ) {
// Step 1: Create an app-level customer record in your own backend
const customer = await db . customers . create ({
email: userData . email ,
first_name: userData . firstName ,
last_name: userData . lastName ,
phone: userData . phone ,
date_of_birth: userData . dateOfBirth ,
kyc_status: 'pending'
});
// Step 2: Verify BVN
const bvnResult = await client . identity . verifyBVN ({
bvn: userData . bvn ,
first_name: userData . firstName ,
last_name: userData . lastName ,
date_of_birth: userData . dateOfBirth
});
if ( ! bvnResult . verified ) {
throw new Error ( 'BVN verification failed' );
}
// Step 3: Persist verified status in your own system
await db . customers . update ({
where: { id: customer . id },
data: {
kyc_status: 'verified' ,
kyc_level: 1 ,
bvn_verified: true ,
bvn_verified_at: new Date ()
}
});
// Step 4: Optionally provision add-on rails after KYC passes
const railCustomerId = await ensureRailCustomerForTenant ( customer );
const account = await client . virtualAccounts . create ({
customer_id: railCustomerId ,
account_name: ` ${ userData . firstName } ${ userData . lastName } ` ,
bvn: userData . bvn
});
return {
customer_id: customer . id ,
rail_customer_id: railCustomerId ,
account ,
kyc_status: 'verified'
};
}
Step 5: Handle Async Results
Some verifications complete asynchronously:
app . post ( '/webhooks/zentra' , async ( req , res ) => {
const { event , data } = req . body ;
switch ( event ) {
case 'identity.verified' :
await handleVerificationSuccess ( data );
break ;
case 'identity.failed' :
await handleVerificationFailed ( data );
break ;
case 'identity.pending' :
await handleVerificationPending ( data );
break ;
}
res . status ( 200 ). send ( 'OK' );
});
async function handleVerificationSuccess ( data ) {
const { customer_id , verification_type , details } = data ;
await db . customers . update ({
where: { zentra_id: customer_id },
data: {
kyc_status: 'verified' ,
kyc_verified_at: new Date (),
kyc_details: details
}
});
// Notify customer
await notificationService . send ( customer_id , {
title: 'Verification Complete' ,
body: 'Your identity has been verified. You can now access all features.'
});
}
async function handleVerificationFailed ( data ) {
const { customer_id , reason , retry_allowed } = data ;
await db . customers . update ({
where: { zentra_id: customer_id },
data: {
kyc_status: 'failed' ,
kyc_failure_reason: reason
}
});
// Notify customer
await notificationService . send ( customer_id , {
title: 'Verification Failed' ,
body: retry_allowed
? 'Please try again with correct details.'
: 'Contact support for assistance.'
});
}
KYC Levels
Implement tiered verification for different access levels:
Level Requirements Limits 0 Email + Phone View only 1 BVN verified ₦50,000/day 2 BVN + NIN ₦200,000/day 3 Full KYC + Liveness ₦1,000,000/day
function getTransactionLimit ( kycLevel ) {
const limits = {
0 : 0 ,
1 : 5000000 , // ₦50,000
2 : 20000000 , // ₦200,000
3 : 100000000 // ₦1,000,000
};
return limits [ kycLevel ] || 0 ;
}
Error Handling
Handle common KYC errors:
try {
const result = await client . identity . verifyBVN ( data );
return result ;
} catch ( error ) {
switch ( error . code ) {
case 'invalid_bvn' :
throw new Error ( 'Invalid BVN format' );
case 'bvn_not_found' :
throw new Error ( 'BVN not found in database' );
case 'name_mismatch' :
throw new Error ( 'Name does not match BVN records' );
case 'dob_mismatch' :
throw new Error ( 'Date of birth does not match' );
case 'verification_limit' :
throw new Error ( 'Daily verification limit reached' );
case 'service_unavailable' :
throw new Error ( 'Verification service temporarily unavailable' );
default :
throw new Error ( 'Verification failed. Please try again.' );
}
}
Testing in Sandbox
Use test data in sandbox mode:
BVN Result 12345678901Successful verification 12345678902Name mismatch 12345678903DOB mismatch 12345678904BVN not found
Test all failure scenarios to ensure your app handles them gracefully.
Best Practices
Normalize Data : Convert names to uppercase before verification
Handle Partial Matches : Accept 80%+ match scores with manual review
Rate Limit : Prevent brute-force attempts
Audit Trail : Log all verification attempts
Data Security : Encrypt and minimize storage of PII
Next Steps
Identity API Full API reference
Virtual Accounts Create accounts after KYC
Webhooks Handle KYC events
Going Live Production checklist