How RecoverFlow works, end to end | Docs

Docs

How it works, end to end

Reflects the code as of 28 July 2026. If the product changes and this page does not, that is a bug worth emailing about.

The whole system is one pipeline that starts at a Stripe webhook and ends either in a recovered payment or a case marked lost. Here it is in order.

1. Stripe tells us a payment failed

We listen for invoice.payment_failed on your connected account. The handler looks up the merchant by Stripe account ID, then either opens a new recovery case for that invoice or updates the existing one. One case per invoice, never two.

Every webhook event is recorded before it is acted on, so a redelivery from Stripe does not open a second case or send a second email.

2. The decline is classified

The decline code is read and sorted into hard, soft or unknown. Hard means no retry will be scheduled. Unknown means a code we do not recognise, which is treated as recoverable so that a new Stripe code does not silently stop recovery. This is covered properly in decline classification.

3. A retry is scheduled, or deliberately not

For soft and unknown declines a retry is scheduled by decline reason. For hard declines nothing is scheduled, because the attempt would either be refused by the issuer or never put on the wire by Stripe at all. See retry scheduling.

Two guards apply throughout. A second pending attempt is never stacked on a case that already has one, and no attempt is scheduled beyond the recovery window.

4. The first email goes out

When a case first opens, step 1 of the dunning sequence sends immediately. Later steps follow the retry schedule. See dunning emails.

5. Retries execute in the background

Each scheduled attempt is a background job. When it fires, it asks Stripe to pay the invoice and records the outcome. If it declines again, the decline code from that attempt decides whether there is a next one, so a case that starts as insufficient_funds and becomes lost_card stops being retried at that point rather than running out the schedule.

When retries are exhausted the case is marked lost, unless the decline is one that needs a new card, in which case the case stays open for the card-update flow.

6. Recovery is detected and attributed

We watch for the invoice being paid. When it is, the case closes as recovered and is attributed to whichever mechanism most plausibly caused it. If nothing we did can be credited, it is recorded as recovered without our involvement and you are not billed for it. See attribution and billing.

What it runs on

.NET 8 in a Clean Architecture layout, PostgreSQL via EF Core, Hangfire for scheduled jobs, SendGrid for email, hosted on Render. Merchant data is isolated at the database layer with EF Core global query filters, so a query that forgets to filter by tenant returns nothing rather than someone else's rows.

Common questions

How quickly does RecoverFlow react to a failed payment?

The first dunning email is sent when the recovery case opens, which is when Stripe's invoice.payment_failed webhook arrives. Retries are scheduled from that point according to the decline reason.

Does RecoverFlow interfere with Stripe's own Smart Retries?

They coexist. Stripe continues to run whatever retry settings you have configured on your own account. RecoverFlow schedules its own attempts and sends its own emails. If you would rather Stripe handled retries alone, its free settings are genuinely capable and there is a page on this site about when that is the right call.

What happens if Stripe sends the same webhook twice?

Nothing extra. Processed events are recorded, so a redelivery does not open a second case, schedule a second retry, or send a duplicate email.