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.
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);
});
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.
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