Stripe Webhook Not Firing: A Diagnostic Checklist
Open the Stripe event log first. Which of the three things you are looking at tells you which of three unrelated problems you have.
Go to Developers, then Event destinations, and open your endpoint. What you see there splits this into three unrelated problems, and treating the wrong one wastes the most time.
Events listed with failed deliveries means Stripe is trying and your endpoint is rejecting. Events listed as delivered successfully while nothing happened in your app means your handler is the problem. No events at all means Stripe is not sending, which is the worst case and the least obvious.
Case 1: deliveries are failing
Click into a failed attempt and read the response body Stripe recorded. The status code narrows it immediately.
| What Stripe recorded | Almost always means |
|---|---|
| 400 with a signature error | Raw body was parsed before verification |
| 401 or 403 | Auth middleware or CSRF protection on the route |
| 419 | Laravel CSRF: add the route to the except array |
| 404 | Route not registered, or the URL has a typo or trailing slash |
| 500 | Your handler threw; the response body has the trace |
| Timeout | Handler is doing work inline instead of queueing |
| TLS or connection error | Certificate expired, or firewall blocking Stripe |
The signature error is the most common by a wide margin. The exact string is that no signatures were found matching the expected signature for payload, and it means one of three things.
Either you are using the wrong signing secret, or your framework parsed and re-serialised the JSON before verification, or you are mixing test and live. Each endpoint has its own whsec_ value, and the same endpoint has different secrets in test and live mode.
Case 2: delivered, but nothing happened
This is the failure that looks healthiest. Stripe shows 200 on every event and your dashboard is green, because the recommended pattern is to return 200 immediately and process asynchronously.
Once you return 200, Stripe stops caring. If your queue worker is dead, the events are acknowledged and discarded, and there is no signal anywhere that they were not handled.
Check the queue depth before you check anything else in your code. A growing queue with no consumers explains this entire class of report and takes ten seconds to confirm.
Case 3: no events at all
Work through these in order. The first two cover most cases.
Check whether you are in test mode looking for live events, or the reverse. The Dashboard has a mode toggle and it is easy to miss; test-mode events are only ever sent to endpoints registered in test mode.
Check whether the endpoint has been disabled. Stripe retries a failing endpoint for up to three days in live mode and then disables it, emailing the account owner. Once disabled, nothing is sent and there is nothing left to fail.
Check the event types on the subscription. An endpoint subscribed to a narrower set than you think will deliver nothing for the event you are testing, and the Dashboard will show no attempts rather than an error.
If you are testing locally, remember Stripe cannot reach localhost. Use the CLI to forward.
Recovering the events you missed
Failed events can be resent from the Dashboard individually, or from the CLI. For an outage spanning many events, list them from the API for the affected window and replay against your own handler rather than clicking through the UI.
Remember that resends are deliveries of the same event ID, so your handler must be idempotent or you will double-process everything you recover.
Stopping it recurring
The only reliable signal for cases 2 and 3 is a heartbeat placed in the queue worker after the event is committed, because that is the only point that proves work happened. SensaCat monitors that gap, and so do several alternatives; the important part is that something watches processing rather than delivery.
The full setup is in how to monitor Stripe webhooks.