McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Handling Identity Verification Failures Gracefully
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Handling Identity Verification Failures Gracefully

Catch every error from Paystack identity APIs and map it to a user-friendly response. For timeouts and 5xx errors, retry with exponential backoff up to 3 times, then tell the user verification is temporarily unavailable. For 422 errors (account not found), show a clear message asking the user to re-check their details. For rate limit errors, queue the request and try later. Never show raw API errors to users, and never block a user permanently because of a temporary API failure.

In this article

  1. 1. Categories of Identity API Failures
  2. 2. Mapping API Errors to User Messages
  3. 3. Retry Logic with Exponential Backoff
  4. 4. Building a Degraded Mode
  5. 5. Guiding Users Through Failures
  6. 6. Monitoring Verification Failures
  7. 7. Testing Your Failure Handling

Categories of Identity API Failures

Every failure from Paystack identity APIs falls into one of four categories. Each needs a different response.

User errors (4xx except 429). The user sent bad data. Wrong account number, invalid BVN format, missing required fields. The fix is to help the user correct their input.

Rate limits (429). You are calling the API too fast. The fix is to slow down, queue the request, and retry after a delay.

Upstream failures (5xx, timeouts). Paystack or the bank system is having issues. The fix is to retry with backoff, then gracefully degrade if retries fail.

Authentication failures (401). Your API key is wrong or missing. This is a configuration bug, not a user problem. The fix is to check your environment variables.

Treating all failures the same way leads to bad UX. A user who mistyped their account number gets the same "try again later" message as a user who hit a server outage. Handle each category specifically.

Mapping API Errors to User Messages

// error-mapper.js
function mapIdentityError(error) {
  if (!error.response) {
    // Network error or timeout
    if (error.code === 'ECONNABORTED') {
      return {
        userMessage: 'Bank verification is taking longer than usual. Please try again.',
        action: 'retry',
        severity: 'warning',
      };
    }
    return {
      userMessage: 'Could not reach the verification service. Check your internet and try again.',
      action: 'retry',
      severity: 'error',
    };
  }

  var status = error.response.status;

  if (status === 401) {
    return {
      userMessage: 'Verification is temporarily unavailable. Our team has been notified.',
      action: 'alert_team',
      severity: 'critical',
    };
  }

  if (status === 422) {
    var message = error.response.data.message || '';
    if (message.toLowerCase().indexOf('account') !== -1) {
      return {
        userMessage: 'We could not find an account with that number at this bank. Please double-check your account number and bank selection.',
        action: 'user_correction',
        severity: 'info',
      };
    }
    return {
      userMessage: 'The information provided could not be verified. Please check your details and try again.',
      action: 'user_correction',
      severity: 'info',
    };
  }

  if (status === 429) {
    return {
      userMessage: 'We are processing many verification requests. Yours will be completed shortly.',
      action: 'queue',
      severity: 'warning',
    };
  }

  if (status >= 500) {
    return {
      userMessage: 'Bank verification is temporarily unavailable. Please try again in a few minutes.',
      action: 'retry',
      severity: 'warning',
    };
  }

  return {
    userMessage: 'Something went wrong with verification. Please try again.',
    action: 'retry',
    severity: 'error',
  };
}

Never expose raw errors. "Request failed with status code 422" means nothing to a user. "We could not find an account with that number at this bank" tells them exactly what happened and what to do.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Retry Logic with Exponential Backoff

For timeouts and server errors, retry automatically before bothering the user.

// retry-resolve.js
const axios = require('axios');

async function resolveWithRetry(accountNumber, bankCode, maxRetries) {
  maxRetries = maxRetries || 3;
  var lastError;

  for (var attempt = 0; attempt < maxRetries; attempt++) {
    try {
      var 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 },
          timeout: 15000,
        }
      );
      return { success: true, data: response.data.data };
    } catch (error) {
      lastError = error;

      // Do not retry user errors
      if (error.response && error.response.status >= 400 && error.response.status < 500) {
        break;
      }

      // Wait before retrying: 1s, 2s, 4s
      if (attempt < maxRetries - 1) {
        var delay = Math.pow(2, attempt) * 1000;
        await new Promise(function(resolve) {
          setTimeout(resolve, delay);
        });
      }
    }
  }

  return { success: false, error: mapIdentityError(lastError) };
}

Only retry server errors and timeouts. Never retry a 422 (account not found) or 401 (bad key). Those will fail the same way every time.

Cap retries at 3. Three retries with exponential backoff (1s, 2s, 4s) means a maximum wait of 7 seconds plus the request time. Beyond that, the user should be told to try again later.

Set a reasonable timeout. 15 seconds per request is reasonable for African bank systems, which can be slow. Too short and you get false timeouts. Too long and the user stares at a spinner.

Building a Degraded Mode

When identity APIs are completely down, your application needs a plan. The worst option is to block everything.

Option 1: Save and verify later. Let the user submit their bank details. Store them as "pending verification." Run the verification in a background job when the API comes back. Notify the user when verification completes.

// deferred-verify.js
async function saveForLaterVerification(userId, accountNumber, bankCode) {
  await db.query(
    'INSERT INTO pending_verifications ' +
    '(user_id, account_number, bank_code, status, created_at) ' +
    'VALUES ($1, $2, $3, $4, NOW())',
    [userId, accountNumber, bankCode, 'pending']
  );

  return {
    message: 'Your bank details have been saved. We will verify them shortly and notify you.',
  };
}

// Background job - runs every 5 minutes
async function processUnverifiedAccounts() {
  var pending = await db.query(
    'SELECT * FROM pending_verifications WHERE status = $1 ORDER BY created_at ASC LIMIT 10',
    ['pending']
  );

  for (var i = 0; i < pending.rows.length; i++) {
    var record = pending.rows[i];
    var result = await resolveWithRetry(record.account_number, record.bank_code, 2);

    if (result.success) {
      await db.query(
        'UPDATE pending_verifications SET status = $1, resolved_name = $2 WHERE id = $3',
        ['verified', result.data.account_name, record.id]
      );
      // Notify user
    } else {
      await db.query(
        'UPDATE pending_verifications SET status = $1, retry_count = retry_count + 1 WHERE id = $2',
        ['retry_scheduled', record.id]
      );
    }
  }
}

Option 2: Allow low-value payouts without verification. If the API is down and the user has a strong transaction history, consider allowing payouts below a conservative threshold. This keeps your product functional while managing risk.

Option 3: Show estimated downtime. If you know the API is experiencing widespread issues (check the Paystack status page), tell users: "Bank verification is currently experiencing delays. We expect it to be back within [timeframe]."

Guiding Users Through Failures

How you communicate failures determines whether users retry or abandon your product.

Be specific about what went wrong. "Account not found at this bank" is better than "Verification failed." "Bank system is temporarily busy" is better than "Error."

Tell them what to do next. Every error message should include an action: "Please check your account number and try again." "Try again in a few minutes." "Contact support if this keeps happening."

Show what they entered. When verification fails, show the user their submitted details (account number and bank name) so they can spot typos immediately. A user who sees "Account: 001234567 at GTBank" might instantly realize they meant to type 0001234567.

Preserve their input. Never clear the form on failure. If a user typed 10 digits and selected a bank, keep that data in the form so they can edit rather than re-enter everything.

Offer alternatives. If account resolution keeps failing for a specific bank, suggest: "Having trouble with [bank name]? Some banks experience intermittent verification delays. You can try again later or use a different bank account."

Monitoring Verification Failures

Track failure rates to spot problems before they affect many users.

// monitor.js
async function logVerificationAttempt(endpoint, status, duration, errorCode) {
  await db.query(
    'INSERT INTO verification_metrics ' +
    '(endpoint, status, duration_ms, error_code, created_at) ' +
    'VALUES ($1, $2, $3, $4, NOW())',
    [endpoint, status, duration, errorCode]
  );
}

// Check failure rate every 5 minutes
async function checkFailureRate() {
  var result = await db.query(
    'SELECT ' +
    '  COUNT(*) FILTER (WHERE status = 'error') as failures, ' +
    '  COUNT(*) as total ' +
    'FROM verification_metrics ' +
    'WHERE created_at > NOW() - INTERVAL '5 minutes''
  );

  var row = result.rows[0];
  var failureRate = row.total > 0 ? row.failures / row.total : 0;

  if (failureRate > 0.5 && row.total > 10) {
    // More than 50% failures with meaningful volume - alert the team
    await sendAlert('Identity API failure rate is ' + Math.round(failureRate * 100) + '%');
  }
}

Alert thresholds: A failure rate above 50% with at least 10 requests in the window means something is wrong upstream. A failure rate above 90% means the API is effectively down. Adjust thresholds based on your traffic volume.

Track by bank. Sometimes a single bank's system goes down while others work fine. Tracking failures by bank code helps you tell users "Verification for [bank name] is temporarily unavailable" rather than a generic error.

Testing Your Failure Handling

Most developers test the happy path (verification succeeds) but not the failure paths. Test every error scenario.

Simulate timeouts: Set your HTTP client timeout to 1ms temporarily and verify your retry logic kicks in.

Simulate 422: Send a deliberately wrong account number (like "0000000000") and verify the user sees a helpful message.

Simulate 429: Send many rapid requests and verify your rate limit handling queues rather than crashes.

Simulate network failure: Point the API URL to a non-existent host and verify your network error handling works.

Simulate degraded mode: Disable all API calls and verify your "save and verify later" flow works end to end.

For a comprehensive testing strategy, see the complete testing guide.

Key Takeaways

  • ✓Map every API error to a specific user-facing message. Users should never see raw error codes or stack traces.
  • ✓Timeouts are the most common failure. Set a 15-second timeout on identity API calls and retry up to 3 times with exponential backoff.
  • ✓A 422 from Resolve Account means the account does not exist at that bank. This is user error, not a system failure. Guide the user to re-check their details.
  • ✓Rate limit errors (429) mean you are calling too fast. Queue the request and try again after the rate limit window resets.
  • ✓Build a degraded mode: if identity APIs are completely down, let users save their details and verify later. Do not block them from all product functionality.
  • ✓Log every failure with context (endpoint, error code, user ID, timestamp) for debugging and monitoring.

Frequently Asked Questions

Should I show a loading spinner or a progress message during verification?
Show a spinner with a message: "Verifying your bank account..." If it takes more than 5 seconds, update the message: "This is taking a bit longer than usual. Please wait..." If it fails after retries, replace the spinner with a clear error and a retry button.
What if the same user keeps getting verification failures?
After 3 consecutive failures from the same user for the same account details, suggest they contact support. They may have incorrect details they are not aware of, or there may be an issue with their specific bank. Do not lock them out, but do escalate to human assistance.
Should I cache failed verifications?
Cache 422 results (account not found) for a short period (5 minutes) to avoid hammering the API with the same bad input. Do not cache timeout or 5xx failures, because those are temporary and the next attempt may succeed.
How do I test identity API failures in a staging environment?
Use test keys, which return predictable responses. For error scenarios, mock the Paystack API with a test server that returns specific error codes on demand. Tools like nock (Node.js) or WireMock let you simulate any HTTP response.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Identity Verification: Complete Guide

Paystack gives you three identity endpoints: resolve account number, resolve card BIN, and validate customer (BVN). This guide shows you how to use each one, build a KYC flow around them, and stay within the Kenya DPA and Nigeria NDPR while doing it.

Read more

Validating a Customer Before Payout

Every payout you send is money you cannot get back. Validating the recipient before initiating a transfer is not optional. This guide shows you how to chain Paystack identity APIs into a validation gate that catches wrong accounts, name mismatches, and suspicious requests before a single kobo leaves your account.

Read more

Building a KYC Flow on Paystack Identity APIs

A KYC flow is not a single API call. It is a sequence of verification steps that increase in strength as the user needs more from your product. This guide shows you how to chain Paystack identity APIs into a progressive KYC system with tiers, database tracking, and clear UX at every step.

Read more

Rate Limits and Caching for Identity Lookups

Paystack rate-limits identity endpoints to prevent abuse. If you call Resolve Account Number on every page load or every form submission, you will hit the limit and your verifications will fail. This guide shows you how to cache results properly, throttle requests, and build an identity lookup layer that stays fast even under heavy traffic.

Read more

Paystack API Keys: Public, Secret, Test and Live

Paystack gives you four keys: a test public key, a test secret key, a live public key, and a live secret key. Each has a specific role and a specific place in your codebase. Mixing them up is one of the most common mistakes developers make during integration.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.