ShipSafeFull checklist (23 checks)

Payments

Verify the Stripe webhook signature

Stripe webhook signature verification with stripe.webhooks.constructEvent, the raw request body and your whsec_ secret, with a Next.js example.

Why it matters

Your webhook URL is public. Without signature verification, anyone can POST a fake checkout.session.completed event and unlock paid features for free.

How to fix it

  1. Get the endpoint's signing secret (whsec_) from the Stripe dashboard or the Stripe CLI.
  2. Read the raw request body as text. Parsed and re-serialised JSON will not match the signature.
  3. Call stripe.webhooks.constructEvent and return 400 if it throws.
  4. Handle events idempotently, because Stripe can deliver the same event more than once.
// app/api/stripe/webhook/route.ts
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text(); // raw body
  const sig = req.headers.get("stripe-signature")!;
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    return new Response("Bad signature", { status: 400 });
  }
  // handle event.type ...
  return new Response(null, { status: 200 });
}

How to verify

Questions

Why does verification fail even with the right secret?
Usually the body was parsed as JSON first. Use the raw body, and the secret for that exact endpoint and mode.

Related checks

Tick it off in the full checklist

Get new checks by email

New checks and real leak post-mortems, about once a month. No spam.