Stripe webhook works locally but not in production
Start with Stripe's delivery evidence, not a code rewrite. Separate events that were never sent from requests that failed at the edge and requests your handler accepted but processed incorrectly.
In Stripe Workbench, open the live endpoint and inspect Event deliveries. If no delivery exists, verify account, mode, endpoint scope, and selected events. If delivery failed, use its exact status: fix redirects, TLS, method, auth, or timeout first. For signature errors, supply the unmodified request body, the Stripe-Signature header, and that live endpoint's whsec_ secret. Then make processing idempotent, order-independent, and explicit about delayed payments and subscription status.
Diagnostic path
First decide where the event stopped
A browser redirect, a Checkout success page, an application log, and a database row are different signals. Use the live endpoint's Event deliveries in Stripe Workbench as the starting point. Stripe shows whether each delivery is Delivered, Pending, or Failed, plus the HTTP status and retry timing.
| Observed state | Most likely boundary | Next evidence |
|---|---|---|
| No delivery | Wrong account or mode, wrong endpoint scope, event type not selected, or the expected Stripe event never occurred. | Compare the Checkout Session or invoice mode, endpoint destination, event scope, and enabled event list. |
| Unable to connect, TLS, or 3xx | Stripe cannot reach the registered final HTTPS endpoint. | Use the exact endpoint URL, certificate state, and final response without redirects. |
| 400 or 403 | Method, access control, CSRF, payload parsing, signature, or endpoint-secret failure. | Inspect the handler's first rejection and confirm the raw payload, header, and endpoint secret. |
| 500 or timeout | The endpoint ran but failed or took too long before returning a successful response. | Correlate Stripe's event ID and delivery time with server logs; move expensive work out of the request path. |
| 2xx but no order or access | The edge accepted the event, but business logic, database authorization, duplicate handling, or event selection failed. | Trace the event ID through logs, processed-event storage, the intended write, and the resulting customer state. |
The production-only checks
Match the account, mode, endpoint, and objects
Stripe test and live data are separate. Confirm that the production Checkout Session, Price, Product, customer, and endpoint all exist in live mode. A test endpoint does not become a live endpoint when the app deploys.
Use the signing secret for this exact endpoint
Stripe generates a unique whsec_ secret per endpoint and mode. The secret printed by stripe listen is for CLI-forwarded events, not a Dashboard-managed production endpoint. A copied secret from another endpoint can look plausible and still fail every signature.
Check the registered final URL
Use the stable production URL that receives the POST directly. Stripe treats redirects as delivery failures. Preview domains, changing deployment URLs, stale route names, and authentication middleware are common deployment-only differences.
Fix the three inputs to signature verification
Stripe's official libraries verify three values: the unmodified request body, the Stripe-Signature header, and the signing secret for the endpoint. Debug those values as a set.
- Raw body: verify before JSON parsing or any middleware that changes bytes, whitespace, key order, or encoding.
- Signature header: confirm it reaches the handler and resembles Stripe's timestamped signature format.
- Endpoint secret: load the production endpoint's secret from the production environment. Do not log or expose it.
In Express, middleware order matters: the webhook route must receive the raw body before a general express.json() parser. Stripe publishes separate working examples for Next.js App Router and Pages Router. In Python frameworks, read the request bytes rather than a parsed JSON object.
stripe listen --events checkout.session.completed,checkout.session.async_payment_succeeded,invoice.paid,invoice.payment_failed --forward-to localhost:4242/webhook stripe trigger checkout.session.completed
Local forwarding proves the local handler with the CLI secret. It does not prove that the live endpoint URL, live secret, production variables, or live event selection are correct.
Let the HTTP result narrow the fix
- 3xx: register the final destination instead of relying on a redirect.
- 4xx: confirm POST support, public reachability, route-level access controls, CSRF configuration, request parsing, and signature inputs.
- 5xx: find the first server exception associated with the event ID.
- Timeout: acknowledge quickly and move slow fulfillment to an asynchronous worker or durable queue.
- TLS: verify a valid certificate chain and TLS 1.2 or newer.
Do not turn every failure into 200 OK. A successful delivery response should mean the event was safely accepted for processing. Otherwise Stripe loses the signal it uses for retries.
Make the handler safe after delivery succeeds
Make each effect idempotent
Stripe can retry an event and can occasionally deliver a duplicate. Record processed event IDs, enforce a unique business key where possible, and make order creation, entitlement changes, email, and ledger updates safe to run again.
Do not depend on event order
Stripe does not guarantee delivery order. If one event arrives before its expected predecessor, retrieve the current Stripe object or defer processing instead of assuming a fixed sequence.
Model Checkout and subscriptions as state
Do not grant access solely because the browser reached success_url. Fulfillment belongs behind verified server evidence. Delayed payment methods can complete later, and subscriptions change through events such as invoice.paid, invoice.payment_failed, and customer.subscription.updated.
Replay a known failure
After the fix, resend the failed event from Workbench or the Stripe CLI, then verify both the delivery and the resulting application state. Replay it again to prove the second attempt does not duplicate the effect.
Keep a production webhook evidence record
Minimum useful fields
- Stripe event ID, event type, mode, and endpoint.
- Delivery timestamp, response status, and application correlation ID.
- Signing-secret source by endpoint name, never the secret value.
- Processed-event record and the resulting order, invoice, subscription, or entitlement ID.
- Replay result and duplicate-safety result.
- Any state or event path that remains unverified.
This guide helps diagnose integration behavior; it is not a payment-security certification. Review live account configuration and high-risk fulfillment with a qualified engineer.
Turn the code review into a repeatable check
Stripe Webhook & Checkout DoctorRead-only inspection of Next.js, Express, FastAPI, Python, TypeScript, and serverless code for signature, raw-body, idempotency, Checkout URL, and subscription-state review targets. It reports evidence and suggested fixes but does not call Stripe or touch a live account.See the diagnostic skill →Common questions
Why does my Stripe webhook work locally but not in production?
Compare the live endpoint and its Event deliveries with the local CLI listener. The production environment needs its own registered endpoint, matching live event selection, live signing secret, raw-body handling, and reachable final HTTPS URL.
How do I know whether Stripe sent the event?
Use Event deliveries on the live endpoint in Stripe Workbench. No delivery and a failed delivery are different problems. The delivery record supplies the status, timestamp, and retry evidence needed to choose the next check.
Why does signature verification fail even with a whsec_ value?
The value can belong to the wrong mode, endpoint, or CLI listener. Verification also fails if middleware parsed or changed the request body before Stripe's library received it, or if the signature header did not reach the handler.
Can I use the Checkout success page as proof of payment?
Do not use the browser redirect as the sole fulfillment signal. Confirm the relevant server-side Stripe state and verified webhook path, including delayed payment and subscription events where applicable.