Stripe + ExpressWebhook signature 400

Stripe webhook signature verification failed in Express: check the raw body

Stripe verifies the exact request bytes it sent. If `express.json()` parses the request before `stripe.webhooks.constructEvent()`, the handler no longer receives that raw payload and signature verification can fail. Mount the webhook with `express.raw({ type: 'application/json' })` before the general JSON parser, and use the endpoint's correct signing secret.

This guide covers check 05: Payments and transactional integrity in the Zenveus Production Readiness Standard.

For builders

What this means, in plain words

Stripe checks that a webhook arrived unchanged and came from the expected endpoint. A JSON parser can change the body before that check runs. Preserve the raw bytes, use the endpoint’s matching signing secret, and reject invalid signatures before processing payment data.

A scoped repair request

Using an AI builder? Paste this

Use this prompt in Lovable, Cursor, Replit, or Claude Code with the relevant server files available.

Fix my Express Stripe webhook signature failure. Mount the exact route with express.raw before any JSON parser, use its matching environment-specific endpoint secret, verify the signature before side effects, and durably accept verified events before returning success. Work on a branch with synthetic data and mocked external services. Show the smallest diff, identify required adapters and deployment settings, and add allowed and denied tests that prove side effects cannot happen before checks pass. Do not disable security checks to make a test pass.

The same failure may appear as

  • Stripe webhook signature verification failed
  • No signatures found matching the expected signature for payload
  • Webhook returns 400 after checkout
  • express.json parses the body before constructEvent

Find the failure layer

Run these checks before rewriting anything

Each check removes a class of causes. Keep the first failing result, its timestamp, and the production log beside it.

01

Failed delivery

Do not copy signing secrets into logs.

If this failsThe delivery targets the wrong URL or returns an error; confirm the configured endpoint and inspect that handler’s logs.
02

Parser order

Confirm the webhook route receives a `Buffer` of the original body before any JSON or URL-encoded parser changes it.

If this failsParsed JSON reaches constructEvent; mount raw-body handling before every applicable parser, including parent routers.
03

Endpoint secret

Local Stripe CLI forwarding and a dashboard-managed endpoint can use different secrets.

If this failsThe secret belongs to another endpoint or mode; update the deployment configuration and resend a test event.
04

Signature fixture

Verify that the authentic event succeeds and a tampered payload fails.

If this failsA valid event fails or a tampered event succeeds; keep processing stopped and isolate raw-body and secret mismatches.

Ranked diagnosis

Common root causes, in the order we would test them

01

JSON middleware runs first

express.json converts the webhook payload before Stripe verifies its original bytes.

02

Wrong endpoint secret

The handler uses a signing secret from another destination or environment.

03

Body was transformed by another layer

A proxy or parser changes whitespace, ordering, or encoding.

04

Test and live contexts are mixed

The CLI-forwarded endpoint and dashboard destination do not share the same signing secret.

Step-by-step repair

How to fix this in your app

Edit the Express entry point before app.use(express.json()) and the exact Stripe webhook handler. Check parsers on parent routers too.

Production safety ruleNever disable access controls, expose service keys, or add wildcard CORS as a routine shortcut.

01

Mount the raw-body handler first

Put app.post for the exact webhook path with express.raw({type: application/json}) above general JSON parsing. Pass req.body directly to constructEvent; do not parse and re-stringify it.

02

Use the destination signing secret

Read STRIPE_WEBHOOK_SECRET from server configuration. Use the Dashboard destination secret for deployed delivery and the stripe listen secret for CLI forwarding; they are different. Check environment and endpoint path without logging the value.

03

Reject invalid signatures before any write

Read stripe-signature, verify it using Stripe SDK, and return 400 when missing or invalid. After verification, persist the event to a durable inbox with a unique event ID before returning 2xx.

04

Test failure and replay separately

Forward a sandbox event using Stripe CLI, then send an unsigned fixture. Deliver the same valid event twice. The inbox and worker must prevent duplicate business effects while allowing failed processing to retry.

Implementation example

Express: raw payload and durable event inbox

// stripe is your initialized Stripe SDK client.
const secret = process.env.STRIPE_WEBHOOK_SECRET;
if (!secret) throw new Error('Webhook secret is missing');
app.post('/api/stripe/webhook', express.raw({ type: 'application/json' }),
  async (req, res) => {
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body, req.get('stripe-signature') || '', secret);
    } catch { return res.sendStatus(400); }
    try {
      await inbox.saveOnce(event.id, event); // durable unique event ID
      return res.sendStatus(200);
    } catch { return res.sendStatus(500); }
  });
app.use(express.json()); // other routes are mounted after this

Implement inbox.saveOnce in your database: an already-persisted event counts as success, while a storage failure must throw. A worker processes pending events and records completion only with the entitlement write.

Prove the repair

How to check that the fix worked

Run these checks with synthetic data in your test environment, then repeat the relevant acceptance checks after deployment.

  • Valid signed sandbox event: 200 and one inbox record.
  • Unsigned or altered payload: 400 and zero business writes.
  • Repeated event ID: no duplicate inbox record or entitlement.
  • Inbox storage unavailable: non-2xx so delivery can retry.

If the check still fails

If signatures still fail, check whether a parent middleware parsed the body, whether the CLI and Dashboard secrets were mixed, and whether a proxy transformed the bytes.

When the built-in AI fix makes it worse

Recover one reproducible failure.

Pause generated changes, restore a known working branch, and capture one failing request with its logs. Change one layer and rerun the allowed and denied checks before proceeding.

Engineering handoff

What Zenveus checks when the quick fix is not enough

We trace one production request through the complete path, isolate the failing boundary, and leave behind evidence your team can repeat.

Raw-body path

Every parser and parent router before the exact webhook handler.

Secret context

Endpoint, environment, CLI forwarding, and deployment configuration alignment.

Verified intake

Signature checking before durable event acceptance or payment side effects.

Fixture outcomes

Valid acceptance, tampered rejection, and durable-storage failure responses.

Typical repair pattern

Authentic events verify; altered payloads fail

A signed event reaches the handler with its raw body while a modified body returns a controlled 400.

Evidence left behind
  • Root-cause note
  • Verified production check
  • Rollback and prevention steps

Before the next release

Prevent this failure from returning

Keep webhook middleware order covered by an integration test.
Record event IDs and delivery outcomes without logging payment secrets.

Clear answers

Questions teams ask before they touch production

01Can I stringify the parsed JSON again?

No. Whitespace and key order may change, so the string may differ from Stripe's signed bytes.

02Is the `whsec_` value the same in local and production testing?

Not necessarily. Check the signing secret for the exact destination that sent the event.

03Should I skip signature verification to unblock payments?

No. Repair raw-body handling and test with signed fixtures.

04How do I know the fix worked in my app?

Run the verification checks on this page against your test environment, then repeat the relevant checks after deployment. Example code needs your app's authentication, data model, and configuration; reading the guide alone does not verify your deployment.

Official documentation and library references

Free next step

Check the boundary before you hand it over

The free tool helps you inspect this symptom. Its result does not establish whether the whole app is production ready.

The Verdict

Know whether the symptom is contained or structural.

We can see the symptom from here. What we cannot tell you from outside is whether it is contained or structural. A scanner collects evidence. A named senior engineer makes the decision. For $299, a named senior engineer reads your code and signs a written Verdict against the nine checks in the Zenveus Production Readiness Standard. The 48-hour clock begins when the required access and context are available. If the report does not give your developer a list they can act on, you do not pay.

The 48-hour clock starts when the required access and context are available.

Scroll to Top