JustHandled Labs
// payments troubleshooting guide

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.

The short version

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 stateMost likely boundaryNext evidence
No deliveryWrong 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 3xxStripe cannot reach the registered final HTTPS endpoint.Use the exact endpoint URL, certificate state, and final response without redirects.
400 or 403Method, 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 timeoutThe 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 accessThe 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

1

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.

2

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.

3

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

4

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.

5

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.

6

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.

7

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.

Primary references

Find the production-only Stripe mistake.

Stripe Webhook & Checkout Doctor creates an evidence-backed review queue from code, logs, and configuration examples. It starts read-only and cannot verify the live Stripe Dashboard for you.

Get Stripe Webhook & Checkout Doctor on Agensi