Paystack Gateway Responses: Reading What the Bank Told You
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.
Where the Gateway Response Lives
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.
Common Gateway Responses and What They Mean
Here are the gateway responses you will encounter most often, grouped by category.
Successful Responses
| Gateway Response | What It Means |
|---|---|
| Approved | The bank approved the transaction. Everything went through. |
| Successful | Same as Approved. Some processors use this wording instead. |
Card-Related Failures
| 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) |
Processing Failures
| 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) |
Mapping Responses to Customer Messages
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.
Retry Logic Based on Gateway Responses
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.
Analytics and Monitoring with Gateway Responses
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:
- Spike in timeouts. If "Transaction Timeout" suddenly increases, a bank's infrastructure may be having issues. There is nothing you can do except wait and inform affected customers.
- High "Insufficient Funds" rate. If many of your customers fail due to low balances, consider offering smaller payment plans or alternative payment channels like bank transfer where there are no card limits.
- Rising "Declined" rate. A general increase in declines could indicate that Paystack or the bank has updated their fraud rules. It could also mean your product is attracting users who trigger fraud flags. Investigate.
- "Do Not Honor" patterns. If this response clusters around specific card types, banks, or times of day, it may point to bank-specific rules you need to work around by offering alternative payment methods.
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.
Channel-Specific Gateway Responses
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.
Gateway Responses in Production Debugging
When a customer contacts support saying "my payment failed," the gateway_response is the first thing to check. Here is a practical debugging flow:
- Get the transaction reference from the customer or your logs.
- Verify the transaction via the API: GET /transaction/verify/{reference}.
- Read the status and gateway_response fields.
- Check the channel to understand which payment method was used.
- Look at the authorization object for card details (last 4 digits, card type, bank).
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.
Key Takeaways
- ✓The gateway_response field is found in the transaction object after verification. It contains the raw bank or processor message explaining why a transaction succeeded or failed.
- ✓Common successful responses include "Approved" and "Successful." Common failures include "Declined," "Insufficient Funds," "Card Expired," "Invalid Card Number," and "Transaction Timeout."
- ✓Never show the raw gateway_response directly to customers. Map it to a friendly, actionable message. "Insufficient Funds" becomes "Your card does not have enough balance. Please try a different card or fund your account."
- ✓Some responses are retryable (timeout, system malfunction) and some are permanent (card expired, invalid card). Your retry logic should distinguish between the two.
- ✓The gateway_response is different from the transaction status. A transaction can have status "failed" with various gateway_response values explaining the specific failure reason.
- ✓Log every gateway_response for analytics. Patterns in failure reasons reveal issues: if "Timeout" spikes, there may be a bank infrastructure problem. If "Declined" is high, your customer base may have card issues.
Frequently Asked Questions
- Is the gateway_response field always present on a transaction?
- Yes, the gateway_response field is present on all verified transactions, whether successful or failed. For successful transactions it is typically "Approved" or "Successful." For failed transactions, it describes the specific failure reason.
- Can the same gateway response have different meanings on different banks?
- Yes. "Declined" is a generic response that different banks use for different reasons. One bank may decline due to fraud rules while another declines due to international transaction restrictions. The gateway_response does not always give a precise reason, which is why "Do Not Honor" and "Declined" are catch-all responses.
- Should I show the gateway_response to the customer?
- No. Map the raw gateway_response to a friendly, actionable message on your server. Responses like "Do Not Honor" confuse customers. Instead, tell them what to do: "Your bank did not approve this transaction. Please try a different card or contact your bank."
- How do I handle a gateway_response I have never seen before?
- Have a fallback message in your mapper: "We could not process your payment. Please try again or use a different payment method." Log the unknown response so you can add a specific mapping later. New gateway responses appear occasionally as banks and processors update their systems.
- Are gateway responses different in test mode vs live mode?
- In test mode, Paystack simulates responses based on test card numbers and scenarios. The response values (Approved, Declined, etc.) are the same strings you see in live mode, but they are triggered by test conditions rather than real bank responses. This lets you test your mapping logic before going live.
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