Failed delivery
Do not copy signing secrets into logs.
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
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
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
Find the failure layer
Each check removes a class of causes. Keep the first failing result, its timestamp, and the production log beside it.
Do not copy signing secrets into logs.
Confirm the webhook route receives a `Buffer` of the original body before any JSON or URL-encoded parser changes it.
Local Stripe CLI forwarding and a dashboard-managed endpoint can use different secrets.
Verify that the authentic event succeeds and a tampered payload fails.
Ranked diagnosis
express.json converts the webhook payload before Stripe verifies its original bytes.
The handler uses a signing secret from another destination or environment.
A proxy or parser changes whitespace, ordering, or encoding.
The CLI-forwarded endpoint and dashboard destination do not share the same signing secret.
Step-by-step repair
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.
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.
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.
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.
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
// 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 thisImplement 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
Run these checks with synthetic data in your test environment, then repeat the relevant acceptance checks after deployment.
When the built-in AI fix makes it worse
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
We trace one production request through the complete path, isolate the failing boundary, and leave behind evidence your team can repeat.
Every parser and parent router before the exact webhook handler.
Endpoint, environment, CLI forwarding, and deployment configuration alignment.
Signature checking before durable event acceptance or payment side effects.
Valid acceptance, tampered rejection, and durable-storage failure responses.
Typical repair pattern
A signed event reaches the handler with its raw body while a modified body returns a controlled 400.
Before the next release
Clear answers
No. Whitespace and key order may change, so the string may differ from Stripe's signed bytes.
Not necessarily. Check the signing secret for the exact destination that sent the event.
No. Repair raw-body handling and test with signed fixtures.
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.
Related symptoms
Official documentation and library references
Free next step
The free tool helps you inspect this symptom. Its result does not establish whether the whole app is production ready.
The Verdict
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.
Free decision aid
Get a senior view of the constraint, the evidence you have, and the next decision that removes the most risk.
No email required for this decision aid. Dismiss once and this popup stays closed for the session.