An invoice can be paid at Stripe while your app never grants access. That kind of revenue break is easy to miss until a customer contacts support.
Most Stripe webhook failures fall into four buckets: Stripe couldn’t deliver the event, your endpoint rejected its signature, your application accepted it but failed afterward, or your billing rules interpreted the event incorrectly. The right fix depends on the failure layer, so retries alone won’t repair every problem.
Start by locating the exact event and its latest delivery attempt before changing code.
How to debug Stripe webhook failures by layer
A failed renewal can look identical in your product, even when the underlying cause is completely different. Use the event’s delivery record, your endpoint logs, and the current invoice or subscription state to separate the issue.
| Failure layer | What Stripe can see | Typical clue | First check |
|---|---|---|---|
| Delivery | The endpoint didn’t return success | Timeout, DNS error, 404, 500, or non-2xx response | URL, network access, route, TLS, and response status |
| Authentication | The endpoint returned an error after receiving the request | 400 with a signature-verification error | Endpoint secret, Stripe-Signature, and raw request body |
| Application processing | Stripe received 200 or 204, but your internal work failed | Missing entitlement update or failed background job | Event logs, queue records, database transaction, and worker errors |
| Billing state | The event processed, but your app reached the wrong business decision | User remains active after failed collection, or loses access after payment | Invoice status, subscription status, event ordering, and access rules |
A delivery marked “failed” tells you Stripe did not receive a successful acknowledgment. It does not tell you whether the subscription payment itself failed.
Likewise, a successful 2xx response only confirms that your endpoint accepted the notification. It doesn’t prove that your app updated a customer record, created an invoice entry, or changed access correctly.
Verify endpoint delivery and signature authentication
Check delivery evidence before changing code
Open the affected event in Stripe’s webhook destination view. Current Stripe interfaces often call this area Workbench and use an Event deliveries tab. Some accounts and support pages still use older Dashboard labels such as Webhooks and Failed.
Inspect the response code, response body, attempt timestamp, and endpoint URL. Stripe treats a 2xx response as success. A redirect, 401, 404, 429, 500, or timeout keeps the delivery in a failed state and can trigger retries.
Use Stripe’s webhook delivery guide to confirm that the correct endpoint is enabled and subscribed to the events you expect. For subscription billing, also confirm that the endpoint is configured in the same mode as the event. A live invoice will not go to a test-only destination.
Check the basics before touching application logic:
- Confirm Stripe can reach the public HTTPS URL without a VPN, IP allowlist block, bot challenge, or login page.
- Confirm your route accepts
POSTrequests at the exact configured path. - Check reverse-proxy and platform logs for timeouts, request-size limits, or TLS errors.
- Return
200or204quickly after safely recording the event.
Preserve the raw request body for signature checks
Signature errors often come from body parsing, not Stripe. Your verification step must use the exact raw bytes Stripe sent, together with the Stripe-Signature header and the signing secret for that endpoint.
A representative request has POST /billing/stripe/webhook, a Stripe-Signature: t=...,v1=... header, and an untouched JSON byte stream. If middleware parses and reserializes that JSON before verification, the signature no longer matches.
Use the endpoint-specific secret, not a publishable key, API key, or a secret copied from another endpoint. CLI-forwarded events also use the temporary signing secret printed by the CLI, which differs from a Dashboard endpoint secret.
Stripe’s signature troubleshooting checklist calls out the same failure points: wrong secret, altered body, and missing or incorrect signature header.
Returning
200after a non-durable queue handoff prevents Stripe from retrying, even if your worker never receives the event.
Separate application processing from billing-state errors
Store each event before doing slow work
After signature verification, persist the event before performing slow or failure-prone work. This could mean inserting an event row in your database, writing to a durable queue, or creating an outbox record in the same transaction as your internal update.
Use Stripe’s event ID as a unique key. A table with a unique stripe_event_id field prevents duplicate emails, duplicate CRM actions, and repeated access changes. Store a status such as received, processing, complete, or failed so you can see where work stopped.
A useful structured log entry might look like this: event_id=evt_123 type=invoice.paid livemode=true signature_verified=true intake=stored job_id=job_456 response=204.
If the same event arrives again and your database shows it as complete, return 204. If the prior attempt failed before completion, resume or retry your internal job according to its state. Do not create a second invoice record or send another renewal email.
Stripe can retry deliveries, and events can arrive more than once. Stripe also does not promise event ordering. Your handler needs to tolerate a renewal event arriving before an earlier subscription update.
Keep subscription status separate from delivery status
Some apparent Stripe webhook failures are really billing-state mistakes. For example, invoice.payment_failed means a collection attempt failed. It does not always mean you should cancel access immediately, because Stripe may make later collection attempts.
For that event, inspect the invoice’s attempt_count, customer, subscription, amount due, and current invoice status. Your policy might send a payment-update email after the first failure, keep access during a grace period, and revoke access only after the subscription reaches the status your business treats as unpaid.
Use invoice.paid to confirm successful invoice collection before granting or renewing paid access. Track customer.subscription.updated for status changes, but don’t treat every update as proof of payment. A cancellation scheduled for period end also needs different handling from an already-ended subscription.
Stripe’s subscription webhook guidance covers payment failures, trial endings, status changes, and customer authentication actions. When events arrive out of order, retrieve the current invoice or subscription from Stripe and compare it with the state stored in your app.
Recover missed events without creating duplicate work
Let automatic retries work, then resend deliberately
Stripe retries failed live-mode deliveries for up to three days with exponential backoff. In sandbox or test mode, Stripe makes three retry attempts over a few hours. A successful 2xx stops retries for that delivery.
First fix the endpoint, signature check, or application outage. Then inspect which events remain undelivered. Stripe’s undelivered event recovery process describes how to identify those events and process them safely.
You can manually resend an event from the Dashboard for up to 15 days after it was created. The Stripe CLI can resend events for up to 30 days. A resend repeats the notification. It does not rerun the original charge attempt or reverse a billing decision already made.
Avoid sending a large batch of old events into a handler that lacks idempotency. Automatic retries may still be pending when you start a manual replay.
Replay through an event ledger
Treat replay as controlled recovery work. Filter events by time range, endpoint, type, and processing status. Then process each event through the same signature, persistence, and idempotency path as a normal delivery.
For subscription records that may have changed since the missed event, fetch the current Stripe object before changing customer access. The current invoice and subscription state should guide the final decision, while the event record explains why the workflow ran.
If a background job failed after your endpoint returned 204, Stripe cannot know that work failed. Retry the internal job from your event ledger instead of manually resending the Stripe event.
Test and live mode have different failure clocks
Use a separate verification path for each mode
Test mode and live mode have separate objects, endpoint settings, signing secrets, and retry behavior. Log the event’s livemode value and keep test records away from live customer access data.
Live failures have a longer recovery window, up to three days of exponential retries. Test or sandbox failures disappear much faster, after three retries over a few hours. That difference can make a test integration appear stable when it only succeeds because you fixed it quickly.
For subscription sandbox flows, Stripe’s current documentation says it won’t attempt to charge the customer unless it receives a successful webhook response. A broken test endpoint can therefore look like a billing problem when the real issue is delivery or authentication.
Test the full path with a real test subscription lifecycle: successful invoice payment, failed payment, retry, cancellation, and a duplicate event. Check that each case records one event, produces the intended access state, and returns a 2xx response only after durable intake.
Conclusion: Next Actions
Reliable subscription billing depends on separating transport errors from your own processing and billing decisions. The strongest safeguard is an idempotent event ledger that records verified events before background work begins.
Use this next-action checklist:
- Find one failed event and record its endpoint URL, HTTP status, response body, mode, and event type.
- Verify the endpoint secret and raw-body handling before parsing JSON.
- Add a unique event-ID record and return
200or204only after durable storage. - Compare invoice and subscription state before changing paid access.
- Fix the root cause, then use retries or a controlled replay to recover missed events.