DEV Community

SaaSFactory
SaaSFactory

Posted on

Paste your Stripe webhook handler here: 7 checks that run in this post

The most-viewed Stripe question on Stack Overflow right now is not a subtle one. It is Webhook payload must be provided as a string or a Buffer25,121 views, 13 answers. Right behind it: NextJS Stripe Webhook Raw Body (5,492 views) and Express 4.17 raw body for one endpoint (5,504 views).

Same root cause every time, and it is only rule 1 of the 7 things that go wrong in webhook handlers. So instead of another checklist you have to read: paste your handler below and press Run.

It ships with a deliberately broken Express handler so you can see output immediately — replace it with yours. Everything is plain JS in your browser: your code is not uploaded anywhere, there is no backend, and it never asks for a key or a whsec_ secret.

If the embed does not load, open it directly: codesandbox.io/s/nkr9g7.

Next to the tool: the same 7 rules, run by a human against your whole repo (not one pasted file), with file:line references and the fix for each finding, are what we sell as the Stripe Integration Audit — $39, delivered in 48h, full refund if the repo cannot be reviewed. The tool above is free and stays free.

The 7 rules it runs

  1. Raw body must reach constructEvent. Stripe signs the exact bytes it sent. express.json(), bodyParser.json(), request.get_json() or await req.json() hand you a re-serialized object — key order, spacing and unicode escaping all change, so the HMAC can never match. Fix: express.raw({ type: 'application/json' }) on that route, mounted before any global JSON parser; request.get_data() in Flask; await req.text() in a Next.js route handler.
  2. The signature is actually verified. No constructEvent / construct_event in the file means anyone who learns your URL can POST a fake checkout.session.completed.
  3. No hardcoded sk_ / rk_ / whsec_ literals. Each endpoint (CLI forwarding, test, live) has its own signing secret, so a hardcoded one is both a leak and a guaranteed production failure.
  4. idempotencyKey on create calls. Stripe retries any delivery that times out or answers non-2xx. Without an idempotency key derived from event.id, a retry charges twice.
  5. De-duplication by event.id. At-least-once delivery is a guarantee, not an edge case. If event.id never appears in your handler, duplicates are invisible to you.
  6. Amount never read from request data. amount: req.body.amount is editable by the caller. Derive it server-side.
  7. No legacy Charges/Sources API. No SCA/3DS support means European cards get declined.

What it cannot see

Static pattern checks on one pasted file. It does not know your env vars, your proxy, whether that express.json() is mounted on a different router, or what your database does with event.id. A clean result means "none of the 7 known shapes are visible here", not "correct". That limitation is exactly why the paid version is a human reading the repo.

If it flags something on your code that is actually fine — or misses something it should have caught — tell me in the comments with the snippet and I will fix the rule.

Top comments (0)