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 Next.js Pages Router
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Handle Paystack Webhooks in Next.js Pages Router

In Next.js Pages Router, you handle Paystack webhooks by creating an API Route at pages/api/webhooks/paystack.ts. You must disable the built-in body parser with a custom config export, read the raw body manually from the request stream, compute an HMAC SHA512 hash with your secret key, and compare it to the x-paystack-signature header. Return a 200 status immediately after verification.

In this article

  1. 1. API Routes in the Pages Router
  2. 2. Disable the Body Parser
  3. 3. Read the Raw Body from the Request Stream
  4. 4. Verify the HMAC SHA512 Signature
  5. 5. Route Events to Handler Functions
  6. 6. Middleware and CSRF Considerations
  7. 7. Deployment and Production Notes

API Routes in the Pages Router

The Next.js Pages Router provides API Routes for building server-side endpoints. Any file inside pages/api/ becomes an API endpoint. These handlers receive Node.js IncomingMessage (req) and ServerResponse (res) objects, which are familiar if you have used Express.

The main challenge for Paystack webhooks is that Next.js API Routes parse the request body automatically by default. The built-in bodyParser middleware converts the raw JSON bytes into a JavaScript object before your handler runs. Once that happens, you cannot compute the HMAC signature over the original raw bytes, and verification fails.

The fix is straightforward: disable the body parser for your webhook route and read the raw body yourself. If you are using the App Router instead, see Handle Paystack Webhooks in Next.js App Router.

Disable the Body Parser

Create the file pages/api/webhooks/paystack.ts. The first thing you need is a config export that turns off the body parser:

import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

// Disable the default body parser so we can read the raw body
export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    return res.status(405).json({ error: 'Method not allowed' });
  }

  // Handler code goes here...
}

With bodyParser: false, the req.body property will be undefined. You need to read the raw bytes from the request stream yourself. This is the only way to get the exact bytes that Paystack signed.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Read the Raw Body from the Request Stream

Since the body parser is disabled, you need a helper function that reads the request stream into a Buffer. Here is a straightforward implementation:

function getRawBody(req: NextApiRequest): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];

    req.on('data', (chunk: Buffer) => {
      chunks.push(chunk);
    });

    req.on('end', () => {
      resolve(Buffer.concat(chunks));
    });

    req.on('error', (err) => {
      reject(err);
    });
  });
}

Alternatively, you can install the raw-body npm package, which handles edge cases like content-length limits and encoding:

npm install raw-body
import getRawBody from 'raw-body';

// Inside your handler:
const rawBody = await getRawBody(req);

Either approach gives you a Buffer containing the exact bytes Paystack sent. This Buffer is what you pass to the HMAC function.

Verify the HMAC SHA512 Signature

With the raw body in hand, you can now verify the Paystack signature. Here is the complete handler:

import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

export const config = {
  api: {
    bodyParser: false,
  },
};

function getRawBody(req: NextApiRequest): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    req.on('data', (chunk: Buffer) => chunks.push(chunk));
    req.on('end', () => resolve(Buffer.concat(chunks)));
    req.on('error', reject);
  });
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const secret = process.env.PAYSTACK_SECRET_KEY;
  if (!secret) {
    console.error('PAYSTACK_SECRET_KEY is not set');
    return res.status(500).json({ error: 'Server config error' });
  }

  // Read raw body
  const rawBody = await getRawBody(req);

  // Get signature header
  const signature = req.headers['x-paystack-signature'] as string;
  if (!signature) {
    return res.status(400).json({ error: 'No signature' });
  }

  // Compute HMAC SHA512
  const hash = crypto
    .createHmac('sha512', secret)
    .update(rawBody)
    .digest('hex');

  if (hash !== signature) {
    return res.status(400).json({ error: 'Invalid signature' });
  }

  // Parse the verified payload
  const event = JSON.parse(rawBody.toString('utf-8'));

  // Return 200 immediately
  res.status(200).json({ received: true });

  // Process the event (ideally in a background queue)
  // await processEvent(event);
}

The critical line is .update(rawBody). You are passing the Buffer directly, not a re-serialized string. This guarantees the bytes match what Paystack signed.

Route Events to Handler Functions

Once the signature is verified and you have parsed the JSON, route the event by type. A clean approach is a handler map:

interface PaystackEvent {
  event: string;
  data: Record<string, unknown>;
}

const handlers: Record<string, (data: Record<string, unknown>) => Promise<void>> = {
  'charge.success': async (data) => {
    const reference = data.reference as string;
    const amount = data.amount as number;
    console.log('Payment received: ' + reference + ' for ' + String(amount));
    // Update order status, credit user account, etc.
  },
  'transfer.success': async (data) => {
    console.log('Transfer completed: ' + String(data.reference));
  },
  'transfer.failed': async (data) => {
    console.log('Transfer failed: ' + String(data.reference));
  },
  'subscription.create': async (data) => {
    console.log('Subscription created: ' + String(data.subscription_code));
  },
};

async function processEvent(event: PaystackEvent) {
  const handler = handlers[event.event];
  if (handler) {
    await handler(event.data);
  } else {
    console.log('Unhandled event type: ' + event.event);
  }
}

In production, replace the console.log calls with actual database operations, queue pushes, or whatever your application needs. The key rule remains: keep the webhook handler itself fast and push heavy work to a background process.

Middleware and CSRF Considerations

Next.js API Routes in the Pages Router do not have built-in CSRF protection, so you do not need to exempt your webhook route from any CSRF middleware. However, if you have installed a custom CSRF package or authentication middleware that applies to all API routes, you need to skip it for the webhook endpoint.

If you use next-connect or a similar middleware library, make sure your webhook route does not pass through authentication or session middleware. Paystack's servers do not send cookies or auth tokens. They send the x-paystack-signature header, which is your only form of authentication.

If you have a global middleware file (middleware.ts) that checks authentication, exclude the webhook path:

// middleware.ts
export const config = {
  matcher: [
    '/((?!api/webhooks).*)',
  ],
};

This matcher pattern tells Next.js to run middleware on all routes except those starting with api/webhooks.

Deployment and Production Notes

When deploying to Vercel, your API Route becomes a serverless function. A few things to keep in mind:

  • Function timeout: Hobby plan gives you 10 seconds. Pro gives 60 seconds. Return 200 quickly and do not block on heavy operations.
  • Cold starts: The first request after a period of inactivity may take longer. This is usually not a problem for webhooks because Paystack waits several seconds before considering a delivery failed.
  • Environment variables: Set PAYSTACK_SECRET_KEY in your Vercel project settings under Environment Variables. Make sure it is available in the Production environment.
  • Logs: Use console.log for webhook events. You can view these in the Vercel Functions logs in real time.

For self-hosted Next.js deployments (running next start behind nginx or similar), make sure your reverse proxy passes the raw body through without modification. Some proxy configurations strip or re-encode the body, which will break signature verification.

This guide is part of the Paystack integration guides by language and framework series.

Key Takeaways

  • ✓Create an API Route at pages/api/webhooks/paystack.ts for the webhook endpoint.
  • ✓Disable the built-in body parser by exporting config = { api: { bodyParser: false } } so you can read the raw body.
  • ✓Read the raw body from the request stream using a buffer helper function.
  • ✓Compute HMAC SHA512 with the Node.js crypto module and compare to x-paystack-signature.
  • ✓Return res.status(200).json({ received: true }) immediately before any heavy processing.
  • ✓No CSRF exemption is needed for API Routes in Next.js Pages Router.

Frequently Asked Questions

Why do I need to disable bodyParser for Paystack webhooks in Next.js Pages Router?
The default body parser converts the raw JSON bytes into a JavaScript object. When you try to re-stringify that object for HMAC computation, the whitespace or key ordering may differ from the original. This causes the signature check to fail. Disabling bodyParser lets you read the exact bytes Paystack sent.
Can I use the same webhook handler for both Pages Router and App Router?
No. Pages Router uses NextApiRequest and NextApiResponse objects, while App Router uses the standard Web Request and Response APIs. The signature verification logic is the same, but the code for reading the raw body and returning responses is different. Pick the one that matches your project setup.
What happens if my Next.js webhook handler returns a non-200 status?
Paystack treats any non-2xx response as a failed delivery and will retry the webhook. It retries several times with increasing intervals. If all retries fail, the event is dropped and you can find it in your Paystack Dashboard webhook logs.
Do I need to handle GET requests on my webhook endpoint?
No. Paystack only sends POST requests to your webhook URL. You should reject GET requests with a 405 Method Not Allowed response. In the Pages Router, check req.method at the top of your handler.
Should I use Pages Router or App Router for new Paystack integrations?
If you are starting a new project, use the App Router. Route Handlers are simpler for webhooks because they use the standard Web Request API, which gives you request.text() for raw body access without any configuration. The Pages Router approach works fine for existing projects.

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

Verify Paystack Payments in Svelte and SvelteKit

Verify Paystack transactions in SvelteKit using server endpoints for popup verification and load functions for redirect callback verification. Covers amount validation, idempotent fulfillment, and SSR verification for instant results.

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

Assigning Dedicated Virtual Accounts to Users at Signup

For wallet-based apps and marketplaces, every user needs a dedicated virtual account from day one. This guide covers how to integrate DVA creation into your signup flow, handle failures gracefully, and design the UX so new users see their account number immediately after registration.

Read more

Paystack Split Not Applied to Transaction

You set up a split on Paystack, but the transaction settled entirely to your main account. The split was not applied. Here is why it happens and how to fix it.

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.