A webhook endpoint breaks: a bad deploy, an expired secret, a new redirect, a login wall in front of the route. Checkout keeps working, so customers keep paying. Your app just never hears about it.

Once the endpoint is fixed, the next question is the one that matters for revenue: who paid in the meantime, and how do they get what they bought? This post walks through Stripe's documented recovery path, step by step. Every claim links to the doc it comes from (see Sources at the end).

First, how much time Stripe gives you

Stripe doesn't drop a failed event right away. From the webhook docs: "Stripe attempts to deliver events to your destination for up to three days with an exponential back off in live mode." In a sandbox it's shorter: "We retry event deliveries created in a sandbox three times over the course of a few hours."

There's one way to lose those retries. Stripe: "If your destination has been disabled or deleted when we attempt a retry, we prevent future retries of that event." And from the event destinations docs: "Stripe won't attempt to resend any events generated while the destination is disabled."

So when an endpoint is failing, fix the URL or the code behind it. Don't delete or disable the endpoint to make the errors stop.

Step 1: Confirm the live endpoint is enabled

List your webhook endpoints with your live secret key. Each endpoint has a status, and Stripe's API reference says it "can be enabled or disabled."

curl -G https://api.stripe.com/v1/webhook_endpoints -u "$STRIPE_LIVE_KEY:" # Check each endpoint's "url", "status" and "enabled_events" 

Check that the URL is your production URL, the status is enabled, and the event types you fulfill on are in enabled_events.

Step 2: List exactly what failed

Stripe's guide to processing undelivered events says to call the List Events API with three parameters:

  • ending_before: "An event ID that was successfully delivered immediately before the outage. This anchors the list to the window of events you missed."
  • types: "The event types your endpoint listens for."
  • delivery_success: "Set to false to return only events that failed delivery to at least one of your endpoints."
curl -G https://api.stripe.com/v1/events \ -u "$STRIPE_LIVE_KEY:" \ -d ending_before=evt_LAST_GOOD_EVENT \ -d "types[]=checkout.session.completed" \ -d delivery_success=false 

Two limits to know. "Stripe only returns events created in the last 30 days." And "By default, the response returns up to 10 events. To retrieve all events, use auto-pagination."

Step 3: Replay them without doing anything twice

This is where recovery scripts cause new problems. Stripe warns: "Guard against processing the same event twice. Your manual recovery script might run more than once, and Stripe's automatic retries might deliver some events while your script is running."

Stripe's pattern is to check whether an event is already handled, mark it as processing, do the work, then mark it as processed. Here is that shape in Node, using the library's for await auto-pagination:

// recover-missed-events.mjs (Node 18+, stripe npm package) import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) const events = stripe.events.list({ ending_before: 'evt_LAST_GOOD_EVENT', types: ['checkout.session.completed'], delivery_success: false, }) for await (const event of events) { // Your own data store: skip anything already being handled or done if (await isProcessingOrProcessed(event.id)) continue await markAsProcessing(event.id) await fulfillCheckout(event.data.object.id) await markAsProcessed(event.id) } 

Per Stripe's docs, "Using ending_before with auto-pagination returns events in chronological order, so you process them in the order they were created."

Inside fulfillCheckout, follow Stripe's fulfillment guide: "Retrieve the Checkout Session from the API with the line_items property expanded" and "Check the payment_status property to determine if it requires fulfillment." That way the replay trusts Stripe's current answer, not an old payload.

Step 4: Make the endpoint answer the retries

Processing the backlog yourself doesn't tell Stripe to stop. From the same guide: "Stripe still considers manually processed events as undelivered and continues its automatic retry schedule. When your endpoint receives an event you've already processed, skip it and return a 200 response to stop further retries."

So the webhook handler needs the same "already processed?" check as the script. If it sees an event ID it already handled, it returns 200 and does nothing else.

Only a few events? Resend them instead

For a short outage, Stripe's two manual retry options may be enough:

  • "In the Stripe Dashboard, click Resend when looking at a specific event. This works for up to 15 days after the event creation."
  • "With the Stripe CLI, run the stripe events resend <event_id> --webhook-endpoint=<endpoint_id> command. This works for up to 30 days after the event creation."

The same caveat applies: "Manually resending an event that failed to reach a webhook endpoint doesn't cancel Stripe's automatic retry behavior, even if it results in a 2xx status code." Your handler still has to be safe to run twice.

Older than 30 days?

Those events won't come back from the Events API. Rebuild from the payments themselves: the List Checkout Sessions API takes a created filter ("Only return Checkout Sessions that were created during the given date interval") and a status filter, where complete means "The checkout session is complete." Run each session through the same fulfillCheckout function, which checks payment_status and skips anything already granted.

Next time, find out the same day

Stripe's health alerts won't catch this on most accounts. The basic tier, which "All Stripe users receive," covers 402 and 500 error alerts. Webhook latency alerts are in the advanced tier, which is for "Users on Stripe's Growth, Premium or Enterprise Support plan." A small daily job that runs the delivery_success=false query from Step 2 over the last 24 hours works on any account.

Checklist

  • [ ] The live endpoint is enabled and points at the production URL
  • [ ] Failing endpoints get fixed, not disabled or deleted, so Stripe keeps retrying
  • [ ] Missed events are listed with ending_before, types and delivery_success=false, with auto-pagination
  • [ ] The recovery script and the webhook handler share one "already processed" check
  • [ ] Already-processed events get a 200 so Stripe stops retrying
  • [ ] Fulfillment retrieves the Checkout Session and checks payment_status
  • [ ] Anything older than 30 days is rebuilt from Checkout Sessions
  • [ ] A daily check flags failed deliveries before customers write in

Before replaying a backlog, it helps to know the endpoint really is fixed. WebhookProof's free checker sends 3 harmless test requests to a webhook URL you own and reports whether Stripe can reach it and whether unsigned requests are rejected. The full guide, including a daily alert script, is here: Is your Stripe webhook failing silently?

WebhookProof is an independent tool, not affiliated with Stripe, Inc. Quotes are from the linked documentation as read on 2026-10-30.

Sources