Skip to main content
GullySystem
A practical guide

Stripe webhooks not working: why orders do not update

Ganesh HSStrategy and Technology, GullySystem
Practical advice · 4 min read

In this guide 6 sections
The short answer

Orders stay pending when Stripe’s event never reaches your handler or your handler rejects it. Look for five causes. A signature check run on a parsed body. The wrong secret for the environment. A proxy block. A slow reply that triggers retries. Unguarded duplicate events.

Start in the Stripe dashboard, not your code

Open the webhook endpoint in the dashboard and read the delivery log. Each event shows the response your server returned. A 400 points at signature or parsing. A 404 means a wrong path. A timeout means slow work inside the handler.

No attempts at all? Stripe is calling a different URL than you think. Check that the endpoint lists the event types you handle, such as checkout.session.completed or payment_intent.succeeded. Missing event types are a quiet cause of pending orders.

  • Response code on each delivery
  • Event type subscribed to the endpoint
  • Live mode versus test mode toggle
  • Time of the first failed attempt

The signature check fails on a parsed body

Stripe signs the exact bytes it sent. The Stripe-Signature header is verified against that raw payload. If Express runs its JSON middleware first, the body is re-serialised and the check fails every time.

The fix is to mount express.raw on the webhook route only, ahead of any global express.json call. In Laravel or plain PHP, read php://input directly. Never pass a decoded array into the verification call.

Also check for middleware that trims whitespace, rewrites encodings or adds a CSRF token requirement. A webhook route has no browser session, so exempt it from CSRF.

Wrong endpoint or secret for the environment

Each endpoint has its own signing secret, which starts with whsec_. Test mode and live mode have separate endpoints and secrets. Pasting the staging secret into production is the most common deployment slip.

The Stripe CLI listen command prints a third secret for local forwarding. It works for that session only. Keep the three secrets in separate environment variables and name them clearly so nobody swaps them.

  • Production endpoint secret
  • Staging endpoint secret
  • Local CLI forwarding secret
  • API key mode matching the secret mode

Firewalls, Nginx rules and slow replies

If the log shows connection errors, look at the path between Stripe and your app. A WAF rule, a basic auth block, a geo filter or an Nginx location that returns 403 will all stop events. Allow the webhook path without login.

Reply fast. Stripe expects a 2xx status quickly, and it retries any delivery that gets no reply, a 5xx error or a timeout. Do the heavy work, such as emailing invoices or syncing stock, in a queue. Acknowledge first. Process after.

A slow handler also sets a trap. The first attempt is still running when the retry arrives, and both try to mark the same order paid.

Handle duplicates and order of arrival

Stripe may deliver an event more than once, and events can arrive out of order. Store each event id in a table with a unique constraint. Id already there? Return success and skip.

Do not trust arrival order. A payment_intent.succeeded event can land before your order row has its Stripe reference. Look the order up by metadata you set at checkout, and fetch the object from the API when in doubt. Then replay one failed event from the dashboard and watch the order flip.

Webhook triage sheet

One page, with a row for every failed delivery. Columns record event id, event type, response code, environment, secret used, time to reply and whether the order was already paid. A closing block lists the five checks in order: endpoint URL, subscribed events, raw body, secret, duplicate guard.

Open a blank worksheet to print

Questions owners ask

Why does Stripe show a 400 on my webhook?

Usually signature verification failed. The cause is almost always a body that was parsed or altered before verification, or a secret copied from the wrong endpoint.

Can I test webhooks without deploying?

Yes. The Stripe CLI forwards events to a local port and prints its own signing secret. Use that secret only for the local session.

Should the webhook or the redirect mark an order paid?

The webhook. A customer can close the tab before the success page loads. Treat the redirect as a display step and the event as the record.

How do I replay a missed event?

Open the event in the dashboard and resend it to the endpoint once the fix is live. A unique event id table keeps the replay from double-processing.

Browse all guidesBack to top ↑
Next step

Have a specific situation to work through?

This article covers the general case. Tell us what you’re actually dealing with and we’ll respond directly.

Share Your Requirements