Disclosure: we're the Swipe Again team. What follows is the coordination design we ship for Stripe Billing merchants, described at the level of mechanics. If you run recovery on Stripe — ours or anyone's — this is the interaction worth getting right.
Stripe Billing already retries failed subscription payments. Smart Retries is on by default for many accounts, and it is genuinely good at what it does: it picks retry times using network and issuer signals no outside tool can see.
So when a merchant adds a recovery layer on top, there is an immediate design problem. Two systems now own the same failure. If the second system doesn't know what the first is doing, you get duplicated attempts, wasted attempt budget, and — in the worst case — two charges racing for the same invoice.
The naive solutions are both bad. "Turn Stripe's retries off and let us run everything" throws away issuer-side signal. "Wait until Stripe finishes, then start" means your recovery begins days late, after the failure has cooled — and the tail of Stripe's schedule can stretch for weeks.
We chose a third option: read Stripe's state before every decision, and weave our attempts into the gaps.
The inputs are already on the invoice
Everything the coordination needs is observable from the invoice object and the webhooks around it:
-
attempt_count— how many times Stripe has tried this invoice so far. -
next_payment_attempt— when Stripe plans to try next, if it does. - The latest charge's decline code — which tells us whether time can help this failure at all.
Our engine re-reads the invoice fresh before firing anything. State cached at ingestion time is not good enough; by the time a gap opens, the invoice may have been paid, voided, or scheduled differently. Every retry decision starts from a live read.
The weave rules
Simplified, the decision before each of our attempts looks like this:
- If Stripe has an attempt coming within roughly 24 hours, defer. Don't burn one of our attempts competing with an imminent native try. Wait, re-read, reassess after it lands.
- If Stripe's schedule has a real gap, fire into the gap. A failure sitting idle for five days is decaying for no reason.
- If Stripe has exhausted its schedule, run our full plan. There's nothing left to collide with.
- Hold a reserve. We cap the combined attempts (Stripe's plus ours) at 15 and keep a few of our slots unspent until late in the window, so a late-stage opportunity — a payday landing on day 20 — still has budget.
- Light native usage extends our plan. If Stripe only tried once or twice, the failure has seen less total pressure, so our ladder stretches a little longer rather than compressing into the same window.
And underneath all of it, the decline-code layer still applies: insufficient-funds failures ride a widening ladder aimed at pay cycles (out toward ~day 25), while hard declines never get retried at all — they route straight to the card-update flow.
Why double-charging can't happen
Coordination is only safe if a double charge is structurally impossible, not just unlikely. Three mechanisms do that work:
- Idempotency keys on every charge attempt, so a retried API call can never create a second charge.
- A claim step before firing: the recovery record is atomically claimed, so two workers (or a webhook redelivery) can't both decide to fire the same attempt.
- A single recovery choke point: whichever path succeeds — our retry, Stripe's retry, or the customer updating their card — the recovery is recorded through one function, so an invoice can be marked recovered exactly once, attributed to exactly one cause.
If your recovery setup can't describe its equivalent of those three, the double-charge question hasn't been answered — it's been deferred.
The merchant-facing version
None of this requires the merchant to configure anything. They don't turn Smart Retries off; they don't pick a schedule. The tuning advice we give on our Stripe dunning page is minimal: leave the native retries on, let the weave coordinate, and let the branded dunning emails (rather than Stripe's generic ones) carry the customer-facing side.
That's the whole philosophy, and it's the same one behind how Swipe Again works on every platform: conduct the recovery, respect the platform's machinery, and never make two systems guess at each other across the same invoice.
Top comments (0)