Fix: Stripe Webhook Signature Verification Failed

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.

Root cause: Your framework parsed the request body to JSON before you could read it as raw text. Signature verification requires the exact original bytes — even one character difference fails the HMAC check.

Fix for Next.js App Router

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 });
}
Never call 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.

Fix for Next.js Pages Router

// 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 });
}

Fix for Express

// 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());

Other common causes

Check 1
Wrong signing secret

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));
Check 2
Auth middleware blocking the route

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 →
For informational purposes only. Always test in staging before production. MetricLogic accepts no responsibility for issues arising from use of these tools. © 2026 MetricLogic.
HttpFixer by MetricLogic · Blog · All Tools · Generators MIT · GitHub →