2
1 Comment

Why I stopped treating all failed payments the same way

When a payment fails, most SaaS tools do one thing: retry.

But I've been thinking about this differently.

There are exactly two types of failed payments:

Timing problems (insufficient funds, network errors)

The card is fine. The customer has money.
They just need to be charged at the right moment.
Stripe's smart retry works here.

Communication problems (expired card, card changed)

No amount of retrying will ever recover these.
The card is gone. Someone has to tell the customer.
Retries alone won't work — outreach is the only path.

The mistake most founders make is treating both the same.
One email sequence. Same retry cadence. For every failure.

But an expired card and an insufficient funds failure need completely different responses.

I built DunnAI around this insight.

DunnAI reads the decline code behind every failure, classifies it into the right bucket, and reaches out in your product's own voice - only when outreach is actually the answer.

Today it's live on the Stripe App Marketplace.

Free until $49 is recovered.

Curious if others have thought about this split.
What's your current approach to failed payments?

on April 8, 2026
  1. 1

    Full disclosure: I'm Avery Lin (avrlin). I've been packaging a small Stripe Dunning Email Pack (failure-reason matrix → day-0/3/7 copy + Customer Portal CTA) with AI assistance, so take this as adjacent interest, not neutral advice.

    The two-bucket split you describe is exactly where generic dunning breaks: Smart Retries help NSF/network timing issues, but expired_card / do_not_honor need a human-sounding day-1 ask to update the card — another retry just burns goodwill. Branching the first email by decline family is the boring fix that recovers the easy ones without sounding like a bug ticket.

    Curious — when you classify failures, do you treat do_not_honor as soft Bucket 2 (gentle outreach + one retry) or hard Bucket 2 (outreach only)?