The error No signatures found matching the expected signature for payload means the raw bytes Stripe sent were modified before your code verified them. The fix depends on your framework.
The App Router uses the Web Fetch API. Call request.text() first — before anything else touches the body.
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
import { NextResponse } from 'next/server';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const secret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: Request) {
// ✓ Read raw body as text FIRST
const body = await request.text();
const sig = request.headers.get('stripe-signature');
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig!, secret);
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 400 });
}
// handle event...
return NextResponse.json({ received: true });
}
request.json() before verifying. Once the body stream is consumed as JSON, you cannot get the original raw bytes back. The re-serialized output differs in whitespace or key ordering.// pages/api/webhooks/stripe.ts
import { buffer } from 'micro';
export const config = { api: { bodyParser: false } };
export default async function handler(req, res) {
const rawBody = (await buffer(req)).toString('utf8');
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(rawBody, sig, secret);
} catch (err) {
return res.status(400).json({ error: err.message });
}
res.json({ received: true });
}
// Use express.raw() on the webhook route specifically
router.post('/stripe', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// req.body is a Buffer when using express.raw()
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
res.json({ received: true });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// In app.js — express.json() must come AFTER webhook routes
app.use('/api/webhooks', webhookRouter);
app.use(express.json());
Stripe has separate secrets for test and live mode — and the CLI generates a different secret from dashboard-created endpoints. Print your secret and compare the first 8 characters.
console.log('Secret prefix:', process.env.STRIPE_WEBHOOK_SECRET?.slice(0, 12));
If you use Clerk, NextAuth, or Better Auth — your middleware may be intercepting the webhook before your handler runs. See: Fix Stripe webhook Clerk middleware 401.
Generate the complete verified handler for your framework in one click.
Open WebhookFix →