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.
// 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
}
}
// 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.
This guide is part of the Paystack testing guide.
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