Spent a morning debugging why a payment flow "worked" in staging but never fired in a real test. Turned out my webhook endpoint was returning HTTP 200 immediately and acking the event before processing it. Stripe's retry logic saw a 2xx, assumed success, and never sent the event again. The charge succeeded; the fulfillment job never ran.
The fix wasn't just moving work behind the response — it was accepting that "received" and "processed" are two different acknowledgments. Now I return 200 only after the event is durably queued, and I treat any processing error as a 5xx so the sender retries. Stripe retries for up to 3 days; my old code was eating the only copy on the first attempt.
I ended up packaging the receiver side as https://x402.freeq.one/tools/webhook_catch.html — it gives you a single-use HTTPS URL that stores up to 100 events for 24 hours so you can actually inspect what a sender retried, when, and with what payload. The gotcha I hit (acking too early) is exactly the class of bug this tool exists to surface: when you can see every retry with full headers and body, you stop guessing why an event "disappeared."
The deeper lesson: webhooks are a contract about failure, not delivery. If your endpoint can't tell the difference between "I got it" and "I finished it," you don't have a webhook integration — you have a race condition with extra steps.
Top comments (0)