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. Handle Paystack Webhooks in Node.js and Express
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Handle Paystack Webhooks in Node.js and Express

To handle Paystack webhooks in Express, create a POST route that reads the raw request body, computes an HMAC SHA-512 hash using your secret key, and compares it to the x-paystack-signature header. If the signatures match, parse the event and process it. Return a 200 status immediately.

In this article

  1. 1. How Paystack Webhooks Work
  2. 2. Capturing the Raw Body
  3. 3. Verify the Webhook Signature
  4. 4. Process Webhook Events
  5. 5. Testing Webhooks Locally
  6. 6. Production Checklist
  7. 7. Ship Payments Faster

How Paystack Webhooks Work

When a payment succeeds (or fails, or a subscription renews), Paystack sends a POST request to the webhook URL you configured in your dashboard. The request body is JSON containing the event type and the full transaction data.

Paystack signs every webhook with an HMAC SHA-512 hash of the request body, using your secret key. The hash is sent in the x-paystack-signature header. You verify this signature to confirm the webhook actually came from Paystack and was not tampered with.

To set your webhook URL: go to your Paystack Dashboard > Settings > API Keys & Webhooks > Webhook URL.

Capturing the Raw Body

Express normally parses JSON request bodies automatically with express.json(). But signature verification needs the raw bytes, not the parsed object. If Express parses the body first, rebuilding it as a string may produce different whitespace or key ordering, breaking the signature check.

The solution: capture the raw body alongside the parsed body.

var express = require('express');
var crypto = require('crypto');
var app = express();

// For webhook route: capture raw body
app.use('/api/webhooks/paystack', express.json({
  verify: function(req, res, buf) {
    req.rawBody = buf;
  }
}));

// For all other routes: normal JSON parsing
app.use(express.json());

The verify option in express.json() runs before parsing. We store the raw buffer on the request object so we can hash it later. This middleware must be registered before the default express.json() to avoid conflicts.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Verify the Webhook Signature

Create the webhook route and verify the signature before processing:

var PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;

app.post('/api/webhooks/paystack', function(req, res) {
  // 1. Get the signature from the header
  var signature = req.headers['x-paystack-signature'];

  if (!signature) {
    return res.status(400).send('Missing signature');
  }

  // 2. Compute the expected hash
  var hash = crypto
    .createHmac('sha512', PAYSTACK_SECRET)
    .update(req.rawBody)
    .digest('hex');

  // 3. Compare
  if (hash !== signature) {
    console.error('Webhook signature mismatch');
    return res.status(400).send('Invalid signature');
  }

  // 4. Signature valid. Process the event.
  var event = req.body;

  console.log('Received event:', event.event);

  // Return 200 immediately
  res.sendStatus(200);

  // Process asynchronously (after response)
  handlePaystackEvent(event).catch(function(err) {
    console.error('Error processing webhook:', err);
  });
});

Returning 200 before processing is important. If your processing takes more than a few seconds and Paystack does not get a response, it will retry the webhook. Sending 200 first tells Paystack "I received it" while you process in the background.

Process Webhook Events

Handle the events relevant to your business:

async function handlePaystackEvent(event) {
  var eventType = event.event;
  var data = event.data;

  if (eventType === 'charge.success') {
    await handleSuccessfulPayment(data);
  } else if (eventType === 'charge.failed') {
    console.log('Payment failed for reference:', data.reference);
  } else if (eventType === 'subscription.create') {
    await handleNewSubscription(data);
  } else if (eventType === 'invoice.payment_failed') {
    await handleFailedRenewal(data);
  } else if (eventType === 'transfer.success') {
    await handleSuccessfulTransfer(data);
  } else {
    console.log('Unhandled event type:', eventType);
  }
}

async function handleSuccessfulPayment(data) {
  var reference = data.reference;
  var amount = data.amount;
  var currency = data.currency;

  // Idempotent fulfillment
  var result = await db.orders.updateOne(
    { reference: reference, paid: false },
    { $set: { paid: true, paidAt: new Date(), paystackData: data } }
  );

  if (result.modifiedCount === 0) {
    console.log('Order already fulfilled or not found:', reference);
    return;
  }

  // Deliver value
  var order = await db.orders.findOne({ reference: reference });
  await sendConfirmationEmail(order.email, order);
}

The key events for most applications:

  • charge.success - one-time payment completed
  • subscription.create - new subscription started
  • invoice.payment_failed - subscription renewal failed
  • transfer.success / transfer.failed - payout completed or failed
  • refund.processed - refund completed

Testing Webhooks Locally

Paystack cannot reach your localhost. You have two options for local testing:

Option 1: Use a tunnel. Tools like ngrok or localtunnel expose your local server to the internet:

npx ngrok http 3000

Copy the HTTPS URL (something like https://abc123.ngrok.io) and set it as your webhook URL in the Paystack dashboard: https://abc123.ngrok.io/api/webhooks/paystack.

Option 2: Simulate locally. Construct a webhook payload and sign it yourself:

var crypto = require('crypto');

var payload = JSON.stringify({
  event: 'charge.success',
  data: {
    reference: 'test_ref_123',
    amount: 500000,
    currency: 'NGN',
    status: 'success',
    customer: { email: 'test@example.com' },
  },
});

var hash = crypto
  .createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
  .update(payload)
  .digest('hex');

// Use curl or fetch to POST to your local server
// with header x-paystack-signature: 
// and body: 

Production Checklist

  1. HTTPS only. Your webhook URL must use HTTPS. Paystack will not send webhooks to HTTP URLs in production.
  2. Respond quickly. Return 200 within 5 seconds. Queue heavy processing for background workers.
  3. Idempotent handlers. Paystack may send the same event more than once. Your handler must produce the same result regardless of how many times it runs.
  4. Log everything. Log the event type and reference for every webhook. This helps debug missing payments.
  5. Monitor failures. Check the Paystack dashboard for failed webhook deliveries. If your endpoint returns non-200 responses, Paystack shows them in the webhook logs.

Ship Payments Faster

This guide is part of the Paystack integration guides by language and framework series. For the full payment flow, see Accept Payments with Paystack in Node.js and Express.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Webhooks are the reliable payment notification channel. Redirect callbacks can fail if the customer closes their browser.
  • ✓You must verify the x-paystack-signature header using HMAC SHA-512 with your secret key. Never process unverified webhooks.
  • ✓The raw body is required for signature verification. If Express parses the JSON before you hash it, the signature will not match.
  • ✓Return 200 immediately. Do heavy processing asynchronously. If Paystack does not get a 200 within a few seconds, it retries.
  • ✓Paystack retries failed webhooks with increasing intervals. Make your handler idempotent so duplicate events cause no harm.
  • ✓The most important event for payments is charge.success. For subscriptions, also handle subscription.create and invoice.payment_failed.

Frequently Asked Questions

Why does my webhook signature verification fail?
The most common cause is Express parsing the JSON body before you hash it. The raw bytes must be hashed, not the re-serialized object. Use the verify option in express.json() to capture the raw buffer. Also make sure you are using the correct secret key (test vs live).
How many times does Paystack retry a failed webhook?
Paystack retries with increasing intervals. If your endpoint keeps returning non-200 responses, Paystack eventually stops trying. Check the webhook logs in your Paystack dashboard to see failed deliveries.
Can I use the same webhook URL for test and live modes?
Yes, but you need to use the correct secret key for signature verification. Test webhooks are signed with your test secret key, and live webhooks with your live secret key. If you serve both from the same endpoint, check the key mode based on the incoming data.
Do I still need to verify transactions if I use webhooks?
Yes. The webhook confirms the event happened, but you should still call the verify endpoint to get the authoritative transaction state. This provides defense in depth. The webhook tells you what happened; verification confirms it.

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

Paystack Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Accept Payments with Paystack in Svelte and SvelteKit

Accept Paystack payments in SvelteKit using server endpoints for backend logic and Svelte components for the frontend. This guide covers initializing transactions, inline popup checkout, redirect checkout, and verification in a single full-stack SvelteKit project.

Read more

Handle Paystack Webhooks in Flask

Receive and verify Paystack webhooks in your Flask application. This guide covers accessing request.data for raw bytes, HMAC SHA-512 signature verification, processing charge.success events, and making handlers idempotent.

Read more

Build Paystack Subscriptions in ASP.NET Core

Implement Paystack recurring billing in ASP.NET Core. This guide covers creating plans, subscribing customers through transaction initialization, handling renewal webhooks with C#, and cancelling subscriptions.

Read more

Paystack Payment Channels: Card, Bank, USSD, Mobile Money, QR

Paystack supports six payment channels: card, bank transfer, USSD, mobile money, QR, and Apple Pay. Each channel has different availability by market, different settlement behaviors, and different user experiences. This guide covers all of them so you know what your customers can use and what your code needs to handle.

Read more

Paystack Amount Too Low Error

Paystack rejects transactions below a minimum amount that varies by currency. This guide lists every minimum, explains the kobo/pesewa/cents confusion that causes most "amount too low" errors, and gives you a server-side validation function to catch the problem before it reaches the API.

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.