← Templates

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 deploy

What “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:

  1. 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.
  2. Use the endpoint’s signing secret, not your API key. It starts with whsec_ and is specific to one endpoint.
  3. 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.
  4. 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.

Deploy this template