A Stripe webhook endpoint with signature verification already written
Verify Stripe webhook signatures correctly — HMAC-SHA256, constant-time comparison, replay protection and secret rotation — in a deployable endpoint you can read in full before you trust it.
Signature verification is already written, and tested
Your webhook URL is public. Anyone who finds it can POST JSON at it, and a forged payment_intent.succeeded is worth real money to whoever gets one past you. This template verifies Stripe's signature in about sixty lines you can read in one sitting, with tests covering replay, tampering, secret rotation, malformed headers and truncated signatures.
The whole template is public — read it before you trust it.
Or scaffold it
$ npx wawesome init --template stripe-webhook
$ npx wawesome deployWhat “verifying the signature” actually means
Stripe signs every webhook it sends and puts the result in a Stripe-Signature
header. Verifying it is four things, and the fourth is the one that gets skipped:
- Sign the right bytes. The HMAC-SHA256 is computed over
${timestamp}.${rawBody}— the raw request body, byte for byte. Parsing the JSON and re-serialising it changes the bytes and every signature stops matching. - Use the endpoint’s signing secret, not your API key. It starts with
whsec_and is specific to one endpoint. - Compare in constant time. A
===on two hex strings leaks how many leading characters matched, which is enough to recover a valid signature one byte at a time. - Reject old timestamps. A signature stays valid forever unless you check the timestamp it was signed with. Without that, a single captured request can be replayed at you indefinitely.
This template does all four, and one more thing the docs mention and most code misses: during a secret rotation Stripe signs with every active secret, so the header carries several signatures and any one of them matching is enough.
The endpoint
POST / ──▶ verify Stripe-Signature ──▶ 200 {"received": true}
│
└── no match ──▶ 400
Events are typed on the way through: event.type narrows event.data.object to
the right Stripe type, so session.customer_details.email autocompletes and a
misspelling fails to compile rather than returning undefined in production.
The handler already covers checkout.session.completed,
payment_intent.succeeded, payment_intent.payment_failed and
customer.subscription.deleted.
Getting it live
npx wawesome init --template stripe-webhook
npx wawesome deploy
deploy prints the endpoint’s public URL. Paste it into Stripe Dashboard →
Developers → Webhooks → Add endpoint, copy the signing secret it hands back,
and store it:
npx wawesome env set STRIPE_WEBHOOK_SECRET whsec_... --secret
The secret only exists once the endpoint does, which is why it is set after the deploy rather than before it. Secrets are encrypted at rest and never readable back — not from the CLI, the dashboard, or the API — and are decrypted only for the moment your function runs.
Then send a test event from the Stripe dashboard and watch it arrive:
npx wawesome logs --follow
Why not copy this from a blog post
Because you would be copying security-critical code you have not read, from a
page that has no tests and no idea which Stripe SDK version you are on. The
version here is short on purpose: src/stripe-signature.ts is meant to be read
in full before you trust it, and src/stripe-signature.test.ts is there so you
can see exactly which attacks it was written against.
- stripe
- webhook
- payments
- typescript
Ready in about a minute
Sign in with GitHub, deploy, and get a public HTTPS endpoint.