Skip to main content
QuantLab Logo

Revenue Recovery Engineering · 2026

Handling Failed Payments in Stripe: Dunning, Retries & Recovering Churn

Failed payments quietly drain 20% to 40% of subscription revenue at most SaaS companies. This is the engineering and product playbook for recovering it: Smart Retries, dunning, the right webhooks, grace periods, and card-update flows that actually convert.

Bill Beltz, Founder & Principal Engineer
By , Founder & Principal EngineerPublished 12 min read

Quick answer: how do I reduce failed-payment churn?

Turn on Smart Retries and Stripe's built-in dunning emails first — that is free recovery. Listen for the invoice.payment_failed webhook to drive in-app banners and feature gating, keep access during the past_due grace period instead of cutting it on the first decline, and give customers a one-click card-update link. Handle the authentication_required case explicitly, because 3D Secure retries only succeed with customer interaction. Done well, this recovers the majority of involuntary churn at near-zero ongoing engineering cost.

A churned customer who never wanted to leave is the most frustrating kind of lost revenue — and the easiest to win back. At QUANT LAB USA we treat failed-payment recovery as core billing infrastructure, not an afterthought. This post is the playbook we implement on a subscription billing engagement. It assumes you already have the basics from our Next.js + Stripe guide and reliable webhooks per our webhook security best practices.

Why payments fail (and which are recoverable)

Not every decline is equal. The recovery strategy depends entirely on why the charge failed, which Stripe surfaces in the charge's decline_code and the PaymentIntent's last_payment_error. Hard declines (lost or stolen card, fraud) will never recover on retry and need a new card. Soft declines (insufficient funds, a temporary issuer block) are exactly what Smart Retries was built for.

Decline codeMeaningRecoverable?
insufficient_fundsAccount lacks fundsYes — retry near payday
expired_cardCard past expiryYes — prompt card update
card_declined (generic)Issuer declined, no detailOften — Smart Retry
do_not_honorIssuer block, unspecifiedSometimes — retry later
lost_card / stolen_cardCard reported lost/stolenNo — require new card
authentication_required3DS challenge neededYes — customer must auth

Layer 1: Smart Retries (free, turn it on first)

Stripe's default behavior retries failed subscription charges on a fixed schedule. Smart Retries replaces that with a model trained across the Stripe network that times each retry for when it is most likely to clear — after a likely payday, once a temporary block lifts, or when the issuer is most responsive. You configure the retry window and the cancel-or-leave-unpaid endpoint behavior in the Dashboard under Billing settings.

The key decision is what happens when retries are exhausted: Stripe can cancel the subscription, mark it unpaid, or leave it past_due. Choose deliberately. Cancellation is clean but ends the relationship; leaving it unpaid keeps the subscription alive for a longer manual-recovery tail. Whatever you pick, make sure your webhook handler reflects the final state into your own database so your app's entitlements stay correct.

Layer 2: dunning emails and the card-update flow

Dunning is the sequence of nudges that asks the customer to fix their payment method. Stripe ships built-in dunning emails plus a hosted, secure card-update page — enable both before you write a single line of custom code. The hosted page is PCI-friendly: the customer enters new card details directly with Stripe, so your servers never touch the number.

The single highest-leverage improvement is reducing friction on the update. One click from the dunning email straight to a pre-authenticated update page beats "log in, find billing, locate the update form" by a wide margin. The Stripe Customer Portal gives you that hosted update surface for free; link to it from your in-app banner as well as from email. Build fully custom branded dunning only once you have evidence that revenue is still leaking after the defaults.

Layer 3: webhooks that drive your app state

Stripe handles the money side, but your application has to reflect the failure so you can show banners, gate features, and trigger account-management work. Subscribe to and handle these events:

  • invoice.payment_failed — a charge was declined; read next_payment_attempt and attempt_count to decide messaging severity.
  • invoice.payment_action_required — the retry needs 3D Secure; route the customer to authenticate.
  • customer.subscription.updated — the subscription moved into past_due or back to active after recovery.
  • invoice.paid — a previously failed invoice recovered; clear the banner and restore any gated features.
  • customer.subscription.deleted — Stripe gave up; downgrade or suspend.

Make every handler idempotent and keyed off the event ID, exactly as covered in our webhook security best practices. A double-processed invoice.paid that grants a duplicate credit is its own kind of revenue bug. Use our Stripe webhook tester to validate the payloads before you ship.

The grace-period question: when to actually cut access

This is where teams quietly torch recoverable revenue. The instinct is to suspend the account the moment a payment fails. Do not. A first decline is frequently a soft decline that Smart Retries clears within days. If you cut access immediately, you punish a paying customer, generate a support ticket, and often trigger a real cancellation out of frustration.

The pattern that works: keep full access during past_due, show a non-blocking in-app banner with a one-click update link, and only downgrade or suspend when the subscription transitions to canceled or unpaid after retries are exhausted. Align your grace window to your retry window — there is no reason to gate features while Stripe is still actively retrying a card that will likely succeed.

Advanced: pre-dunning and card-expiry prevention

The cheapest failed payment is the one that never happens. Two preventive plays. First, card-account updater: Stripe can automatically receive updated card numbers and expiry dates from participating networks when a customer's bank reissues a card, so many expirations heal silently with no customer action. Second, pre-dunning: listen for customer.subscription.trial_will_end and watch for cards approaching their expiry month, then proactively prompt an update before the renewal charge ever fails.

For teams running this at scale across many customers and plans, the recovery system becomes part of the broader billing architecture — entitlements, invoicing, and reconciliation all have to agree on the subscription's state. That whole surface is what we wrap in our payments, invoicing, and licensing service.

FAQ

What is involuntary churn and why does it matter?

Involuntary churn is when a subscriber leaves not because they wanted to cancel, but because their payment failed — an expired card, an insufficient-funds decline, or a bank's fraud block. For most SaaS, failed payments account for 20% to 40% of total churn. Because these customers still want your product, recovering them is the cheapest revenue you will ever earn; no acquisition spend is required.

What are Stripe Smart Retries?

Smart Retries is Stripe's machine-learning retry engine. Instead of retrying a failed charge on a fixed schedule, it uses signals across the Stripe network to pick the moment a retry is most likely to succeed — for example, retrying after a payday pattern or once a temporary bank block clears. You enable it in the Stripe Dashboard under Billing retry settings, and it typically recovers a meaningfully higher share of failed invoices than fixed-interval retries.

Which Stripe webhook tells me a payment failed?

Listen for invoice.payment_failed. It fires whenever a subscription invoice charge is declined, and the invoice object includes next_payment_attempt (when Stripe will retry) and the attempt_count. Pair it with invoice.payment_action_required for charges that need 3D Secure authentication, customer.subscription.updated to track the subscription moving into past_due, and customer.subscription.deleted for when Stripe finally gives up and cancels.

How long should the grace period be before I cut off access?

Match it to your retry window. If Smart Retries runs for up to roughly two to three weeks, keep access during past_due so you do not punish a customer whose card will recover on its own. A common pattern: full access during past_due with an in-app banner, then downgrade or suspend only when the subscription transitions to canceled or unpaid. Cutting access on the first decline destroys recoverable revenue and generates support tickets.

Should I build my own dunning emails or use Stripe's?

Start with Stripe's built-in dunning emails and the hosted card-update page — they require zero engineering and recover a large share of failures. Build custom dunning (your own branded emails, in-app prompts, SMS) when you have data showing meaningful revenue still leaking after Stripe's defaults, or when you want the messaging on-brand. Most teams over-invest in custom dunning before exhausting the free built-in recovery.

How do I handle a customer whose card needs 3D Secure to retry?

When a retry requires authentication, the invoice triggers invoice.payment_action_required and the PaymentIntent carries a status of requires_action. You cannot complete 3DS server-side — the customer must authenticate. Send them to the Stripe-hosted invoice page or your own page using the PaymentIntent client secret and the confirmPayment flow so they can complete the challenge. Surface this clearly in-app, because these recover only with customer interaction.

Sources & references

  1. [1]Smart Retries and automatic collection · Stripe Docs
  2. [2]Managing failed payments and dunning · Stripe Docs
  3. [3]Decline codes reference · Stripe Docs
  4. [4]Customer portal for card updates · Stripe Docs

Find out how much revenue you are leaking.

Free 30-minute review of your dunning, retry, and grace-period setup. If involuntary churn is bigger than you think — and it usually is — we will show you exactly where it leaks.

Or call Bill at (770) 652-1282
All blog postsPublished June 3, 2026