Failed Payments and Dunning: Recover the Subscription Revenue You're Quietly Losing (Stripe, 2026) — Cesar Ayala
← All posts

Failed Payments and Dunning: Recover the Subscription Revenue You're Quietly Losing (Stripe, 2026)

Involuntary churn is subscription revenue lost to failed card charges, not customers leaving. Recover it with a stack: Stripe Smart Retries (ML retry timing), automated dunning emails plus a self-serve Customer Portal, and Card Account Updater. Drive access off invoice.payment_failed and the past_due to unpaid state machine, not the client.

What is involuntary churn, and why is it bigger than you think?

Involuntary churn is subscription revenue you lose to failed card charges — expired cards, reissued numbers, temporary declines — not customers choosing to leave. Unlike voluntary churn, it is largely recoverable with Stripe’s Smart Retries, Card Account Updater, dunning emails, and a correct webhook state machine. Most teams under-instrument it because it hides inside one churn number.

When most founders look at their churn number, they see one figure and assume it means “people decided to leave.” It doesn’t. That single number is hiding two completely different problems, and they have completely different fixes.

Voluntary churn is a customer actively choosing to cancel. They clicked the cancel button, downgraded, or let a contract lapse. That’s a product or pricing signal — they didn’t get enough value, the price didn’t fit, a competitor won. You fix voluntary churn with the product, the onboarding, and the pricing. It’s hard, slow work, and you rarely win those customers back.

Involuntary churn is different. These are subscriptions you lose to a failed card charge — an expired card, a reissued card with a new number, a temporary decline, a bank that flagged the recurring charge. The customer never decided to leave. They still want your product. The charge just didn’t go through, and if nothing recovers it, Stripe eventually marks the subscription unpaid or canceled and you quietly lose revenue you should have kept.

Here’s the part teams under-appreciate: involuntary churn is often a large slice of total churn, and unlike voluntary churn, it’s largely recoverable. Voluntary churn is a product problem. Involuntary churn is a billing-plumbing problem — and billing plumbing is exactly the kind of thing you fix once with Stripe tooling and a correct webhook state machine, then it pays you back every month.

My opinion after running this on real subscriptions: most teams under-instrument involuntary churn because it hides inside one churn metric. They go re-design onboarding to fight churn when half the bleed was expired cards they could have recovered with a config toggle and a webhook handler. And recovery lives or dies on event discipline, which is why the whole system rests on the webhooks vs polling vs API idea that the event is the source of truth — you can’t reliably recover what you don’t react to correctly.

Voluntary vs involuntary churn

Voluntary churn

  • Customer chose to cancel or downgrade
  • A product / pricing / value signal
  • Fix with onboarding, pricing, product
  • Rarely won back
  • Not what dunning recovers

Involuntary churn

  • Subscription lost to a FAILED card charge
  • Expired, reissued, or declined cards
  • A billing-plumbing problem
  • Largely recoverable
  • Exactly what this playbook recovers
Two different problems hiding inside one churn number. Dunning only recovers the right-hand column.

How do Stripe Smart Retries actually work?

The naive way to handle a failed recurring charge is a fixed retry schedule: try again on day 1, day 3, day 7. That works, but it’s blind. Charging a card at 2am on a payday-minus-five day is not the same as charging it the morning the customer’s paycheck lands. Timing is most of the battle in recovery, and a fixed schedule ignores it.

Smart Retries is Stripe’s answer, and it’s part of Stripe Billing’s Revenue Recovery suite. Instead of a fixed schedule, it uses machine learning trained on Stripe’s network to pick the optimal retry time per failed payment — using signals like the customer’s recent activity and the best time to charge that particular card. You’re not guessing day 1 / day 3 / day 7 anymore; the model is timing each retry to maximize the odds it clears.

You don’t configure the timing yourself — you configure the policy. You set how many times to retry and the window to retry within: 1 week, 2 weeks, 3 weeks, 1 month, or 2 months. Stripe’s ML then places the retries inside that window. For a B2C consumer product I’d lean toward a window that spans at least one pay cycle so a retry can land after the next paycheck. For B2B, a tighter window plus a human nudge often does more.

This all lives in your Billing settings, and it’s worth reading the official docs before you flip it on: Smart Retries and the broader Revenue Recovery overview.

Conceptually, enabling it is a settings + retry-policy decision, not application code. But here’s the mental model for what it controls:

// Conceptual — this is configured in Stripe Billing settings,
// not set per-request in your app code.
const retryPolicy = {
  smartRetries: true,        // ML picks the optimal retry time per failed payment
  retryWindow: "1 month",    // 1 week | 2 weeks | 3 weeks | 1 month | 2 months
  // Stripe places the retries inside this window; you don't pick the days.
};
// After the final attempt in the window, the subscription moves to
// `unpaid` or `canceled` per your Billing settings — that's your trigger.

The recovery flow, end to end

Charge failsinvoice.payment_failed fires; subscription becomes past_due
Smart RetriesML picks optimal retry times inside your chosen window
Dunning + PortalAutomated email prompts the customer to fix their card
RecoveredA retry clears → subscription returns to active
Or: final attempt failsSubscription moves to unpaid or canceled → revoke access
One failed charge moves through retries and dunning, then branches to recovered or to your final state.

Dunning emails and self-serve: how do you get customers to fix their own card?

Smart Retries handles the cases where the card could clear on a better day — a temporary decline, an over-limit moment. But a genuinely expired or canceled card will never clear on retry alone. Somebody has to put in a new card. That somebody is the customer, and your job is to make it trivial.

Stripe can send automated dunning emails when a payment fails, prompting the customer to update their card. You turn these on in Billing settings — no email service to wire up. The email tells the customer the charge failed and gives them a way to fix it. Stripe’s automated emails can also prompt customers around card expiry, not only on a hard failure, so some of this work happens before a charge ever fails.

The critical word there is a way to fix it. An email that says “your payment failed, please contact us” is worthless — it just generates a support ticket. Pair the dunning email with the Stripe Customer Portal or the hosted invoice page so the customer updates their card themselves, in one tap, without ever talking to you. The portal is a Stripe-hosted page where customers manage their subscription and payment method; the hosted invoice page lets them pay or update the card behind a specific failed invoice.

My strong opinion here: the dunning email is only as good as the one-tap path behind it. Teams obsess over email copy and forget that the recover-rate lever is the friction between “I read the email” and “my new card is saved.” If you’ve already built your subscription and webhook foundation from how to integrate Stripe into your SaaS, enabling the Customer Portal is a small addition that pays for itself the first month an expired card gets fixed by the customer instead of a support agent.

Can you prevent failures before they happen with Card Account Updater?

The cheapest recovery is the failure that never happens. Before you optimize retries and write dunning copy, there’s a layer that stops a whole class of failures from entering the flow at all.

Card Account Updater automatically refreshes saved card details — a new expiry date, a reissued card number — by communicating with the card networks and issuers before the charge fails. When a bank reissues a customer’s card or extends the expiry, the network has the updated details; Card Account Updater pulls them in so your next recurring charge runs against the current card instead of the dead one. The failure never happens, so it never enters the retry-and-dunning machine.

Network tokenization helps here too: when the card is stored as a network token rather than a raw PAN, the token can survive a card reissue, so the charge keeps working across the change.

This is the layer I’d turn on first. Retries and dunning are recovery — they kick in after a failure and after you’ve already annoyed the customer with an email. Card Account Updater is prevention — it quietly keeps charges working and the customer never knows anything almost went wrong. Prevention beats recovery on every axis: no failed charge, no retry risk, no dunning email, no support ticket, no churn risk. The cheapest failed payment is the one that never happens.

The recovery stack, in order

  1. 1. Card Account UpdaterPrevent — refresh expired / reissued cards before they fail
  2. 2. Smart RetriesRe-time — ML picks the optimal retry moment per failed payment
  3. 3. Dunning emailsPrompt — automated email when a payment fails
  4. 4. Self-serve Customer PortalResolve — one-tap card update, no support ticket
Build it bottom-up: prevent first, then re-time, then prompt, then make self-serve effortless.

How do you drive access off the past_due state machine and webhooks?

This is the part where teams quietly create bugs, so I’ll be blunt: the webhook event is the source of truth, not the client. Never grant or revoke access because your front end thinks a payment succeeded or failed. The browser can lie, retry, drop the connection, or simply be wrong. Stripe’s events are authoritative; build your access logic on them.

Here’s the state machine. When a recurring charge fails, Stripe fires invoice.payment_failed and moves the subscription to past_due. During the retry window the subscription stays past_due, and the invoice’s next_payment_attempt tells you when the next retry is scheduled. This is not the moment to revoke access. The customer is mid-recovery; cutting them off now guarantees the churn you’re trying to prevent. Let Smart Retries and dunning do their work.

Only after the final attempt in your window fails does Stripe move the subscription to unpaid or canceled (depending on your Billing settings). That is your revoke signal.

// Express. Verify the signature on the RAW body — req.body must be a Buffer,
// so mount this route with express.raw, NOT express.json:
//   app.post("/webhooks/stripe", express.raw({ type: "application/json" }), handler)
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    const sig = req.headers["stripe-signature"];
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    return res.status(400).send(`Webhook signature failed: ${err.message}`);
  }

  switch (event.type) {
    case "invoice.payment_failed": {
      const invoice = event.data.object;
      // Still in the retry window — DON'T revoke yet.
      // next_payment_attempt is set while retries remain.
      if (invoice.next_payment_attempt) {
        markPastDue(invoice.customer, {
          attempt: invoice.attempt_count,
          nextRetry: invoice.next_payment_attempt,
        });
      }
      break;
    }
    case "customer.subscription.updated":
    case "customer.subscription.deleted": {
      const sub = event.data.object;
      // Final attempt failed — subscription is unpaid or canceled.
      // Handle BOTH events: a canceled final state may arrive as deleted.
      if (sub.status === "unpaid" || sub.status === "canceled") {
        revokeAccess(sub.customer);
      }
      break;
    }
  }
  res.sendStatus(200);
});

Two rules to live by. First, attempt_count and next_payment_attempt tell you where in the retry flow you are — use them to message (“we’ll try again on…”) rather than to cut access. Second, revoke on unpaid/canceled, never on the first invoice.payment_failed. Getting this state machine right is the difference between a recovery system and an accidental cancellation machine.

What recovery rate should you honestly expect?

Vendors quote the best-case number; you should budget for the honest one. Stripe cites that businesses recover ~55% of failed payments on average with its Revenue Recovery tooling (as of 2026, confirm current). Treat that as a marketing-grade average and confirm it for your own business — your mix of B2B vs B2C, geography, and card types moves this a lot.

Independent datasets often report lower real-world numbers, especially in B2C, frequently landing in the ~25–38% range. And one specific caveat: Smart Retries alone tends to land lower than the full stack — retries can only recover cards that will eventually clear, not cards that are genuinely dead and need replacing.

That’s the whole argument for stacking. Smart Retries + dunning emails + Card Account Updater + the self-serve portal each recover a different failure type, and combined they beat any single lever. Card Account Updater prevents the expiry failures; Smart Retries clears the temporary declines; dunning plus the portal recovers the cards that genuinely need replacing. No single tool covers all three.

Set an honest recovery target

Stripe cites~55% of failed payments recovered on average (as of 2026, confirm current)
Often seen (B2C)~25–38% in independent datasets
Smart Retries aloneLands lower than the full stack
Your jobMeasure your own recovery rate — don't trust the average
Cited averages are a starting hypothesis, not a forecast for your business. Measure your own number.

If you’re scoping billing for a new product, this is also where billing resilience earns its place in the budget — see how much it costs to build a SaaS MVP. And if your pricing is metered rather than flat, the same recovery discipline rides on top of usage-based billing for AI SaaS with Stripe — a failed charge on a usage invoice is still recoverable revenue. The dunning stack is cheap to turn on and expensive to skip.

FAQ: quick answers on failed payments and dunning

When should I revoke access? On unpaid or canceled — after the final retry in your window fails — not on the first invoice.payment_failed. Cutting access mid-retry kills the recovery you set up.

unpaid vs canceled — which final status should I pick? It’s a Billing setting. canceled ends the subscription cleanly; unpaid keeps it around (access revoked) so the customer can more easily revive the same subscription if they come back and fix the card. I lean toward unpaid when reactivation friction matters; pick canceled if you’d rather force a fresh start. Either way, revoke access on both.

Does Smart Retries replace dunning emails? No — they stack. Smart Retries re-times the charge; dunning prompts the human to replace a card that will never clear on its own. Different failures, both needed.

Will retries trip fraud rules or look bad to issuers? Smart Retries is built around sensible timing rather than hammering the card, which is exactly the behavior issuers tolerate. A fixed, aggressive retry schedule is far more likely to look abusive than ML-timed retries.

Should I trust the client when it says a payment succeeded? No. The webhook event is the source of truth. Grant and revoke off verified Stripe events, never off the browser.

B2B vs B2C — should I expect the same results? No. B2C leans on Card Account Updater and Smart Retries (consumer cards expire and get reissued constantly). B2B often recovers more through dunning to a billing contact and a clean self-serve portal. Measure each separately.

So what should you actually turn on?

Turn on the full stack and let each layer do its job: Card Account Updater to prevent expired-and-reissued-card failures before they happen, Smart Retries to ML-time the retry on cards that can still clear, automated dunning emails to prompt the customer, and the self-serve Customer Portal so they fix the card in one tap without a support ticket. Drive every grant and revoke off the webhooks — invoice.payment_failed and the past_due to unpaid/canceled state machine — never off the client, and never cut access until the final retry fails. Then measure your own recovery rate instead of trusting a cited average, because your number depends on your customers, not Stripe’s network-wide mean. Start with the official docs: Smart Retries and Revenue Recovery. This is the cheapest meaningful revenue lift in most subscription businesses — you’re not selling anything new, you’re keeping the customers you already won.