Bonaventure OgetoBy Bonaventure Ogeto|

Paystack Identity Verification: Complete Guide

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 Identity APIs: What You Get

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.

Resolve Account Number: Code and Walkthrough

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.

Resolve Card BIN: What It Tells You

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:

  • Card logo display. Show the correct Visa, Mastercard, or Verve logo as the user types their card number. You only need the first 6 digits.
  • Fraud screening. If a Nigerian user claims to be in Lagos but their card BIN shows a South African bank, that is a signal worth investigating.
  • Card type routing. Some businesses charge differently for credit vs. debit cards. BIN resolution tells you which type it is before the transaction.
  • Prepaid card blocking. If your business does not accept prepaid cards (common for subscription services), BIN resolution lets you detect them early.

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 Verification via Validate Customer

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.

Building a KYC Flow with Paystack Identity APIs

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:

  • Tier 0 (email only): Can browse, purchase small items. No payouts.
  • Tier 1 (bank account resolved): Can receive payouts up to a daily limit.
  • Tier 2 (BVN verified): Full access, higher limits, can be a transfer recipient.

This tiered approach lets users start using your product immediately while adding friction only when they need features that require identity verification.

Handling Identity Verification Failures

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:

  • 422 "Could not resolve account name": The account number does not exist at that bank. The user typed wrong. Show: "We could not find an account with that number at [bank name]. Please check and try again."
  • 503 or timeout: The bank's system is down. This happens regularly with some banks. Show: "We could not reach [bank name] right now. Please try again in a few minutes." Offer a retry button. Do not ask the user to re-enter everything.
  • Rate limit (429): You are calling the endpoint too fast. Queue the request and retry with exponential backoff.
// 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.

Caching and Rate Limits

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:

  • Bank list: Cache for 24 hours. Banks do not appear or disappear daily.
  • Resolved account names: Cache for 7 to 30 days. A bank account name changes only if the holder legally changes their name, which is rare.
  • Card BIN data: Cache for 30 days. BIN assignments change very rarely.
  • BVN verification results: Cache indefinitely. Once verified, store the boolean result. Do not re-verify unless the user explicitly requests it.
// 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.

Data Protection: Kenya DPA and Nigeria NDPR

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):

  • Lawful basis: You need a lawful reason to collect identity data. For KYC, the lawful basis is usually "compliance with a legal obligation" (anti-money laundering rules) or "legitimate interest" (fraud prevention). State this clearly in your privacy notice.
  • Data minimisation: Collect only what you need. If you only need to verify a bank account name, do not also collect the user's ID number, date of birth, and mother's maiden name "just in case."
  • Purpose limitation: If you collected bank details for KYC, you cannot use them for marketing analytics. The data must be used only for the stated purpose.
  • Storage limitation: Do not keep identity data forever. Set a retention period. If a user has not been active for 2 years, purge their identity data.
  • Consent: Get explicit consent before running identity checks. A checkbox that says "I agree to identity verification" is the minimum. Store the timestamp of consent.

Nigeria Data Protection Regulation (NDPR) and the newer Nigeria Data Protection Act (NDPA):

  • Similar principles: Lawful basis, data minimisation, purpose limitation, and storage limitation all apply.
  • BVN-specific rules: BVN data is classified as sensitive personal data. It must be encrypted at rest and in transit. You must not store the raw BVN number. Store a boolean "verified" flag instead.
  • Data Protection Impact Assessment (DPIA): If your product processes identity data at scale (more than a few hundred users), you should conduct a DPIA. This is a formal assessment of the risks of your data processing and the measures you have put in place to reduce them.
  • Breach notification: If identity data is compromised, you must notify the Nigeria Data Protection Bureau within 72 hours.

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(),
  });
}
  • Never store raw BVN numbers. Store bvn_verified: true/false and the timestamp.
  • Encrypt bank account numbers at rest. Use your database's column-level encryption or application-level encryption.
  • Log every identity API call: who requested it, when, and the result. This gives you an audit trail.
  • Add identity data to your data deletion workflow. When a user requests account deletion, delete their bank details, resolved names, and verification records.
  • Review your identity data handling every 6 months. Laws change. Your data practices should keep up.

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.

Key Takeaways

  • Paystack offers three identity APIs: Resolve Account Number, Resolve Card BIN, and Validate Customer (BVN). Each serves a different purpose in your KYC flow.
  • Resolve Account Number is the most commonly used. It takes a bank code and account number and returns the account holder name. Use it to confirm a user owns the bank account they claim before initiating transfers.
  • BVN verification through the Validate Customer endpoint requires prior approval from Paystack. It returns a match/mismatch result, not the raw BVN data itself.
  • Cache identity lookup results aggressively. Account names do not change often, and Paystack rate-limits these endpoints. Store the result in your database with a timestamp and re-verify only when needed.
  • Both the Kenya Data Protection Act and Nigeria NDPR require you to collect only what you need, store it securely, and tell the user what you are doing with their data. Log consent before calling any identity API.

Frequently Asked Questions

Does Paystack Resolve Account Number work for Kenya?
Yes. You can resolve bank account numbers for banks in Nigeria, Ghana, South Africa, and Kenya. Pass the country parameter when fetching the bank list, then use the returned bank codes with the resolve endpoint. The endpoint returns the account holder name registered with the bank.
Can I use Paystack for BVN verification outside Nigeria?
No. BVN is a Nigerian banking identity system. It does not apply to Kenya, Ghana, or South Africa. For KYC in other countries, you would need a separate identity verification provider for national ID or passport verification. Paystack only provides BVN verification through the Validate Customer endpoint.
Why does the resolved account name not match the name my user entered?
Banks store names in their own format, usually uppercase and sometimes in a different order (last name first). "John Okafor" might come back as "OKAFOR JOHN DOE." Use fuzzy name matching instead of exact string comparison. Normalize both names to uppercase, split into parts, sort alphabetically, and compare the overlap.
Is BVN verification instant or async?
It is asynchronous. When you call the Validate Customer endpoint, Paystack returns a 200 response confirming it received the request. The actual verification result arrives later via webhook, either as a customeridentification.success or customeridentification.failed event. Your app needs to handle the pending state and update the user when the result arrives.
Do I need to store the raw BVN number in my database?
No, and you should not. Under Nigeria NDPR, BVN is sensitive personal data. Store only a boolean flag (bvn_verified: true/false) and the timestamp of verification. If you need to re-verify, ask the user to provide their BVN again. Never log, display, or persist the raw BVN value.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon