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. Contract Testing Your Paystack Webhook Handler
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Contract Testing Your Paystack Webhook Handler

Contract test your Paystack webhook handler by: (1) capturing real webhook payloads from Paystack test mode (log them during development), (2) storing them as JSON fixtures, (3) writing tests that feed those fixtures into your handler and assert the correct business logic runs. Also validate incoming payloads against a JSON schema to catch Paystack API changes early.

In this article

  1. 1. Creating Webhook Payload Fixtures
  2. 2. Contract Test Using Fixtures
  3. 3. Learn More

Creating Webhook Payload Fixtures

// tests/fixtures/paystack-charge-success.json
{
  "event": "charge.success",
  "data": {
    "id": 1234567,
    "domain": "test",
    "status": "success",
    "reference": "ref_test_001",
    "amount": 100000,
    "message": null,
    "gateway_response": "Approved",
    "paid_at": "2026-07-22T10:00:00.000Z",
    "created_at": "2026-07-22T09:59:55.000Z",
    "channel": "card",
    "currency": "NGN",
    "ip_address": "197.234.240.2",
    "metadata": {
      "order_id": 42,
      "custom_fields": []
    },
    "fees_breakdown": null,
    "log": null,
    "fees": 1500,
    "fees_split": null,
    "authorization": {
      "authorization_code": "AUTH_test_code",
      "bin": "408408",
      "last4": "4081",
      "exp_month": "05",
      "exp_year": "2030",
      "channel": "card",
      "card_type": "visa",
      "bank": "TEST BANK",
      "country_code": "NG",
      "brand": "visa",
      "reusable": true,
      "signature": "SIG_test",
      "account_name": null
    },
    "customer": {
      "id": 9876543,
      "first_name": null,
      "last_name": null,
      "email": "test@example.com",
      "customer_code": "CUS_test",
      "phone": null,
      "metadata": {},
      "risk_action": "allow",
      "international_format_phone": null
    },
    "plan": {},
    "subaccount": {},
    "order_id": null,
    "paidAt": "2026-07-22T10:00:00.000Z",
    "requested_amount": 100000,
    "pos_transaction_data": null,
    "source": null,
    "fees_breakdown": null
  }
}

Contract Test Using Fixtures

// tests/webhook-contract.test.js
const crypto = require('crypto');
const request = require('supertest');
const app = require('../app');
const db = require('../db');

const chargeSuccessFixture = require('./fixtures/paystack-charge-success.json');

function signPayload(body) {
  return crypto.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY).update(body).digest('hex');
}

describe('Webhook contract: charge.success', () => {
  it('extracts order_id from metadata and marks order paid', async () => {
    await db.orders.create({ id: 42, email: 'test@example.com', status: 'pending', amount: 1000 });

    var body = JSON.stringify(chargeSuccessFixture);
    var sig = signPayload(body);

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

    var order = await db.orders.findByPk(42);
    expect(order.status).toBe('paid');
    expect(order.paystack_reference).toBe('ref_test_001');
  });

  it('rejects payload with missing required fields gracefully', async () => {
    var malformedPayload = { event: 'charge.success', data: {} };
    var body = JSON.stringify(malformedPayload);
    var sig = signPayload(body);

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

    // Should not throw 500 — handle gracefully
    expect(res.status).not.toBe(500);
  });
});

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

  • ✓Capture real Paystack test webhook payloads during development and save them as JSON test fixtures.
  • ✓Feed those fixtures into your webhook handler in tests and assert the expected database updates occur.
  • ✓Validate incoming webhook payloads against a JSON schema — catch field renames or type changes from Paystack early.
  • ✓Test all event types your handler processes: charge.success, charge.failed, subscription.disable, transfer.success.
  • ✓Test the signature validation as a separate contract requirement — your handler must reject payloads without a valid signature.
  • ✓If Paystack changes their webhook schema (adds a required field you depend on), your contract test fixture will expose the mismatch.

Frequently Asked Questions

How do I capture a real Paystack webhook payload to use as a fixture?
In development, log the entire raw webhook request body when a test event fires: console.log(JSON.stringify(req.body, null, 2)). Trigger a test payment, check your server logs, copy the payload, save it as a JSON fixture file. This gives you an accurate fixture that matches Paystack's actual schema.
Should I validate incoming webhook payloads against a schema?
Yes — use a lightweight schema validator like Ajv or Zod. Define the expected shape of charge.success, charge.failed, and other events. Log validation failures but do not reject the webhook (Paystack expects 200). This lets you detect schema changes from Paystack without breaking your payment flow.
What is the difference between contract testing and integration testing here?
Integration tests verify that your system works end-to-end with the real API. Contract tests verify that your handler correctly handles the specific data shape that Paystack sends. Contract tests use fixtures (stored payloads) rather than calling the real API, making them faster and more focused on the data contract.
Does Paystack publish a schema or specification for webhook events?
Paystack documents the webhook event payload structure in their API reference. They do not publish a formal JSON Schema or OpenAPI spec for events as of 2026. The best approach is to capture live payloads and use those as your source of truth for fixtures.

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

Simulating Declined Cards on Paystack

A card decline is one of the most common failure states in a payment flow, and your application must handle it gracefully. This guide shows how to simulate specific card declines in Paystack test mode and how to verify your decline handling code works correctly.

Read more

Mocking the Paystack API in PHPUnit

PHPUnit tests for Paystack integrations in Laravel or PHP can run without any network calls using Laravel's Http::fake() or Mockery. This guide shows how to mock Paystack responses, test webhook handlers, and verify your payment logic in isolation.

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 Insufficient Funds Response Handling

When a customer's account does not have enough money, Paystack returns "Insufficient Funds" in the gateway_response. Your job is to detect this, show a tactful message that does not embarrass the customer, and offer alternatives.

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.