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
Understand why parsed bodies break signatures
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.
Fix for Express: use express.raw()
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.
1const express = require('express');2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);3const app = express();45// Webhook route FIRST — raw body6app.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_SECRET12 );13 // req.body is a Buffer here14 res.json({ received: true });15 }16);1718// JSON parsing for everything else AFTER19app.use(express.json());Expected result: Express serves the raw Buffer body to the webhook handler, and signature verification passes.
Fix for Next.js Pages Router: disable bodyParser
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.
1// pages/api/webhook.js2import { buffer } from 'micro';3import Stripe from 'stripe';45export const config = { api: { bodyParser: false } };67const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);89export default async function handler(req, res) {10 const buf = await buffer(req);11 const sig = req.headers['stripe-signature'];1213 const event = stripe.webhooks.constructEvent(14 buf, sig, process.env.STRIPE_WEBHOOK_SECRET15 );1617 // 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.
Fix for Next.js App Router: use request.text()
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.
1// app/api/webhook/route.js2import Stripe from 'stripe';3import { NextResponse } from 'next/server';45const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);67export async function POST(request) {8 const body = await request.text();9 const sig = request.headers.get('stripe-signature');1011 const event = stripe.webhooks.constructEvent(12 body, sig, process.env.STRIPE_WEBHOOK_SECRET13 );1415 // 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.
Fix for Fastify: use rawBody plugin
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.
1const fastify = require('fastify')();2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34// Register raw body plugin5await fastify.register(require('fastify-raw-body'), {6 field: 'rawBody',7 global: false,8 encoding: false,9 runFirst: true,10});1112fastify.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_SECRET18 );19 // Handle event...20 return { received: true };21});Expected result: Fastify exposes the raw body via request.rawBody, and signature verification succeeds.
Debug signature mismatches
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.
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));910 // 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
1require('dotenv').config();2const express = require('express');3const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);45const app = express();6const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;78// 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'];1314 if (!sig) {15 return res.status(400).json({ error: 'Missing stripe-signature header' });16 }1718 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 }2627 console.log(`Event verified: ${event.type} (${event.id})`);2829 // Handle the event30 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 }4041 res.json({ received: true });42 }43);4445// JSON parsing for all other routes46app.use(express.json());4748app.get('/', (req, res) => res.send('Webhook server'));4950const 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation