Stripe retries your webhooks, which means `invoice.paid` can arrive twice. Two queue workers can grab the same event at the same moment. And a declined card can lock your customer out while Stripe is still retrying the charge. 40 passing tests as proof. You get signature verification over the raw request body, idempotency enforced by a unique database index, queue-based processing with exponential backoff, full Cashier subscription sync, and a configurable grace period for failed payments.
Hi Product Hunt š
I built this after watching a client's billing silently break for three weeks.
The trigger was mundane: a Stripe webhook handler that did real work inline
(calling the API, sending a welcome email). A slow response made Stripe time out
and retry, the retry overlapped the original execution, and `invoice.paid` ran
twice. Nobody noticed until a customer asked why they'd been charged twice.
Fixing that properly taught me three things that most Laravel/Stripe
implementations get wrong:
1. **An `if exists` check is not idempotency.** It's a race. Two overlapping
deliveries both pass the check, and the second `INSERT` throws a duplicate key
error that becomes a 500 ā which makes Stripe retry *more*. You need a unique
index and you need to treat the duplicate key as success.
2. **Webhook handlers must not do real work.** Verify the signature, write one
row, queue one job, return 200. Everything else happens in a worker with a
retry policy you control.
3. **Never revoke access on the first failed payment.** Stripe's Smart Retries
are already trying again for days. Revoking immediately throws away a customer
Stripe was about to recover. This is called involuntary churn and it's the
largest *recoverable* revenue loss in subscription billing.
So I packaged the correct version of all of it. 40 tests, and they run against
MySQL rather than SQLite ā because the unique index and the concurrency
behaviour are the entire point, and those are exactly what differ between
engines.
Happy to answer anything about webhooks, Cashier, or the general misery of
debugging billing. And if you've got a webhook horror story, I'd genuinely like
to hear it ā I'm collecting them.
ā https://boukataya.gumroad.com/l/...