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
- Get the endpoint's signing secret (
whsec_) from the Stripe dashboard or the Stripe CLI. - Read the raw request body as text. Parsed and re-serialised JSON will not match the signature.
- Call
stripe.webhooks.constructEventand return 400 if it throws. - 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
curl -X POSTa made-up event to your webhook URL; it must return 400.- Run
stripe trigger checkout.session.completedwith the CLI; it should succeed.
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
- Keep Stripe test and live keys apart
- Decide prices and access on the server
- Check permissions on the server
Tick it off in the full checklist