The gateway_response field in a Paystack transaction response contains the acquiring bank's reason for approving or declining a payment. Common values include "Approved" (success), "Declined" (generic refusal), "Insufficient Funds" (card has no money), "Card Expired" (past expiry date), "Invalid PIN" (wrong PIN entered), and "Do Not Honor" (bank refused without reason). Map each response to a customer-friendly message in your error handling code so users know whether to retry, use a different card, or contact their bank.
When your customer pays through Paystack, the payment request travels from Paystack to an acquiring bank, then to the customer's issuing bank (the bank that issued their card). The issuing bank makes the decision to approve or decline the transaction and sends back a response code. Paystack translates this code into a human-readable string and places it in the gateway_response field of the transaction data.
You will find this field in two places: the response from GET /transaction/verify/:reference and the payload of charge.success or charge.failed webhook events. For successful transactions, the value is typically "Approved" or "Successful." For failed transactions, it contains the reason for failure.
// Example: checking gateway_response after verification
const response = await fetch(
`https://api.paystack.co/transaction/verify/${reference}`,
{ headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` } }
);
const { data } = await response.json();
if (data.status === 'failed') {
console.log('Failure reason:', data.gateway_response);
// Output: "Insufficient Funds" or "Declined" or "Do Not Honor" etc.
}
The gateway_response is the bank's voice in your transaction. Learning to read it properly is the difference between telling a customer "payment failed" (useless) and telling them "your card does not have enough funds, please try another card" (actionable).
Here are the gateway responses you will encounter most frequently, what causes them, and what to tell the customer.
| Gateway Response | Cause | Customer Message | Retryable? |
|---|---|---|---|
| Approved / Successful | Bank approved the charge. | N/A (payment succeeded) | N/A |
| Insufficient Funds | Card balance is too low for the transaction amount. | "Your card does not have enough funds. Please try a different card or fund your account." | Yes, after funding |
| Declined | Generic decline. Bank did not give a specific reason. | "Your bank declined this payment. Please try a different card or contact your bank." | Maybe, with different card |
| Do Not Honor | Bank refused the transaction. Could be fraud suspicion, spending limit, or internal policy. | "Your bank did not approve this payment. Please contact your bank or try a different card." | No, unless bank resolves it |
| Invalid PIN | Customer entered wrong PIN during checkout. | "The PIN you entered is incorrect. Please try again with the correct PIN." | Yes, immediately |
| Expired Card | Card expiry date has passed. | "This card has expired. Please use a card with a valid expiry date." | No |
| Card Not Enrolled | Card is not enrolled for 3D Secure / online transactions. | "This card is not enabled for online payments. Contact your bank to enable it, or use a different card." | No, unless bank enables it |
| Transaction Not Permitted | Card type or account type cannot process this transaction. | "Your bank does not allow this type of transaction on this card. Please try a different card." | No |
| Exceeds Withdrawal Limit | Transaction amount exceeds the card's daily or per-transaction limit. | "This amount exceeds your card's transaction limit. Try a smaller amount or contact your bank to raise the limit." | Yes, with lower amount or after limit reset |
| Invalid Transaction | Something about the transaction data was rejected by the bank. | "This payment could not be processed. Please try again or use a different payment method." | Maybe |
| Bank System Error / System Malfunction | The bank's system is temporarily unavailable. | "Your bank's system is temporarily unavailable. Please try again in a few minutes." | Yes, after waiting |
| Restricted Card | Card has been blocked by the bank, possibly due to reported fraud or account issues. | "This card cannot process payments at this time. Please contact your bank or use a different card." | No |
This is not an exhaustive list. Banks occasionally send unusual response codes, and Paystack may relay them as-is. If you encounter a gateway_response not listed here, the safest customer message is "Your bank could not process this payment. Please try a different card or contact your bank."
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Build a mapping function that translates raw gateway responses into customer-friendly messages. This keeps your error handling consistent across your application.
// gateway-messages.ts
const GATEWAY_MESSAGES = {
'Insufficient Funds': {
customer: 'Your card does not have enough funds. Please try a different card or fund your account.',
retryable: true,
category: 'customer-fixable',
},
'Declined': {
customer: 'Your bank declined this payment. Please try a different card or contact your bank.',
retryable: false,
category: 'bank-decision',
},
'Do Not Honor': {
customer: 'Your bank did not approve this payment. Please contact your bank or try a different card.',
retryable: false,
category: 'bank-decision',
},
'Invalid PIN': {
customer: 'The PIN you entered is incorrect. Please try again with the correct PIN.',
retryable: true,
category: 'customer-fixable',
},
'Expired Card': {
customer: 'This card has expired. Please use a card with a valid expiry date.',
retryable: false,
category: 'card-issue',
},
'Card Not Enrolled': {
customer: 'This card is not enabled for online payments. Contact your bank to enable it, or use a different card.',
retryable: false,
category: 'card-issue',
},
'Transaction Not Permitted': {
customer: 'Your bank does not allow this type of transaction on this card. Please try a different card.',
retryable: false,
category: 'card-issue',
},
'Exceeds Withdrawal Limit': {
customer: 'This amount exceeds your card transaction limit. Try a smaller amount or contact your bank.',
retryable: true,
category: 'customer-fixable',
},
'Bank System Error': {
customer: 'Your bank system is temporarily unavailable. Please try again in a few minutes.',
retryable: true,
category: 'transient',
},
'System Malfunction': {
customer: 'Your bank system is temporarily unavailable. Please try again in a few minutes.',
retryable: true,
category: 'transient',
},
'Restricted Card': {
customer: 'This card cannot process payments at this time. Please contact your bank or use a different card.',
retryable: false,
category: 'card-issue',
},
};
const DEFAULT_MESSAGE = {
customer: 'Your bank could not process this payment. Please try a different card or contact your bank.',
retryable: false,
category: 'unknown',
};
export function getGatewayMessage(gatewayResponse) {
if (!gatewayResponse) return DEFAULT_MESSAGE;
return GATEWAY_MESSAGES[gatewayResponse] || DEFAULT_MESSAGE;
}
// Usage in your payment callback handler
const message = getGatewayMessage(transaction.gateway_response);
if (message.retryable) {
// Show retry button alongside the message
showPaymentError(message.customer, { showRetry: true });
} else {
// Show message with "use different card" option
showPaymentError(message.customer, { showAlternatePayment: true });
}
The category field helps your analytics. You can track what percentage of failures are customer-fixable (they entered a wrong PIN or have no funds) versus card issues (expired, blocked) versus transient bank errors. This data tells you whether your payment failure rate is something you can influence or not.
Gateway responses vary significantly by country and bank. Understanding these patterns helps you build better error handling for your specific market.
Nigeria. "Do Not Honor" is extremely common from Nigerian banks, often triggered by spending limits that customers do not realize exist. GTBank, First Bank, and Access Bank each have slightly different limit thresholds. "Invalid PIN" is frequent because Nigerian card payments require PIN entry, and customers mistype it more often than you would expect. "Card Not Enrolled" appears when customers try to use cards that have not been enrolled for online payments through their banking app.
Ghana. Mobile money transactions through Paystack do not produce traditional gateway responses. Instead, the status field indicates whether the MoMo charge was successful, pending, or failed. Card payments in Ghana produce similar responses to Nigeria, but "Exceeds Withdrawal Limit" is more common due to lower default limits on Ghanaian cards.
South Africa. 3D Secure failures are more common because South African banks have stricter 3DS requirements. "Transaction Not Permitted" may appear for cross-currency transactions if a card is not enabled for international purchases.
Kenya. Paystack card payments in Kenya produce standard gateway responses. M-Pesa payments through Paystack use the mobile money flow and have their own status patterns (pending, success, failed) rather than traditional gateway response codes. If you support both card and M-Pesa, you need separate error handling paths.
Store the gateway_response for every transaction, not just failed ones. Over time, this data reveals actionable patterns.
// In your webhook handler or verification callback
await db.query(
`INSERT INTO transactions (reference, status, gateway_response, amount, currency, channel, created_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())`,
[
transaction.reference,
transaction.status,
transaction.gateway_response,
transaction.amount,
transaction.currency,
transaction.channel,
]
);
// Later: query failure patterns
// "What are the top 5 failure reasons this month?"
const failureReport = await db.query(`
SELECT gateway_response, COUNT(*) as count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1) as percentage
FROM transactions
WHERE status = 'failed'
AND created_at >= DATE_TRUNC('month', CURRENT_DATE)
GROUP BY gateway_response
ORDER BY count DESC
LIMIT 5
`);
If "Insufficient Funds" dominates your failures, consider offering installment payments or a pay-later option. If "Card Not Enrolled" is high, add a checkout message guiding customers to enroll their card through their banking app. If "Bank System Error" spikes at certain hours, consider suggesting alternative payment channels during those windows.
This data also helps you set realistic expectations. A 5% card failure rate due to "Do Not Honor" and "Insufficient Funds" is normal for Nigerian card payments. Trying to optimize below that is wasted effort because those failures are outside your control.
"Do Not Honor" deserves its own section because it is the most frustrating gateway response for both developers and customers. The bank has decided not to process the transaction but will not say why.
Possible hidden reasons include: the customer's daily spending limit is reached, the bank's fraud detection flagged the transaction, the card is restricted for online use, the merchant category code (MCC) is blocked, or the bank simply has an internal policy that triggered. You cannot know which reason applies, and neither can Paystack.
What you can do:
Track "Do Not Honor" rates by bank. If one bank produces this response significantly more than others, it may indicate a configuration issue with your Paystack account's MCC or a bank-level policy change. Paystack support can sometimes investigate if you provide specific transaction references.
Test your error mapping by creating scenarios that produce different gateway responses. Paystack's test mode does not simulate all real-world gateway responses, but you can verify your mapping logic independently.
getGatewayMessage function and verify it returns the correct customer message, retryable flag, and category.null, undefined, and an empty string. Your function should handle all three without crashing.// Example unit tests
describe('getGatewayMessage', () => {
it('returns correct message for Insufficient Funds', () => {
const result = getGatewayMessage('Insufficient Funds');
expect(result.customer).toContain('enough funds');
expect(result.retryable).toBe(true);
expect(result.category).toBe('customer-fixable');
});
it('returns default for unknown response', () => {
const result = getGatewayMessage('Some Weird Bank Error');
expect(result.customer).toContain('could not process');
expect(result.retryable).toBe(false);
});
it('handles null gracefully', () => {
const result = getGatewayMessage(null);
expect(result.customer).toBeDefined();
});
});
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