Bonaventure OgetoBy Bonaventure Ogeto|

Reading Paystack Gateway Response Codes

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.

What the Gateway Response Field Is

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

Common Gateway Responses and What They Mean

Here are the gateway responses you will encounter most frequently, what causes them, and what to tell the customer.

Gateway ResponseCauseCustomer MessageRetryable?
Approved / SuccessfulBank approved the charge.N/A (payment succeeded)N/A
Insufficient FundsCard 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
DeclinedGeneric 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 HonorBank 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 PINCustomer entered wrong PIN during checkout."The PIN you entered is incorrect. Please try again with the correct PIN."Yes, immediately
Expired CardCard expiry date has passed."This card has expired. Please use a card with a valid expiry date."No
Card Not EnrolledCard 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 PermittedCard 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 LimitTransaction 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 TransactionSomething 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 MalfunctionThe bank's system is temporarily unavailable."Your bank's system is temporarily unavailable. Please try again in a few minutes."Yes, after waiting
Restricted CardCard 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."

Mapping Gateway Responses in Your Code

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.

Regional Patterns in Gateway Responses

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.

Tracking Gateway Responses for Business Intelligence

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.

The "Do Not Honor" Problem

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

  • Tell the customer to call their bank. This is the only reliable path. The bank's customer service can see the internal reason and resolve it. Your message should say: "Your bank did not approve this payment. Please call the number on the back of your card and ask them to authorize the transaction, then try again."
  • Offer an alternative payment method. If the customer has been declined by one card, show them bank transfer, USSD, or mobile money options. Switching channels often works because the restriction may only apply to card payments.
  • Do not retry automatically. If a bank said "Do Not Honor," sending the same charge again will produce the same result. Repeated attempts may also trigger additional fraud flags on the customer's account.

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.

Verifying Your Gateway Response Handling

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.

  1. Unit test the mapping function. Pass each known gateway_response string into your getGatewayMessage function and verify it returns the correct customer message, retryable flag, and category.
  2. Test unknown responses. Pass a string your mapping does not recognize (like "Random Bank Error") and confirm it falls back to the default message gracefully.
  3. Test null/undefined. Pass null, undefined, and an empty string. Your function should handle all three without crashing.
  4. Verify UI rendering. Check that your checkout page correctly displays the customer message, shows or hides the retry button based on the retryable flag, and offers alternative payment methods for non-retryable errors.
// 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();
  });
});

Key Takeaways

  • The gateway_response field appears in both the Verify Transaction response and the charge.success/charge.failed webhook payload. Always check it for failed transactions.
  • Most gateway responses fall into three categories: customer-fixable (insufficient funds, wrong PIN), card-issue (expired, blocked), and bank-side (do not honor, system error). Your error handling should distinguish between these.
  • Never show the raw gateway_response to customers. Translate it into a clear, actionable message. "Insufficient Funds" becomes "Your card does not have enough funds for this payment. Please try a different card or fund your account."
  • "Do Not Honor" is the most frustrating response because the bank gives no reason. The customer must contact their bank. There is nothing you or Paystack can do about it.
  • Store the gateway_response alongside the transaction in your database. Over time, this data reveals patterns like which banks decline most often and helps you optimize your checkout flow.
  • Some gateway responses are transient (bank system unavailable). Build retry logic for these, but not for permanent failures like expired cards.

Frequently Asked Questions

Are Paystack gateway response strings case-sensitive in my code?
Yes. Paystack returns these strings with specific capitalization (e.g., "Insufficient Funds" not "insufficient funds"). Match them exactly in your mapping. If you want case-insensitive matching, convert the response to lowercase before looking it up, but make sure your mapping keys are also lowercase.
Do mobile money and bank transfer payments have gateway responses?
Mobile money and bank transfer payments do not produce traditional card-style gateway responses. Their status field indicates success, pending, or failed, and the message field may contain provider-specific error details. You need separate error handling logic for non-card payment channels.
Can I get more detail from the bank beyond the gateway_response?
No. The gateway_response is the most detail Paystack can share from the bank. Banks do not expose their internal decision logic through payment networks. If you need more information about a specific decline, contact Paystack support with the transaction reference and they may be able to get additional context from the acquiring bank.
Why do I see "Approved" as gateway_response but the transaction status is "failed"?
This is rare but can happen when the bank approved the charge but a subsequent step failed (like a post-authorization check). Always use the transaction status field ("success" or "failed") as the source of truth, not the gateway_response. The gateway_response is diagnostic information, not the final verdict.
Should I log gateway responses that contain customer card information?
Gateway responses from Paystack do not contain card numbers, CVVs, or other sensitive card data. They are safe to log and store. However, the broader transaction object may contain masked card details (first six and last four digits) which you should handle according to your PCI compliance requirements.

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