Paystack provides identity verification through three main APIs: Resolve Account Number (returns the account name for a given bank account), Resolve Card BIN (returns the card brand and bank), and Validate Customer for BVN verification. You call these from your backend only, cache the results, and build KYC flows that verify user identity before enabling transfers or high-value transactions.
Paystack provides three identity-related endpoints. Each one answers a different question about your customer.
1. Resolve Account Number. Given a bank account number and bank code, Paystack asks the bank: "What name is on this account?" The bank returns the account holder's name. You use this to verify that a user actually owns the bank account they entered. This is the endpoint you will use most often.
2. Resolve Card BIN. Given the first 6 digits of a card number (the BIN), Paystack returns the card brand (Visa, Mastercard, Verve), the issuing bank, the card type (debit, credit, prepaid), and the country of issue. You use this for fraud screening, card-type routing, and displaying the right card logo in your checkout UI.
3. Validate Customer (BVN). This endpoint lets you verify a customer's Bank Verification Number against their identity record. It does not return the BVN itself. It returns whether the information you provided matches what is on file. You need prior approval from Paystack to use this endpoint.
All three endpoints are GET or POST requests to https://api.paystack.co. All require your secret key in the Authorization header. None of them should ever be called from a frontend.
For the full picture of how these fit into the Paystack ecosystem, see the complete Paystack engineering guide.
This is the endpoint you will call when a user enters their bank details for a payout, a withdrawal, or a transfer recipient setup. It confirms that the account number and bank combination map to a real person, and it gives you their name.
// resolve-account.ts
import axios from 'axios';
interface ResolvedAccount {
account_number: string;
account_name: string;
bank_id: number;
}
async function resolveAccountNumber(
accountNumber: string,
bankCode: string
): Promise<ResolvedAccount> {
const response = await axios.get(
'https://api.paystack.co/bank/resolve',
{
params: {
account_number: accountNumber,
bank_code: bankCode,
},
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
return response.data.data;
}
// Usage
const result = await resolveAccountNumber('0001234567', '058');
console.log(result.account_name); // "JOHN DOE OKAFOR"
The bank_code parameter is not the bank's name. It is a numeric code that Paystack assigns. To get the list of bank codes, call GET https://api.paystack.co/bank and cache the result. The list changes rarely.
// fetch-banks.ts
async function fetchBanks(): Promise<Array<{ name: string; code: string }>> {
const response = await axios.get(
'https://api.paystack.co/bank',
{
params: { country: 'nigeria' }, // or 'kenya', 'ghana', 'south_africa'
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
return response.data.data;
}
What the response looks like. On success, you get back a data object with account_number, account_name, and bank_id. The account name is exactly what the bank has on file, usually in uppercase. "JOHN DOE OKAFOR" is a normal response. Your UI should handle uppercase names gracefully.
When it fails. If the account number does not exist at that bank, Paystack returns a 422 with a message like "Could not resolve account name." This is not an error in your code. It means the user typed the wrong number. Show them a clear message and let them try again.
Name matching. The resolved name will not always match what the user typed exactly. "John Okafor" might come back as "OKAFOR JOHN DOE." You need fuzzy matching, not exact string comparison. We cover this in detail later in this guide.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
The card BIN (Bank Identification Number) is the first 6 digits of any card number. It identifies the issuing bank, card brand, and card type. Paystack lets you look up this information without processing a payment.
// resolve-bin.ts
async function resolveCardBin(bin: string) {
const response = await axios.get(
`https://api.paystack.co/decision/bin/${bin}`,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
return response.data.data;
}
// Usage
const binInfo = await resolveCardBin('539923');
// Returns:
// {
// bin: "539923",
// brand: "Mastercard",
// sub_brand: "",
// country_code: "NG",
// country_name: "Nigeria",
// card_type: "DEBIT",
// bank: "Guaranty Trust Bank",
// linked_bank_id: 9
// }
Use cases for BIN resolution:
What BIN resolution does NOT tell you. It does not tell you the cardholder's name, the card's expiry date, or whether the card has sufficient funds. It tells you about the card product, not the specific card. Do not use it as a substitute for transaction verification.
BVN (Bank Verification Number) is Nigeria's biometric identity system for banking customers. Every bank account holder in Nigeria has a BVN. Paystack lets you verify a customer's identity against their BVN record through the Validate Customer endpoint.
Important: you need prior approval from Paystack to use this endpoint. Contact Paystack support or your account manager to get access enabled. It is not available by default on new accounts.
// validate-customer-bvn.ts
async function validateCustomerBVN(
customerCode: string,
bvn: string,
firstName: string,
lastName: string,
accountNumber: string,
bankCode: string
) {
const response = await axios.post(
`https://api.paystack.co/customer/${customerCode}/identification`,
{
type: 'bvn',
value: bvn,
first_name: firstName,
last_name: lastName,
account_number: accountNumber,
bank_code: bankCode,
country: 'NG',
},
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
}
);
return response.data;
}
How BVN verification works. You do not get the BVN data back. Instead, Paystack compares the information you submit (name, account number, bank code) against the BVN record. The result comes back asynchronously via webhook. You will receive a customeridentification.success or customeridentification.failed event.
// webhook-handler.ts (BVN verification result)
app.post('/webhooks/paystack', (req, res) => {
// Verify signature first (see webhook guide)
const event = req.body;
if (event.event === 'customeridentification.success') {
const customerCode = event.data.customer_code;
// Mark user as BVN-verified in your database
await db.users.update({
where: { paystackCustomerCode: customerCode },
data: {
bvnVerified: true,
bvnVerifiedAt: new Date(),
},
});
}
if (event.event === 'customeridentification.failed') {
const customerCode = event.data.customer_code;
// Log the failure reason
console.log('BVN verification failed:', event.data);
// Notify the user to retry or contact support
}
res.sendStatus(200);
});
BVN verification is async. Unlike Resolve Account Number (which returns immediately), BVN verification takes time. Paystack sends you a webhook when it is done. Your UI needs to handle the "verification in progress" state. Show a pending indicator and tell the user you will notify them when verification is complete.
What BVN does NOT work for. BVN is Nigeria-specific. It does not apply to Kenya, Ghana, or South Africa. For Kenya, you would use the national ID number or passport for KYC. Paystack does not currently provide a Kenya national ID verification API. You would need a separate provider for that.
A practical KYC flow combines multiple identity checks in a sequence. Here is a pattern that works for most African fintech applications.
Step 1: Collect bank details. The user enters their bank account number and selects their bank from a dropdown. You populate the dropdown by fetching /bank and caching the list.
Step 2: Resolve the account. Call Resolve Account Number. Display the resolved name back to the user. Ask them: "Is this your name?" If yes, proceed. If no, let them re-enter.
Step 3: Compare names. Compare the resolved bank account name with the name the user entered during registration. This requires fuzzy matching because names come back in different formats.
// name-matching.ts
function normalizeNameForComparison(name: string): string[] {
return name
.toUpperCase()
.replace(/[^A-Z\s]/g, '') // remove non-alpha chars
.split(/\s+/) // split into words
.filter(w => w.length > 1) // drop initials
.sort(); // alphabetical order
}
function namesMatch(
registeredName: string,
bankName: string,
threshold: number = 0.6
): boolean {
const regParts = normalizeNameForComparison(registeredName);
const bankParts = normalizeNameForComparison(bankName);
if (regParts.length === 0 || bankParts.length === 0) return false;
let matchCount = 0;
for (const part of regParts) {
if (bankParts.includes(part)) matchCount++;
}
const score = matchCount / Math.max(regParts.length, bankParts.length);
return score >= threshold;
}
// "John Okafor" vs "OKAFOR JOHN DOE" => match
// "Mary Smith" vs "OKAFOR JOHN DOE" => no match
Step 4 (Nigeria only): BVN verification. If your application requires BVN, submit the verification request and show a "verification in progress" state. Listen for the webhook to update the user's status.
Step 5: Store the verification result. Record what you verified, when you verified it, and the result. Do not store the raw BVN number. Store a boolean bvnVerified flag and a timestamp.
// Database schema for identity verification
// users table
// id UUID PRIMARY KEY
// email TEXT NOT NULL
// full_name TEXT NOT NULL
// bank_code TEXT
// account_number TEXT
// resolved_name TEXT -- name returned by Paystack
// name_match BOOLEAN -- did resolved name match registered name
// bvn_verified BOOLEAN DEFAULT false
// bvn_verified_at TIMESTAMP
// kyc_level INTEGER DEFAULT 0 -- 0=none, 1=bank, 2=bvn
// created_at TIMESTAMP DEFAULT NOW()
KYC tiers. Most applications do not need full verification for every user. Set up tiers:
This tiered approach lets users start using your product immediately while adding friction only when they need features that require identity verification.
Identity checks fail often. Bank systems go down. Users mistype numbers. Names do not match. Your code needs to handle every failure gracefully.
Account resolution failures:
// resolve-with-retry.ts
async function resolveWithRetry(
accountNumber: string,
bankCode: string,
maxRetries: number = 3
): Promise<ResolvedAccount | null> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await resolveAccountNumber(accountNumber, bankCode);
} catch (error: any) {
const status = error.response?.status;
if (status === 422) {
// Account does not exist. Do not retry.
return null;
}
if (status === 429 || status === 503 || !status) {
// Rate limited or bank down. Wait and retry.
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise(r => setTimeout(r, delay));
continue;
}
// Unknown error. Log and throw.
throw error;
}
}
return null; // All retries exhausted
}
Name mismatch failures:
When the resolved name does not match the registered name, do not block the user silently. Show them the resolved name and ask them to confirm. Sometimes the mismatch is legitimate: a married name change, a middle name difference, or a shortened first name.
Log mismatches for manual review. If your risk team sees a pattern of mismatches from one account, that is a fraud signal. If it is a one-off, it is probably just a name formatting difference.
BVN verification failures:
The customeridentification.failed webhook does not always tell you why it failed. Common reasons: the BVN is invalid, the name does not match the BVN record, or the bank account is not linked to that BVN. Tell the user verification failed and ask them to double-check their details. Provide a way to contact support for cases where the data is correct but the verification still fails.
Paystack rate-limits identity endpoints more strictly than payment endpoints. If you call Resolve Account Number for the same account 50 times in an hour, you will get throttled. The solution is caching.
What to cache:
// identity-cache.ts
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function resolveAccountCached(
accountNumber: string,
bankCode: string
): Promise<ResolvedAccount | null> {
const cacheKey = `resolve:${bankCode}:${accountNumber}`;
// Check cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Call Paystack
const result = await resolveWithRetry(accountNumber, bankCode);
if (result) {
// Cache for 7 days
await redis.set(cacheKey, JSON.stringify(result), 'EX', 7 * 24 * 60 * 60);
}
return result;
}
If you do not have Redis, cache in your database. Add a table:
CREATE TABLE identity_cache (
id SERIAL PRIMARY KEY,
cache_type TEXT NOT NULL, -- 'resolve_account', 'resolve_bin'
lookup_key TEXT NOT NULL UNIQUE, -- 'bank_code:account_number'
result JSONB NOT NULL,
cached_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_identity_cache_lookup ON identity_cache(cache_type, lookup_key);
CREATE INDEX idx_identity_cache_expiry ON identity_cache(expires_at);
Run a daily cron job to delete expired rows. This is simpler than Redis and perfectly adequate for most applications.
Rate limit awareness. If you are building a feature where many users verify their bank accounts at the same time (like a bulk onboarding flow), stagger the API calls. Process them in batches of 10 with a 1-second delay between batches. Paystack does not publish exact rate limits for identity endpoints, so be conservative and monitor for 429 responses.
Identity data is personal data. In both Kenya and Nigeria, handling it wrong is not just bad practice. It is illegal.
Kenya Data Protection Act (2019):
Nigeria Data Protection Regulation (NDPR) and the newer Nigeria Data Protection Act (NDPA):
Practical steps for compliance:
// consent-logging.ts
async function logIdentityConsent(
userId: string,
verificationType: 'bank_resolve' | 'bvn' | 'card_bin',
ipAddress: string
) {
await db.consentLogs.create({
userId,
action: `identity_verification_${verificationType}`,
consentText: 'I authorise McTaba to verify my identity using my bank account details for the purpose of account verification and fraud prevention.',
ipAddress,
consentedAt: new Date(),
});
}
bvn_verified: true/false and the timestamp.For the full context of how identity verification fits into the broader Paystack integration, see the complete engineering guide.
If you want to learn how to build production-grade payment integrations from scratch, including KYC, webhooks, and compliance, the McTaba 26-week bootcamp covers the full stack for African developers.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course