Verifying Stripe webhook signatures
Updated 2026-08-04
A webhook endpoint is a public URL that accepts POSTs. Without signature verification, anyone who discovers it can post whatever they like — and your code will believe it.
For a notification relay that means fake sale alerts. For anything that grants access or credits an account, it means a vulnerability.
The check
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function handler(request) {
const sig = request.headers.get("stripe-signature");
const raw = await request.text(); // RAW bytes, not JSON
let event;
try {
event = stripe.webhooks.constructEvent(
raw, sig, process.env.STRIPE_WEBHOOK_SECRET,
);
} catch (err) {
return new Response(`invalid signature: ${err.message}`, { status: 400 });
}
switch (event.type) {
case "invoice.paid":
case "payment_intent.succeeded":
await notify(event);
break;
}
return new Response("ok", { status: 200 });
}
The mistake everyone makes
Parsing the body before verifying.
The signature is computed over the exact bytes Stripe sent. If your framework
parses JSON automatically — Express's express.json() is the classic — you get
an object, and re-serialising it produces different bytes: different key order,
different whitespace. The signature then fails and you conclude the secret is
wrong.
In Express:
app.post("/webhook",
express.raw({ type: "application/json" }), // NOT express.json()
handler);
In a Worker or edge function, use request.text() and never request.json()
before the check.
Where the secret comes from
Stripe → Developers → Webhooks → your endpoint → Signing secret. It
starts whsec_. It is per-endpoint, so a test endpoint and a live endpoint have
different secrets.
Idempotency
Stripe retries failed deliveries, so you will receive duplicates. Key on
event.id and skip anything already processed — otherwise a transient timeout
becomes a double notification, or worse, a double credit.
Return 200 quickly
Stripe treats a slow or failing response as a delivery failure and retries. Acknowledge first, do the work after — or at least keep the handler fast. Long processing inside the handler produces a retry storm.
Other platforms
Lemon Squeezy and Gumroad both sign webhooks too, with different header names and algorithms. Same principle: verify against the raw body before trusting anything.
If you'd rather not run any of this, FRGMNT surfaces a webhook URL in the Vault that points at a stateless relay — it forwards a push notification and stores nothing. See revenue webhooks without a server.
Frequently asked
Do I have to verify Stripe webhook signatures?
If your endpoint does anything consequential, yes. Without verification anyone who discovers the URL can post fabricated events, and your application will treat them as real.
Why does signature verification fail even with the right secret?
Almost always because the body was parsed before verification. The signature covers the exact raw bytes, so parsing to JSON and re-serialising changes them and the check fails.
Read next
- Revenue webhooks without a serverYou don't need to run infrastructure to receive Stripe webhooks. Serverless functions, relays and hosted route…
- Get a push notification for every Stripe paymentFour ways to be told the moment a Stripe payment lands — email, Slack, Stripe's own app, or a unified pager ac…
- The CLI and MCP serverQuery True MRR, today's cash and transactions from your shell — or let an AI assistant read your numbers.…