The gateway_response field in a Paystack transaction object contains the raw message from the acquiring bank or payment processor. Common values include "Approved," "Declined," "Insufficient Funds," "Card Expired," and "Transaction Timeout." Use this field to determine why a payment failed and to show helpful, actionable messages to your customers instead of generic error text.
When you verify a transaction by calling GET /transaction/verify/{reference}, the response contains a data object with all the transaction details. Inside that object is a field called gateway_response. This is the message the bank or payment processor sent back to Paystack about the transaction outcome.
// Verifying a transaction and reading the gateway response
async function verifyAndReadResponse(reference) {
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
var data = await response.json();
var txn = data.data;
console.log('Status: ' + txn.status);
console.log('Gateway Response: ' + txn.gateway_response);
console.log('Channel: ' + txn.channel);
console.log('Amount: ' + txn.amount);
return {
status: txn.status,
gatewayResponse: txn.gateway_response,
channel: txn.channel,
};
}
The gateway_response also appears in webhook payloads for charge.success events. When a successful payment triggers a webhook, the transaction data inside the event includes the gateway_response. For successful transactions it is typically "Approved" or "Successful." For failed transactions (which you see when verifying after a redirect or polling), it describes the failure.
Do not confuse gateway_response with the top-level status field. The status tells you whether the transaction succeeded or failed. The gateway_response tells you why. A status of "failed" paired with a gateway_response of "Insufficient Funds" tells you the card did not have enough balance. A status of "failed" with "Transaction Timeout" tells you the bank did not respond in time.
Here are the gateway responses you will encounter most often, grouped by category.
| Gateway Response | What It Means |
|---|---|
| Approved | The bank approved the transaction. Everything went through. |
| Successful | Same as Approved. Some processors use this wording instead. |
| Gateway Response | What It Means | Retryable? |
|---|---|---|
| Declined | The bank declined the transaction without a specific reason. Often triggered by fraud rules on the bank side. | No (try different card) |
| Insufficient Funds | The card does not have enough balance to cover the charge. | Yes (after funding) |
| Card Expired | The card's expiration date has passed. The customer needs a new card. | No (need new card) |
| Invalid Card Number | The card number is wrong. Could be a typo or a non-existent card. | No (correct and retry) |
| Invalid Expiry | The expiry date entered does not match the card records. | No (correct and retry) |
| Invalid CVV | The CVV/CVC code entered does not match. | No (correct and retry) |
| Card Not Enrolled | The card is not enrolled for 3D Secure / online transactions. Common with some debit cards. | No (customer must contact bank) |
| Restricted Card | The card has been restricted by the issuing bank. Could be due to fraud flags or account holds. | No (customer must contact bank) |
| Card Stolen | The card has been reported stolen. Do not retry. | No (do not retry) |
| Gateway Response | What It Means | Retryable? |
|---|---|---|
| Transaction Timeout | The bank did not respond within the expected time. Network issues or bank system overload. | Yes (wait and retry) |
| System Malfunction | The bank's system encountered an internal error. | Yes (wait and retry) |
| Transaction Not Permitted | The bank does not allow this type of transaction for the card or account. | No (customer must contact bank) |
| Exceeds Withdrawal Limit | The transaction exceeds the card's daily or per-transaction spending limit. | Yes (try smaller amount or wait) |
| Do Not Honor | The bank declined without explanation. This is a catch-all for various bank-side rules. | No (try different card) |
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Raw gateway responses are not meant for customers. "Do Not Honor" means nothing to a regular person. Your job is to translate each response into a clear, helpful message that tells the customer what to do next.
// gatewayMessageMapper.js
var GATEWAY_MESSAGES = {
'Approved': {
message: 'Payment successful.',
action: null,
retryable: false,
},
'Successful': {
message: 'Payment successful.',
action: null,
retryable: false,
},
'Declined': {
message: 'Your bank declined this transaction.',
action: 'Please try a different card or contact your bank for details.',
retryable: false,
},
'Insufficient Funds': {
message: 'Your card does not have enough balance for this payment.',
action: 'Please fund your account and try again, or use a different card.',
retryable: true,
},
'Card Expired': {
message: 'The card you used has expired.',
action: 'Please use a valid, non-expired card.',
retryable: false,
},
'Invalid Card Number': {
message: 'The card number you entered is not valid.',
action: 'Please check the card number and try again.',
retryable: false,
},
'Invalid Expiry': {
message: 'The expiry date does not match your card.',
action: 'Please check the expiry date and try again.',
retryable: false,
},
'Invalid CVV': {
message: 'The CVV code does not match.',
action: 'Please check the 3-digit code on the back of your card and try again.',
retryable: false,
},
'Transaction Timeout': {
message: 'The payment timed out. This is usually a temporary issue.',
action: 'Please wait a moment and try again.',
retryable: true,
},
'System Malfunction': {
message: 'There was a temporary issue processing your payment.',
action: 'Please try again in a few minutes.',
retryable: true,
},
'Do Not Honor': {
message: 'Your bank did not approve this transaction.',
action: 'Please try a different card or contact your bank.',
retryable: false,
},
'Exceeds Withdrawal Limit': {
message: 'This transaction exceeds your card spending limit.',
action: 'Please try a smaller amount or contact your bank to increase your limit.',
retryable: true,
},
'Card Not Enrolled': {
message: 'Your card is not enabled for online payments.',
action: 'Please contact your bank to enable online transactions, or use a different card.',
retryable: false,
},
'Restricted Card': {
message: 'Your card has been restricted by your bank.',
action: 'Please contact your bank to resolve the restriction.',
retryable: false,
},
};
function getCustomerMessage(gatewayResponse) {
var mapping = GATEWAY_MESSAGES[gatewayResponse];
if (!mapping) {
// Unknown response - generic fallback
return {
message: 'We could not process your payment.',
action: 'Please try again or use a different payment method.',
retryable: true,
};
}
return mapping;
}
// Usage in your payment verification flow
function buildPaymentResult(transaction) {
if (transaction.status === 'success') {
return {
success: true,
message: 'Payment successful.',
};
}
var customerMessage = getCustomerMessage(transaction.gateway_response);
return {
success: false,
message: customerMessage.message,
action: customerMessage.action,
canRetry: customerMessage.retryable,
// Never send the raw gateway_response to the frontend
// Log it server-side for debugging
};
}
Store this mapping on your server, not in your frontend code. You want to be able to update messages without redeploying your client application. If you discover a new gateway response value you have not seen before, your fallback message covers it while you add a proper mapping.
Not all failures deserve a retry. Retrying a "Card Expired" transaction will fail every time. Retrying a "Transaction Timeout" has a good chance of succeeding on the second attempt.
// retryLogic.js
var RETRYABLE_RESPONSES = [
'Transaction Timeout',
'System Malfunction',
'Insufficient Funds',
'Exceeds Withdrawal Limit',
];
var DO_NOT_RETRY = [
'Declined',
'Card Expired',
'Invalid Card Number',
'Invalid Expiry',
'Invalid CVV',
'Card Not Enrolled',
'Restricted Card',
'Card Stolen',
'Do Not Honor',
'Transaction Not Permitted',
];
function shouldRetry(gatewayResponse) {
if (DO_NOT_RETRY.indexOf(gatewayResponse) !== -1) {
return { retry: false, reason: 'Permanent failure' };
}
if (RETRYABLE_RESPONSES.indexOf(gatewayResponse) !== -1) {
return { retry: true, reason: 'Temporary failure' };
}
// Unknown response - default to not retrying automatically
return { retry: false, reason: 'Unknown response' };
}
// Retry with backoff
async function retryPayment(reference, maxRetries) {
var attempts = 0;
var delays = [2000, 5000, 15000]; // 2s, 5s, 15s
while (attempts < (maxRetries || 3)) {
var result = await verifyTransaction(reference);
if (result.status === 'success') {
return result;
}
var retryDecision = shouldRetry(result.gateway_response);
if (!retryDecision.retry) {
console.log(
'Not retrying: ' + result.gateway_response
+ ' (' + retryDecision.reason + ')'
);
return result;
}
var delay = delays[attempts] || delays[delays.length - 1];
console.log(
'Retrying in ' + delay + 'ms (attempt '
+ (attempts + 1) + ' of ' + (maxRetries || 3) + ')'
);
await new Promise(function(resolve) {
setTimeout(resolve, delay);
});
attempts++;
}
return { status: 'failed', gateway_response: 'Max retries exceeded' };
}
Important: retry logic applies mainly to scenarios where your system is re-initiating a transaction or polling for status. For the standard Initialize Transaction flow, the customer is on the Paystack checkout page and Paystack handles retries internally. Retry logic is more relevant when you are using the Charge API for background charges on saved authorizations.
Gateway responses are a gold mine for understanding your payment health. Track them in your analytics to spot trends and problems early.
// Track gateway responses for analytics
async function logGatewayResponse(transaction) {
await db.query(
'INSERT INTO payment_analytics (reference, status, gateway_response, channel, amount, created_at) VALUES ($1, $2, $3, $4, $5, NOW())',
[
transaction.reference,
transaction.status,
transaction.gateway_response,
transaction.channel,
transaction.amount,
]
);
}
// Weekly report: failure reasons breakdown
async function getFailureBreakdown(startDate, endDate) {
var result = await db.query(
'SELECT gateway_response, COUNT(*) as count, '
+ 'ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) as percentage '
+ 'FROM payment_analytics '
+ 'WHERE status = $1 AND created_at BETWEEN $2 AND $3 '
+ 'GROUP BY gateway_response '
+ 'ORDER BY count DESC',
['failed', startDate, endDate]
);
return result.rows;
}
What to watch for in your analytics:
Build a simple dashboard or weekly email report that shows your success rate, top failure reasons, and trends over time. This is how you keep your checkout conversion healthy.
Different payment channels produce different gateway responses. Card payments have the richest set of responses. Bank transfers, USSD, and mobile money have simpler patterns.
Bank transfers: The gateway_response for successful bank transfers is typically "Approved" or "Successful." Failed bank transfers are rare because the customer either sends the money or does not. The common failure mode is expiry: the generated account number times out before the customer transfers.
USSD: USSD transactions may time out if the customer does not complete the USSD session. The gateway_response will be "Transaction Timeout" or a similar message. USSD transactions also fail if the customer enters the wrong PIN on their bank USSD menu.
Mobile money: Mobile money transactions (MTN, Vodafone, AirtelTigo in Ghana; M-Pesa in Kenya) have their own set of responses. Common failures include insufficient balance on the mobile money wallet, wrong PIN, and network timeout.
QR payments: QR payment failures typically come from the scanning app not completing the payment or the QR code expiring.
For all non-card channels, the most important thing to handle is the pending state. These channels often do not resolve immediately. The customer initiates payment, and the result comes back asynchronously via webhook. During the pending window, the gateway_response may not be final. Always wait for the webhook confirmation before treating the transaction as complete or failed.
For details on handling each payment channel, see the guides on USSD payments, bank transfers, and mobile money.
When a customer contacts support saying "my payment failed," the gateway_response is the first thing to check. Here is a practical debugging flow:
The gateway_response combined with the channel and card details usually tells you exactly what happened. "Insufficient Funds" on a Verve debit card means the customer's bank account is low. "Card Not Enrolled" on a Mastercard means the card has not been activated for online purchases. Your support team can then give the customer specific advice instead of "try again."
Log the full transaction object (minus sensitive card details) when a payment fails. This gives your team historical data to diagnose issues without making extra API calls. Some failures are intermittent (bank outages), and having the timestamps helps correlate with known incidents.
For a broader understanding of how gateway responses fit into the payment lifecycle, see the complete guide to accepting payments with Paystack.
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