Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to fix Stripe webhook invalid signature error

The Stripe webhook invalid signature error means the raw request body used for signature verification doesn't match what Stripe sent. This happens when your framework parses the body before constructEvent runs. Fix it by using framework-specific raw body handling: express.raw() for Express, bodyParser: false for Next.js, or rawBody for Fastify.

What you'll learn

  • Why parsed bodies break Stripe webhook signature verification
  • How to preserve raw bodies in Express, Next.js, and Fastify
  • How to debug signature mismatches step by step
  • How to test webhook signatures locally with the Stripe CLI
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read20 minutesStripe API v2024-12+, Express 4+, Next.js 13+, Fastify 4+March 2026RapidDev Engineering Team
TL;DR

The Stripe webhook invalid signature error means the raw request body used for signature verification doesn't match what Stripe sent. This happens when your framework parses the body before constructEvent runs. Fix it by using framework-specific raw body handling: express.raw() for Express, bodyParser: false for Next.js, or rawBody for Fastify.

Why Your Webhook Signature Is Invalid

Stripe computes an HMAC-SHA256 signature from the exact bytes of the request body it sends. When your web framework automatically parses the body into JSON, those original bytes are lost. Re-serializing the parsed object with JSON.stringify may produce different whitespace, key ordering, or Unicode escaping. The result: the recomputed signature doesn't match, and constructEvent throws an error. The fix is framework-specific — you need to capture the raw body before any parsing occurs.

Prerequisites

  • A Stripe webhook endpoint returning signature errors
  • Node.js 18+ with your web framework (Express, Next.js, or Fastify)
  • Your webhook signing secret (whsec_) from the Stripe Dashboard or CLI

Step-by-step guide

1

Understand why parsed bodies break signatures

Stripe signs the exact bytes it sends. JSON.parse() then JSON.stringify() can change the byte sequence: key order may differ, Unicode escapes may be normalized, and whitespace may change. Even one byte difference means a completely different HMAC signature.

Expected result: You understand that the raw body bytes must be identical to what Stripe sent for the signature to match.

2

Fix for Express: use express.raw()

For Express, add express.raw({ type: 'application/json' }) to your webhook route. This stores the body as a Buffer instead of parsing it. Critical: this middleware must come before any express.json() call.

typescript
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3const app = express();
4
5// Webhook route FIRST — raw body
6app.post('/webhook',
7 express.raw({ type: 'application/json' }),
8 (req, res) => {
9 const sig = req.headers['stripe-signature'];
10 const event = stripe.webhooks.constructEvent(
11 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
12 );
13 // req.body is a Buffer here
14 res.json({ received: true });
15 }
16);
17
18// JSON parsing for everything else AFTER
19app.use(express.json());

Expected result: Express serves the raw Buffer body to the webhook handler, and signature verification passes.

3

Fix for Next.js Pages Router: disable bodyParser

Next.js API routes parse the body by default. Export a config object to disable it, then use the 'micro' package's buffer() function to read the raw body.

typescript
1// pages/api/webhook.js
2import { buffer } from 'micro';
3import Stripe from 'stripe';
4
5export const config = { api: { bodyParser: false } };
6
7const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
8
9export default async function handler(req, res) {
10 const buf = await buffer(req);
11 const sig = req.headers['stripe-signature'];
12
13 const event = stripe.webhooks.constructEvent(
14 buf, sig, process.env.STRIPE_WEBHOOK_SECRET
15 );
16
17 // Handle event...
18 res.json({ received: true });
19}

Expected result: Next.js Pages Router API route reads the raw body with micro's buffer(), and signature verification passes.

4

Fix for Next.js App Router: use request.text()

In the Next.js App Router (route.js files), use the Web API's request.text() method to get the raw body as a string.

typescript
1// app/api/webhook/route.js
2import Stripe from 'stripe';
3import { NextResponse } from 'next/server';
4
5const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
6
7export async function POST(request) {
8 const body = await request.text();
9 const sig = request.headers.get('stripe-signature');
10
11 const event = stripe.webhooks.constructEvent(
12 body, sig, process.env.STRIPE_WEBHOOK_SECRET
13 );
14
15 // Handle event...
16 return NextResponse.json({ received: true });
17}

Expected result: Next.js App Router route handler gets the raw body string and verifies the signature correctly.

5

Fix for Fastify: use rawBody plugin

Fastify doesn't expose the raw body by default. Use the fastify-raw-body plugin or access request.rawBody after configuring the content type parser.

typescript
1const fastify = require('fastify')();
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4// Register raw body plugin
5await fastify.register(require('fastify-raw-body'), {
6 field: 'rawBody',
7 global: false,
8 encoding: false,
9 runFirst: true,
10});
11
12fastify.post('/webhook', {
13 config: { rawBody: true },
14}, async (request, reply) => {
15 const sig = request.headers['stripe-signature'];
16 const event = stripe.webhooks.constructEvent(
17 request.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET
18 );
19 // Handle event...
20 return { received: true };
21});

Expected result: Fastify exposes the raw body via request.rawBody, and signature verification succeeds.

6

Debug signature mismatches

If you're still getting errors, add diagnostic logging to identify the root cause. Check the body type, length, and first few characters. Teams at RapidDev use this debugging approach when integrating Stripe webhooks for their clients.

typescript
1app.post('/webhook',
2 express.raw({ type: 'application/json' }),
3 (req, res) => {
4 console.log('Body type:', typeof req.body);
5 console.log('Is Buffer:', Buffer.isBuffer(req.body));
6 console.log('Body length:', req.body.length);
7 console.log('First 100 chars:', req.body.toString().substring(0, 100));
8 console.log('Signature header:', req.headers['stripe-signature']?.substring(0, 30));
9
10 // Proceed with verification...
11 }
12);

Expected result: Diagnostic logs reveal whether the body is raw (Buffer) or parsed (object), helping you pinpoint the middleware issue.

Complete working example

webhook-raw-body.js
1require('dotenv').config();
2const express = require('express');
3const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
4
5const app = express();
6const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
7
8// Webhook route — raw body MUST come before express.json()
9app.post('/webhook',
10 express.raw({ type: 'application/json' }),
11 (req, res) => {
12 const sig = req.headers['stripe-signature'];
13
14 if (!sig) {
15 return res.status(400).json({ error: 'Missing stripe-signature header' });
16 }
17
18 let event;
19 try {
20 event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
21 } catch (err) {
22 console.error('Webhook signature failed:', err.message);
23 console.error('Body type:', typeof req.body, 'Is Buffer:', Buffer.isBuffer(req.body));
24 return res.status(400).json({ error: `Signature verification failed: ${err.message}` });
25 }
26
27 console.log(`Event verified: ${event.type} (${event.id})`);
28
29 // Handle the event
30 switch (event.type) {
31 case 'payment_intent.succeeded':
32 console.log('Payment succeeded:', event.data.object.amount);
33 break;
34 case 'invoice.payment_failed':
35 console.log('Invoice payment failed:', event.data.object.id);
36 break;
37 default:
38 console.log('Unhandled event:', event.type);
39 }
40
41 res.json({ received: true });
42 }
43);
44
45// JSON parsing for all other routes
46app.use(express.json());
47
48app.get('/', (req, res) => res.send('Webhook server'));
49
50const PORT = process.env.PORT || 3000;
51app.listen(PORT, () => console.log(`Listening on port ${PORT}`));

Common mistakes when fixing Stripe webhook invalid signature error

Why it's a problem: Placing app.use(express.json()) before the webhook route

How to avoid: Move the webhook route with express.raw() ABOVE the global express.json() middleware. Express processes middleware in order.

Why it's a problem: Using JSON.stringify(req.body) as the body argument to constructEvent

How to avoid: Never re-serialize the body. Use the original raw body (Buffer or string). JSON.stringify produces different bytes than the original request.

Why it's a problem: Using request.json() instead of request.text() in Next.js App Router

How to avoid: request.json() parses the body. Use request.text() to get the raw string body for signature verification.

Why it's a problem: Not installing the micro package for Next.js Pages Router

How to avoid: Run npm install micro. The buffer() function from micro reads the raw body stream when Next.js body parsing is disabled.

Best practices

  • Always use framework-specific raw body handling — never rely on re-serialization
  • Place the webhook route before any global body parsing middleware
  • Add diagnostic logging during development to verify the body type is Buffer/string
  • Test locally with the Stripe CLI to catch signature issues before deploying
  • Store the webhook secret in environment variables with no extra whitespace
  • Use separate webhook endpoints for different event categories when possible
  • Monitor webhook delivery status in the Stripe Dashboard for production issues

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Show me how to handle Stripe webhook signature verification in Express, Next.js Pages Router, Next.js App Router, and Fastify. Each framework needs different raw body handling. Include the specific middleware or config needed to preserve the raw request body for stripe.webhooks.constructEvent.

Stripe Prompt

Create Stripe webhook handlers for Express, Next.js (Pages and App Router), and Fastify that correctly preserve raw request bodies for signature verification. Show the framework-specific configuration needed for each.

Frequently asked questions

Why does JSON.stringify not work for Stripe webhook verification?

JSON.stringify may produce different bytes than the original body: key ordering, whitespace, and Unicode escaping can all differ. Stripe signs the exact bytes it sends, so even one byte difference breaks the signature.

How do I get the raw body in Express?

Use express.raw({ type: 'application/json' }) as middleware on your webhook route. This gives you req.body as a Buffer instead of a parsed object. Place this route before any express.json() middleware.

How do I get the raw body in Next.js App Router?

Use request.text() in your route handler (app/api/webhook/route.js). This returns the raw body as a string without JSON parsing.

Do I need the micro package for Next.js?

For the Pages Router, yes — micro's buffer() function reads the raw body when bodyParser is disabled. For the App Router, use the built-in request.text() method instead.

Can I use body-parser instead of express.raw()?

Yes, body-parser has a verify callback that stores the raw body: bodyParser.json({ verify: (req, res, buf) => { req.rawBody = buf; } }). Then use req.rawBody in constructEvent. However, express.raw() on just the webhook route is simpler and more explicit.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.