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. Paystack Insufficient Funds Response Handling
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Paystack Insufficient Funds Response Handling

Paystack returns "Insufficient Funds" in the gateway_response when the customer's bank reports that the account balance is too low for the transaction. Detect it by checking if data.gateway_response contains "insufficient" (case-insensitive). Never show the raw message to the customer. Instead, use a neutral message like "This payment could not be completed. Please try a different payment method or try again later." Offer bank transfer, USSD, or a different card as alternatives. Never state that the customer lacks funds.

In this article

  1. 1. What the Insufficient Funds Response Looks Like
  2. 2. Detecting Insufficient Funds in Code
  3. 3. Customer Messaging: Do Not Embarrass the Customer
  4. 4. Suggesting Alternative Payment Methods
  5. 5. Complete Verification Handler with Decline Categories
  6. 6. API Endpoint for Your Frontend
  7. 7. Email and Push Notification Best Practices
  8. 8. Verification: Test Your Insufficient Funds Handling

What the Insufficient Funds Response Looks Like

When a customer's bank declines the charge due to insufficient funds, the Paystack verification response looks like this:

{
  "status": true,
  "message": "Verification successful",
  "data": {
    "status": "failed",
    "reference": "txn_abc123def456",
    "amount": 1500000,
    "currency": "NGN",
    "channel": "card",
    "gateway_response": "Insufficient Funds",
    "customer": {
      "email": "customer@example.com"
    },
    "authorization": {
      "bank": "Guaranty Trust Bank",
      "card_type": "mastercard",
      "last4": "7629"
    }
  }
}

The key field is data.gateway_response. When it contains "Insufficient Funds", the bank is saying the account does not have enough money to cover the transaction amount.

You might also receive webhooks with this gateway_response. The charge.failed webhook event includes the same data, so your webhook handler should detect and handle it the same way.

Detecting Insufficient Funds in Code

Check the gateway_response string to identify insufficient funds failures. Use a case-insensitive match because different banks may format the message differently.

// Detect insufficient funds in verification response
function isInsufficientFunds(gatewayResponse) {
  if (!gatewayResponse) return false;
  return gatewayResponse.toLowerCase().includes('insufficient');
}

// Usage in your verification handler
function handleVerification(reference) {
  return fetch(
    'https://api.paystack.co/transaction/verify/' + reference,
    {
      headers: {
        'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  )
  .then(function(res) { return res.json(); })
  .then(function(response) {
    var data = response.data;

    if (data.status === 'success') {
      return { success: true, data: data };
    }

    if (isInsufficientFunds(data.gateway_response)) {
      return {
        success: false,
        reason: 'insufficient_funds',
        userMessage: 'This payment could not be completed. Please try a different payment method or try again later.',
        canRetry: false,
        suggestAlternatives: true,
      };
    }

    return {
      success: false,
      reason: 'other',
      userMessage: 'This payment could not be completed. Please try again.',
      gatewayResponse: data.gateway_response,
      canRetry: true,
    };
  });
}

Notice canRetry is false for insufficient funds. There is no point retrying with the same card immediately. The customer's balance has not changed. Retrying will fail again and may trigger fraud alerts at the bank.

For webhooks, use the same detection in your webhook handler:

// Webhook handler
app.post('/api/webhooks/paystack', function(req, res) {
  // ... signature verification ...

  var event = req.body;

  if (event.event === 'charge.failed') {
    var gatewayResponse = event.data.gateway_response;

    if (isInsufficientFunds(gatewayResponse)) {
      // Log for analytics, but do not email the customer
      // about insufficient funds
      logDecline(event.data.reference, 'insufficient_funds', gatewayResponse);
    }
  }

  res.sendStatus(200);
});

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Customer Messaging: Do Not Embarrass the Customer

This is the most important section of this guide. How you message a customer about an insufficient funds decline matters more than the code.

Never do this:

  • "Your card was declined due to insufficient funds" - directly states the customer lacks money
  • "You don't have enough money in your account" - even worse
  • "Payment failed: Insufficient Funds" - showing the raw gateway response
  • Sending an email with "Insufficient Funds" in the subject line

Do this instead:

// Good customer-facing messages for insufficient funds

var messages = {
  // On-screen after failed payment
  inline: {
    heading: 'Payment could not be completed',
    body: 'This payment method could not be used for this transaction. Please try a different payment method.',
  },

  // Email notification (if you send one)
  email: {
    subject: 'Complete your purchase',
    body: 'We noticed your recent payment did not go through. You can complete your purchase using a different payment method.',
  },

  // SMS (if you send one)
  sms: 'Your payment for [Product] could not be completed. Visit [link] to try again with a different payment method.',
};

The pattern: acknowledge the failure without stating the reason. The customer knows why the payment failed. They do not need you to spell it out. Especially not in an email that their partner, parent, or colleague might see.

Some specific rules:

  • Never include "insufficient funds" in any customer-facing text, email, or notification
  • Never include the gateway_response in customer-facing text
  • Frame the message around the payment method, not the customer's finances
  • Focus on the next step (try again, use a different method) rather than the failure
  • If you send push notifications about failed payments, keep them vague: "Complete your purchase"

Suggesting Alternative Payment Methods

After an insufficient funds decline, the most helpful thing you can do is make it easy for the customer to pay a different way. The customer might have funds in a different bank account, or they might prefer to use bank transfer or USSD.

// Initialize with alternative payment channels
function offerAlternatives(email, amount) {
  return fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: email,
      amount: amount * 100,
      channels: ['bank_transfer', 'ussd', 'mobile_money', 'bank'],
      callback_url: 'https://yoursite.com/payment/verify',
    }),
  })
  .then(function(res) { return res.json(); })
  .then(function(data) { return data.data.authorization_url; });
}

// Let the customer pick a method on your page
function showPaymentOptions(email, amount) {
  return {
    bankTransfer: {
      label: 'Pay via Bank Transfer',
      description: 'Transfer directly from any bank account',
      action: function() {
        return initializeWithChannel(email, amount, ['bank_transfer']);
      },
    },
    ussd: {
      label: 'Pay with USSD',
      description: 'Dial a code on your phone to pay',
      action: function() {
        return initializeWithChannel(email, amount, ['ussd']);
      },
    },
    differentCard: {
      label: 'Use a Different Card',
      description: 'Try another debit or credit card',
      action: function() {
        return initializeWithChannel(email, amount, ['card']);
      },
    },
  };
}

function initializeWithChannel(email, amount, channels) {
  return fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: email,
      amount: amount * 100,
      channels: channels,
      callback_url: 'https://yoursite.com/payment/verify',
    }),
  })
  .then(function(res) { return res.json(); })
  .then(function(data) { return data.data.authorization_url; });
}

Bank transfer is the best alternative for insufficient funds on a card. The customer can transfer from any bank account, and the payment settles immediately on Paystack.

Complete Verification Handler with Decline Categories

Here is a complete verification handler that categorizes different failure types and returns appropriate responses for each:

// lib/payment-verification.js

var DECLINE_CATEGORIES = {
  insufficient_funds: {
    match: function(gr) { return gr.includes('insufficient'); },
    userMessage: 'This payment could not be completed. Please try a different payment method.',
    canRetry: false,
    suggestAlternatives: true,
    severity: 'info',
  },
  bank_declined: {
    match: function(gr) { return gr.includes('declined') || gr.includes('do not honor'); },
    userMessage: 'Your bank did not approve this payment. Please try again or use a different method.',
    canRetry: true,
    suggestAlternatives: true,
    severity: 'info',
  },
  card_expired: {
    match: function(gr) { return gr.includes('expired'); },
    userMessage: 'The card used has expired. Please use a different card.',
    canRetry: false,
    suggestAlternatives: true,
    severity: 'info',
  },
  invalid_card: {
    match: function(gr) { return gr.includes('invalid card') || gr.includes('invalid account'); },
    userMessage: 'The card details appear to be incorrect. Please check and try again.',
    canRetry: true,
    suggestAlternatives: false,
    severity: 'info',
  },
  timeout: {
    match: function(gr) { return gr.includes('timeout') || gr.includes('timed out'); },
    userMessage: 'The payment timed out. Please try again.',
    canRetry: true,
    suggestAlternatives: false,
    severity: 'warn',
  },
};

function verifyPayment(reference) {
  return fetch(
    'https://api.paystack.co/transaction/verify/' + reference,
    {
      headers: {
        'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  )
  .then(function(res) { return res.json(); })
  .then(function(response) {
    var data = response.data;

    // Success
    if (data.status === 'success') {
      return {
        success: true,
        amount: data.amount / 100,
        reference: data.reference,
        email: data.customer.email,
        channel: data.channel,
      };
    }

    // Failed - categorize the reason
    var gr = (data.gateway_response || '').toLowerCase();
    var categories = Object.entries(DECLINE_CATEGORIES);

    for (var i = 0; i < categories.length; i++) {
      var category = categories[i][0];
      var config = categories[i][1];

      if (config.match(gr)) {
        console.log(
          'Payment declined [' + category + ']: ' +
          data.reference + ' - ' + data.gateway_response
        );

        return {
          success: false,
          reason: category,
          userMessage: config.userMessage,
          canRetry: config.canRetry,
          suggestAlternatives: config.suggestAlternatives,
          reference: data.reference,
        };
      }
    }

    // Uncategorized failure
    console.log(
      'Payment failed [unknown]: ' +
      data.reference + ' - ' + data.gateway_response
    );

    return {
      success: false,
      reason: 'unknown',
      userMessage: 'This payment could not be completed. Please try again.',
      canRetry: true,
      suggestAlternatives: true,
      reference: data.reference,
    };
  });
}

module.exports = { verifyPayment };

This handler returns a structured response your frontend can use directly. The userMessage is safe to display. The canRetry flag tells the frontend whether to show a "Try Again" button. The suggestAlternatives flag tells it whether to show alternative payment methods.

API Endpoint for Your Frontend

Wire the verification handler into an Express endpoint that your frontend calls after payment:

// routes/payments.js
var express = require('express');
var verifyPayment = require('../lib/payment-verification').verifyPayment;
var router = express.Router();

router.get('/api/payments/verify', function(req, res) {
  var reference = req.query.reference;

  if (!reference) {
    return res.status(400).json({ error: 'Reference is required' });
  }

  verifyPayment(reference).then(function(result) {
    if (result.success) {
      // Grant access, update order status, etc.
      updateOrderStatus(reference, 'paid');

      return res.json({
        success: true,
        message: 'Payment confirmed',
        redirectTo: '/dashboard',
      });
    }

    // Payment failed
    return res.json({
      success: false,
      message: result.userMessage,
      canRetry: result.canRetry,
      suggestAlternatives: result.suggestAlternatives,
      retryUrl: result.canRetry ? '/checkout?reference=' + reference : null,
      alternativesUrl: result.suggestAlternatives ? '/checkout?method=alternatives' : null,
    });
  });
});

module.exports = router;
// Frontend: handle the verification response
function handlePaymentResult(reference) {
  fetch('/api/payments/verify?reference=' + reference)
    .then(function(res) { return res.json(); })
    .then(function(data) {
      if (data.success) {
        window.location.href = data.redirectTo;
        return;
      }

      // Show failure UI
      showPaymentFailed({
        message: data.message,
        showRetry: data.canRetry,
        showAlternatives: data.suggestAlternatives,
        retryUrl: data.retryUrl,
        alternativesUrl: data.alternativesUrl,
      });
    });
}

The frontend never sees "Insufficient Funds" or any raw gateway response. It only sees the sanitized message and action buttons.

Email and Push Notification Best Practices

If you send email notifications about failed payments, apply the same principle: never reveal the decline reason.

// Email template for failed payment
function buildFailedPaymentEmail(customerName, productName, checkoutUrl) {
  return {
    subject: 'Complete your ' + productName + ' purchase',
    body: 'Hi ' + customerName + ',\n\n' +
      'We noticed your payment for ' + productName + ' did not go through.\n\n' +
      'You can complete your purchase by clicking the link below:\n' +
      checkoutUrl + '\n\n' +
      'If you need help, reply to this email and we will assist you.\n\n' +
      'Best,\n[Your Company Name]',
  };
}

// What NOT to send:
// Subject: "Payment failed - Insufficient Funds"
// Body: "Your payment of NGN 15,000 was declined because your account
//        does not have sufficient funds..."

Timing matters too. Do not send the email immediately. Wait 5-10 minutes. The customer might retry on their own. Sending a "your payment failed" email seconds after the failure feels aggressive and can embarrass the customer if someone else sees the notification.

Some businesses skip email notifications for insufficient funds entirely and only send them for abandoned carts (where the customer started checkout but never completed it). This avoids the issue entirely.

Verification: Test Your Insufficient Funds Handling

Test the complete flow to make sure your handling is correct:

Step 1: Simulate insufficient funds.

In Paystack test mode, you can simulate different failures using specific test card CVVs. Use the test card 4084 0840 8408 4081 with different CVVs to trigger different responses. Check Paystack's test documentation for the CVV that simulates insufficient funds.

Step 2: Check the on-screen message.

After the simulated failure, verify your UI shows the sanitized message ("This payment could not be completed. Please try a different payment method.") and not "Insufficient Funds".

Step 3: Check the server logs.

// Your logs should show something like:
// Payment declined [insufficient_funds]: txn_abc123 - Insufficient Funds
// The gateway_response is logged server-side for debugging,
// but never sent to the frontend

Step 4: Verify alternative payment methods are offered.

After the decline, your UI should show options for bank transfer, USSD, or a different card. Click each option and verify it opens a working Paystack checkout with the correct channel.

Step 5: Verify emails (if you send them).

Check the email that is sent after a failed payment. It should not contain "insufficient funds", "declined", or any gateway response text. It should focus on helping the customer complete the purchase.

Step 6: Check your analytics.

Verify the decline is logged in your analytics or monitoring system with the correct category ("insufficient_funds"). This data helps you understand your decline rate and take action if a specific bank or card type is consistently failing.

Key Takeaways

  • ✓Paystack returns "Insufficient Funds" in gateway_response when the customer's bank reports the account balance is too low. This is a bank decision, not a Paystack error.
  • ✓Never show "Insufficient Funds" directly to the customer. It is embarrassing and unnecessary. Use a neutral message that does not reveal the reason.
  • ✓Detect insufficient funds by checking gateway_response.toLowerCase().includes("insufficient"). The exact wording may vary slightly between banks.
  • ✓Offer alternative payment methods after an insufficient funds decline. Bank transfer lets the customer pay from a different account. USSD works for customers who might have funds in a different linked account.
  • ✓Do not automatically retry insufficient funds transactions. The balance has not changed. Retrying will fail again and may trigger fraud detection at the bank.
  • ✓Log the gateway_response for your support and analytics, but keep it out of customer-facing messages and notifications.
  • ✓Consider offering installment or partial payment options if your business model supports it. This turns a failed transaction into a completed one.

Frequently Asked Questions

How do I detect insufficient funds in the Paystack response?
Check the data.gateway_response field in the verification response. If it contains "Insufficient Funds" (or "insufficient" in any casing), the bank declined the transaction due to low balance. Use gateway_response.toLowerCase().includes("insufficient") for reliable detection.
Should I tell the customer their payment failed due to insufficient funds?
No. Never reveal the specific decline reason to the customer. Use a neutral message like "This payment could not be completed" and suggest alternative payment methods. The customer already knows why it failed. Stating it explicitly is embarrassing, especially in shared environments or email notifications that others might see.
Should I automatically retry an insufficient funds transaction?
No. The customer's account balance has not changed between the first attempt and a retry. An automatic retry will fail again and may trigger the bank's fraud detection system, which could lock the customer's card. Let the customer decide when and how to retry.
Can I offer partial payment when a customer has insufficient funds?
Paystack does not support partial payments on a single transaction. However, you could build a split-payment flow where the customer pays a portion now and the rest later, using separate Paystack transactions. This depends on your business model and terms of service.
Is the insufficient funds gateway_response always the same string?
Not exactly. Most banks return "Insufficient Funds" but some may return variations like "INSUFFICIENT FUNDS", "Insufficient funds", or "Insufficient balance". Use a case-insensitive includes() check rather than an exact string match to handle all variations.

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 Errors and Troubleshooting: The Complete Reference

Every Paystack error you will hit in production, what causes it, and how to fix it. Authentication failures, webhook signature problems, transaction errors, transfer issues, and frontend bugs. Code-first solutions for each one.

Read more

Paystack "Declined by Financial Institution" Error

The "Declined by financial institution" response means the customer's bank rejected the transaction. This is not a Paystack error or a bug in your code. The bank made the decision, and neither you nor Paystack can override it.

Read more

Reading Paystack Gateway Response Codes

When a Paystack transaction fails, the gateway_response field contains the bank's reason. "Declined" is not helpful to a developer or a customer. This guide decodes every common response, explains the actual cause, and gives you the exact customer-facing message to show for each one.

Read more

Paystack Callback URL Not Redirecting

Your customer pays but is not redirected back to your site. This happens when callback_url is missing, you are using popup mode instead of redirect mode, or the browser is blocking the redirect.

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.