Who this is for
If you're a developer, this is a server-side BVN verification endpoint you can build on: input validation, correct error handling for IdentityPass's response codes, and the data-retention rules that matter if you're ever audited. If you're building a fintech product with AI assistance, read the "Why this matters" notes closely — BVN is a regulated category of personal data in Nigeria, and getting the handling wrong isn't just a bug, it's a compliance problem.
What a BVN check actually verifies, and why it's not optional
A Bank Verification Number (BVN) is a unique 11-digit identifier tied to a Nigerian's biometric data across all their bank accounts. If your app onboards users who move money — a wallet, a lending product, a marketplace with payouts — Nigerian financial regulation (via the CBN) generally requires you to confirm the person opening the account is who they claim to be. BVN verification is the standard way to do that.
IdentityPass (by Prembly) is one of several providers offering this as an API: you send them a BVN, they return the name, date of birth, and other details registered against it, which you compare against what the user typed in your signup form.
The rule that shapes everything else: this never touches the browser
Why this matters: Your IdentityPass secret key can pull a real person's registered name, date of birth, and phone number from an 11-digit number that's often visible on bank statements, printed documents, or asked for casually over the phone. If that key — or the verification call itself — is reachable from client-side code, anyone who opens your browser devtools can extract it and start querying arbitrary BVNs, at your expense, with no rate limit but the one you built. This entire flow has to run server-side, full stop.
Step 1: Get your credentials
Sign up at IdentityPass and retrieve your App ID and Secret Key from the dashboard. Store them server-side only:
IDENTITYPASS_SECRET_KEY=your-secret-key
IDENTITYPASS_APP_ID=your-app-id
If you're using Next.js, these must not have the NEXT_PUBLIC_ prefix — that prefix is Next.js's explicit signal that a variable is safe to ship to the browser, and these are the opposite of that.
Step 2: Build the verification endpoint
Here's a route handler at src/app/api/kyc/verify-bvn/route.ts that validates input before it ever reaches IdentityPass:
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { bvn } = await req.json();
// ── Validate before you spend money on an API call ──
// IdentityPass charges per verification request. A malformed BVN
// is a free, instant rejection — don't let it burn a paid API call.
if (!bvn || !/^\d{11}$/.test(bvn)) {
return NextResponse.json({ error: 'BVN must be exactly 11 digits' }, { status: 400 });
}
try {
const response = await fetch(
'https://api.myidentitypass.com/api/v2/biometrics/merchant/data/verification/bvn',
{
method: 'POST',
headers: {
'x-api-key': process.env.IDENTITYPASS_SECRET_KEY!,
'app-id': process.env.IDENTITYPASS_APP_ID!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ number: bvn }),
}
);
const result = await response.json();
if (!result.status) {
// A failed lookup is a normal outcome, not a server error —
// the BVN might just not exist, or the request might be malformed
// upstream. Don't leak IdentityPass's raw error to the client.
return NextResponse.json({ error: 'Could not verify this BVN' }, { status: 422 });
}
const { firstname, lastname, dateofbirth, phone } = result.bvn_data;
// Return only what your signup flow needs to compare against —
// not the entire provider payload. See Step 3.
return NextResponse.json({
verified: true,
firstname,
lastname,
dateofbirth,
phone,
});
} catch (error) {
console.error('BVN verification request failed:', error);
return NextResponse.json({ error: 'Verification service unavailable' }, { status: 502 });
}
}
Call it from your signup flow, then compare the returned firstname/lastname/dateofbirth against what the user entered — a mismatch is a strong signal the BVN doesn't belong to this person.
Step 3: Don't store more than you need
This is the section that gets fintech apps in trouble during a data protection audit, and it's the one AI-generated code almost never gets right on its own, because "save the whole API response to the database" is the path of least resistance.
Why this matters: Under the Nigeria Data Protection Act (NDPA), BVN-linked personal data is sensitive. Storing the full provider response indefinitely — including fields you never asked for and never use — expands what you're liable for if you're ever breached, and expands what you'd have to disclose in a data audit. The rule is simple: store the outcome, not the data.
// ❌ Don't do this — storing the full response "just in case"
await db.kycRecord.create({
data: { userId, rawResponse: JSON.stringify(result) },
});
// ✅ Do this — store the decision, not the underlying PII
await db.kycRecord.create({
data: {
userId,
bvnVerified: true,
verifiedAt: new Date(),
// If you need it for support/disputes, store a one-way hash of the
// BVN, never the BVN itself, and never the raw provider payload.
bvnHash: crypto.createHash('sha256').update(bvn).digest('hex'),
},
});
If a support or compliance workflow genuinely needs the underlying details later, re-verify on demand rather than keeping a permanent copy sitting in your database as a liability.
Step 4: Rate-limit the endpoint
Each verification call costs money and, more importantly, each one is a chance for the endpoint to be abused as a free BVN-lookup service if you're not careful. Rate-limit by user/session, not just by IP (IPs are cheap to rotate; authenticated user sessions are not):
// Pseudocode — use your actual rate-limiting infra (Redis, Upstash, etc.)
const attempts = await getAttemptCount(userId, 'bvn-verify');
if (attempts >= 3) {
return NextResponse.json(
{ error: 'Too many verification attempts. Try again later.' },
{ status: 429 }
);
}
Three attempts per user per day is a reasonable starting point for most signup flows — enough to recover from a typo, not enough to make abuse cheap.
Handling failures gracefully
BVN verification fails for reasons that have nothing to do with fraud: a typo, a temporary outage at IdentityPass, or a BVN that's valid but not yet linked in their dataset. Don't design your onboarding flow so that a failed check silently blocks the user with no explanation — surface a clear "we couldn't verify this BVN, please check the number and try again" message, and have a manual-review fallback for edge cases rather than a hard wall.
Common mistakes
- Calling the verification API from client-side code, exposing the secret key.
- Skipping input validation, wasting paid API calls on obviously malformed BVNs.
- Storing the entire provider response, including fields you never use, as permanent PII liability.
- No rate limiting, turning a paid KYC endpoint into a free lookup tool for anyone who finds it.
- Comparing verification results client-side instead of server-side, letting a modified client claim a false match.
Before you go live
- Verification call happens entirely server-side — no key in client code
- Input is validated (11 digits) before any API call is made
- Only the fields you need are persisted; raw provider responses are not stored long-term
- The endpoint is rate-limited per user
- Failure states show the user a clear next step, not a dead end
Key takeaway
BVN verification is one API call, but the surrounding decisions — where it runs, what you store, how you rate-limit it — are what determine whether your KYC flow is compliant or a future incident report. Build the check server-side, store the outcome not the data, and you're most of the way there.
Ready to start building?
Explore the most comprehensive directory of APIs for Nigerian developers and find exactly what you need.
Browse the API Directory