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