Stripe webhooks silently missing events: endpoint versions, retries, and the replay you never ran
Problem
A subscription stayed "trialing" after a successful payment. No exception, no log line:
// dashboard: endpoint has 3 failed delivery attempts, then "Marked as failed"
// app: customer paid, subscription status never updated, no error anywhereStripe had tried to deliver invoice.paid three times, given up, and moved on. Our system learned about the payment when the customer asked why their account was still limited.
Root cause
Webhook delivery is at-most-a-few-times, not guaranteed: roughly 3 days of retries with backoff, then the event is marked failed and you have to fetch it. Missing events then has three classic sources, and we had two:
1. Deploys: endpoint returns 500 during a deploy window → retries hit the same broken build → marked failed. 2. Version skew: the endpoint was created with stripe listen / an old API version; event payload shapes differ from what production code expects (e.g. subscription.latest_invoice moving or being removed). 3. Silent success on unhandled types: handler returns 200 for events it never processed, so gaps are invisible by construction.
// cron: every 5 minutes, fetch events since last cursor and upsert state
const events = await stripe.events.list({
created: { gte: lastCursor },
… 7 more lines in the fix🔒 the fix — including 2 code blocks — is members-only. $1/mo unlocks everything.