McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Simulating Failed Payments on Paystack
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Simulating Failed Payments on Paystack

Simulate Paystack payment failures using: (1) test decline cards (4084080000000409 for Do Not Honor, 4084080000000005 for Insufficient Funds) — these fire charge.failed webhooks; (2) mock the Paystack initialize endpoint returning 5xx — test your server error handling; (3) use an expired access_code — test session timeout handling; (4) send a charge.failed webhook payload manually with a valid HMAC — test your failed payment webhook handler.

In this article

  1. 1. Types of Paystack Payment Failures and How to Simulate Them
  2. 2. Testing the charge.failed Webhook Handler
  3. 3. Learn More

Types of Paystack Payment Failures and How to Simulate Them

Failure TypeHow to SimulateExpected Behavior
Card declined (Do Not Honor) Use test card 4084080000000409 charge.failed webhook fires; order stays pending; user sees decline message
Insufficient funds Use test card 4084080000000005 charge.failed webhook; user told to check balance or try different card
Paystack API 500 on initialize Mock fetch to return 500 response Checkout returns 503 with friendly message; no order created
Paystack API timeout Mock fetch to hang for 30+ seconds Timeout after configured limit; retry logic fires; user sees "try again"
Expired access_code / session Use an already-processed reference Paystack returns error; user is redirected to restart checkout
Duplicate reference Send same reference to initialize twice Paystack returns error on second call; your code handles it without crashing

Testing the charge.failed Webhook Handler

// test: charge.failed webhook handler
const crypto = require('crypto');

describe('charge.failed webhook handling', () => {
  it('marks order as payment_failed without fulfilling it', async () => {
    var orderId = 77;
    await db.orders.create({ id: orderId, status: 'pending', amount: 5000 });

    var payload = {
      event: 'charge.failed',
      data: {
        reference: 'ref_fail_001',
        amount: 500000,
        status: 'failed',
        gateway_response: 'Insufficient Funds',
        metadata: { order_id: orderId },
        customer: { email: 'test@example.com' },
      },
    };
    var body = JSON.stringify(payload);
    var sig = crypto.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY).update(body).digest('hex');

    await request(app)
      .post('/webhook/paystack')
      .set('x-paystack-signature', sig)
      .send(body)
      .expect(200);

    var order = await db.orders.findByPk(orderId);
    expect(order.status).toBe('payment_failed');
    expect(order.failure_reason).toBe('Insufficient Funds');
    // Critically: the order should NOT be fulfilled
    expect(order.fulfilled_at).toBeNull();
  });
});

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Learn More

This guide is part of the Paystack testing guide.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Paystack test decline cards trigger charge.failed webhook events — use them to test your failed payment flow end-to-end.
  • ✓Test the Paystack API being unavailable: mock a 503 response from initialize and verify your endpoint handles it gracefully.
  • ✓Test expired payment session: use an already-used access_code and verify the checkout tells the user to restart.
  • ✓Test webhook handling for charge.failed: POST a valid HMAC-signed charge.failed payload and verify the order is not fulfilled.
  • ✓Test every decline reason your UX displays — map gateway_response values to user-friendly messages and test each mapping.
  • ✓Failed payment recovery: after a failure, can the customer retry with a different card without losing their cart?

Frequently Asked Questions

Does Paystack always send a webhook for failed payments?
Paystack sends a charge.failed event for card declines that reached the processing stage. Some failures (like network errors before the charge was attempted) may not generate a webhook. This is why you should not rely solely on webhooks for payment status — run a nightly reconciliation job that verifies pending orders using the Paystack verify API.
How should my frontend react when a payment fails?
Show a specific, helpful message based on the failure reason. "Do Not Honor" → "Your bank declined this payment. Please contact your bank or try a different card." "Insufficient Funds" → "Your account balance may be too low. Please add funds or use a different card." Give the customer a clear retry path without losing their cart.
Should failed payment orders be deleted or kept in the database?
Keep them — with status "payment_failed". This allows: (1) customer support to see what happened; (2) analytics on your failure rate by gateway_response; (3) the customer to retry the same order. Soft-delete after 7-30 days if needed for cleanup, but never immediately delete failed payment records.
What is the difference between charge.failed and transfer.failed?
charge.failed is when a customer's payment attempt fails (card declined, bank error). transfer.failed is when your payout to a vendor or recipient fails (wrong account number, bank technical error). Both are distinct webhook events requiring different handling — charge.failed marks an order unpaid, transfer.failed requires retry or investigation of the recipient's bank details.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Testing Paystack Integrations: The Complete Guide

A complete guide to testing Paystack integrations at every level: test mode, test cards, unit tests with mocks, integration tests, end-to-end flows with Playwright, webhook simulation, contract testing, CI pipelines, staging environments, load testing, and coverage targets for payment code.

Read more

Unit Testing Paystack Integrations

Unit tests for Paystack integrations focus on your business logic, not the Paystack API itself. The goal is to test that your code responds correctly to different Paystack responses — without ever calling the real API. This guide shows what to unit test and how.

Read more

Integration Testing Paystack Flows End to End

Integration tests for Paystack test the real flow — your backend calls the actual Paystack test API, a webhook fires to your server, and your database is updated. Unlike unit tests with mocks, integration tests catch issues that only appear when the real API is involved.

Read more

Seeding Test Data for a Payments Feature

Good test data is essential for testing payment features. This guide covers creating Paystack test mode resources (plans, subaccounts, split codes) and seeding your application database with matching payment records for realistic test scenarios.

Read more

Paystack Webhook Returning 400 in Next.js App Router

Next.js App Router route handlers receive the request body as a ReadableStream. If you try to read it as JSON directly or use the wrong method, your Paystack webhook handler returns 400. This guide shows the complete working pattern for App Router webhook handlers.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.