DEV Community

Cover image for How to Handle Stripe and Paystack Webhooks in Next.js (The App Router Way)
Esimit Karlgusta
Esimit Karlgusta

Posted on

How to Handle Stripe and Paystack Webhooks in Next.js (The App Router Way)

The #1 reason developers struggle with SaaS payments is Webhook Signature Verification. You set everything up, the test payment goes through, but your server returns a 400 Bad Request or a Signature Verification Failed error.

In the Next.js App Router, the problem usually stems from how the request body is parsed. Stripe and Paystack require the raw request body to verify the signature, but Next.js often tries to be helpful by parsing it as JSON before you can get to it.

Here is the "Golden Pattern" for handling this in 2026.

1. The Route Handler Setup

Create a file at app/api/webhooks/route.ts. You must export a config object (if using older versions) or use the req.text() method in the App Router to prevent automatic parsing.

import { NextResponse } from "next/server";
import crypto from "crypto";

export async function POST(req: Request) {
  // 1. Get the raw body as text
  const body = await req.text();

  // 2. Grab the signature from headers
  const signature = req.headers.get("x-paystack-signature") || 
                    req.headers.get("stripe-signature");

  if (!signature) {
    return NextResponse.json({ error: "No signature" }, { status: 400 });
  }

  // 3. Verify the signature (Example for Paystack)
  const hash = crypto
    .createHmac("sha512", process.env.PAYSTACK_SECRET_KEY!)
    .update(body)
    .digest("hex");

  if (hash !== signature) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  // 4. Now parse the body and handle the event
  const event = JSON.parse(body);

  if (event.event === "charge.success") {
    // Handle successful payment in your database
    console.log("Payment successful for:", event.data.customer.email);
  }

  return NextResponse.json({ received: true }, { status: 200 });
}
Enter fullscreen mode Exit fullscreen mode

2. The Middleware Trap

If you have global middleware protecting your routes, ensure your webhook path is excluded. Otherwise, the payment provider will hit your login page instead of your API.

3. Why this matters for your SaaS

If your webhooks fail, your users won't get their "Pro" access, and your churn will skyrocket. Handling this correctly is the difference between a side project and a real business.

I have spent a lot of time documenting these "Gotchas" while building my MERN stack projects. If you want to see a full implementation of this including Stripe, Paystack, and database logic, check out my deep dive here: How to add Stripe or Paystack payments to your SaaS.

Digging Deeper

If you are tired of debugging the same boilerplate over and over, you might find my SassyPack overview helpful. I built it specifically to solve these "Day 1" technical headaches for other founders.

Happy coding!

Top comments (1)

Collapse
 
onlineproxyio profile image
OnlineProxy

Run this on Node.js and read the raw body once with req.arrayBuffer()-no double-dipping. Route by header: stripe-signature vs x-paystack-signature. For Stripe, feed the raw Buffer to stripe.webhooks.constructEvent with STRIPE_WEBHOOK_SECRET, for Paystack, HMAC-SHA512 the raw UTF-8 text with PAYSTACK_SECRET_KEY and compare using crypto.timingSafeEqual. Test like a boss with Stripe CLI and Paystack via ngrok/cloudflared + dashboard resend, log event.id, alert on non-2xx/latency, and rotate secrets by supporting old + new during rollout.