OneShot

All packsSaaS Billing + Tax CoreSolve

stripe webhook signature verification nextjs app router raw body

constructEvent needs the exact bytes, not re-serialized JSON

Stripe's signature check is computed over the raw request body -- parsing it to JSON first and re-serializing breaks verification in a way that's easy to miss (D3).

3 of 3 runs passedOne decision inside SaaS Billing + Tax Core (nextjs-prisma-postgres).

What goes wrong

Stripe's webhook signature is computed over the exact raw bytes of the request body. A route handler that calls request.json() first (to inspect the event) and then passes the re-serialized object to constructEvent() will fail signature verification intermittently or always, depending on whitespace and key-order differences introduced by re-serialization -- and the failure looks like a configuration problem, not a code-order problem.

Why agents get it wrong

Reading the body as JSON first is the natural instinct for any handler that needs to inspect a webhook's event type before deciding what to do -- and in a framework with an automatic body parser, there's often no other obvious way to get the payload. Next.js App Router route handlers hand you a Web-standard Request specifically so this isn't necessary, but that only helps if the handler is written to take advantage of it.

What SaaS Billing + Tax Core pre-decides

D3 pins the order explicitly: await req.text() before any JSON parsing, so constructEvent() gets the exact bytes Stripe signed. A missing or invalid stripe-signature header returns 400; a handler exception returns 500 so Stripe retries; everything else -- including event types the app doesn't handle -- returns 200, because Stripe disables an endpoint after enough non-2xx responses.

Source: ARCHITECTURE.md D3

Coming soon — $149Full receipt and details for SaaS Billing + Tax Core

Related problems