Introduction
Stripe returned a failure code. Most dunning tools ignore it and send the same email anyway.
That is the expensive mistake. A failed subscription payment is not one problem. It can mean the card expired, the customer has a temporary cash issue, the bank blocked the charge, the card was lost, the issuer needs authentication, or the payment network simply had a bad moment. Stripe gives you the first clue in the failure code. Your recovery workflow should use it.
The founder question is not "what does this code mean in Stripe's API?" The real question is more practical: "My customer got card_expired or insufficient_funds. What email do I send, when do I send it, and should I retry?"
This guide focuses on the codes SaaS founders see most often in Stripe Billing: card_expired or expired_card, insufficient_funds, do_not_honor, and generic card_declined. It also covers less frequent codes that matter because the wrong recovery action can hurt conversion, annoy customers, or make a bank more likely to decline later attempts.
If you want to compare your own rate before reading the playbook, use the free Stripe failed payment benchmark. For the wider retention system, read the complete guide to involuntary churn and the dunning guide for SaaS.
How Stripe failure codes should change your recovery workflow
Every failed payment gives you three decisions to make.
- What should the customer hear?
- When should you retry?
- Should this stay automated, or should a founder step in?
The common mistake is to treat every failure like an old card. That creates awkward emails. If the customer has insufficient_funds, an urgent "update your card now" email can feel tone-deaf. If the bank returned do_not_honor, quietly retrying the same charge without telling the customer often repeats the same failure. If the code is sensitive, such as stolen_card, you should not repeat that literal reason in customer copy.
Good payment recovery starts with the code, translates it into customer-safe language, and chooses timing that matches the failure.
The 4 most common Stripe failure codes in SaaS
These four buckets cover the highest-leverage recovery work for most small SaaS teams. The exact mix varies by geography, price point, card type, and customer segment, but the email logic is stable.
card_expired or expired_card - the easy card update
Average frequency in SaaS benchmarks: ~0.8% of attempted subscription payments.
Average recovery rate with targeted follow-up: ~63%.
What it means
The saved card is out of date. Stripe commonly surfaces this as expired_card, while founders and customers often search for card_expired. In practice, both phrases point to the same recovery path: the card details need to be updated.
What the customer experiences
Usually nothing. The customer may have received a replacement card weeks ago, updated it for personal services, and forgotten that your SaaS still has the old one saved. They are not necessarily trying to cancel. They are not necessarily unhappy. They probably do not know their subscription is at risk until you tell them.
That is why this code is highly recoverable. The action is simple, low-friction, and easy to explain.
Email recommended
Use a friendly, specific card-update email. The tone should be calm, not urgent. The job is to make the update feel quick.
Subject:
Your card on file expired
Body:
Hi, your payment for [Product] did not go through because the card on file appears to be expired. You can update it here in under a minute: [secure billing link]. After that, we will retry the invoice automatically.
Timing
Send this on day 0, as soon as the payment fails. If you have a card account updater running through Stripe or your processor, let that work too, but do not wait several days to notify the customer. The fix is obvious and the tone can be light.
What not to do
Do not make it sound like the customer did something wrong. Do not use aggressive suspension language in the first email. Do not keep retrying the same expired card every few hours. Get the card updated, then retry.
insufficient_funds - the timing problem
Average frequency in SaaS benchmarks: ~1.9%.
Average recovery rate with targeted follow-up: ~31%.
What it means
The issuer declined the payment because the customer did not have enough available funds, available credit, or card limit at that moment.
This does not always mean the customer is in financial trouble. It can be a temporary balance issue, a debit card waiting for payroll, a corporate card limit, or a bank rule that resets later.
What the customer experiences
Unlike expired_card, the customer may know exactly what happened. A heavy-handed message can feel embarrassing. The recovery path is less about "fix your card" and more about "we will try again later."
This is where generic dunning emails often fail. Asking the customer to update a card may be pointless if the card is still the right one. Retrying immediately may also fail again because nothing changed.
Email recommended
Use an empathetic wait-and-retry email. Keep the update link available, but make it optional.
Subject:
We will retry your payment in a few days
Body:
Hi, your payment for [Product] did not go through today. No action is needed right now - we will retry it automatically in a few days. If you would rather use another card, you can update your payment method here: [secure billing link].
Timing
Wait at least 3 days before the next retry. For many SaaS teams, day 3 and day 7 retries perform better than repeated immediate retries. If the account is high-value, add a short human note before the last attempt.
What not to do
Do not retry instantly in a tight loop. Do not lead with "update your card" unless you see repeated failures or the customer asks for another method. Do not write copy that makes the customer feel singled out.
do_not_honor - the bank block
Average frequency in SaaS benchmarks: ~0.7%.
Average recovery rate with targeted follow-up: ~44%.
What it means
The issuing bank blocked the transaction and did not share a public reason. Stripe cannot always tell you why because the issuer keeps that decision private. It can be fraud prevention, risk scoring, geography, merchant category, card settings, or another bank-side rule.
What the customer experiences
This is confusing. The card may work at a grocery store, with another SaaS product, or for a smaller transaction five minutes later. From the customer's perspective, nothing is wrong with the card. From your perspective, Stripe says the bank blocked this charge.
That means the customer needs two clear paths: approve the charge with the bank, or use a different payment method.
Email recommended
Explain the bank block without sounding accusatory.
Subject:
Your bank blocked this subscription charge
Body:
Hi, your bank did not approve the latest [Product] payment. This can happen even when the card works elsewhere. You can either approve the charge with your bank, or add another card here: [secure billing link]. Once that is done, we will retry the invoice.
Timing
Send on day 0. This is urgent because another silent retry often repeats the same decline. Give the customer context before the next attempt.
What not to do
Do not retry several times before contacting the customer. Do not say "Stripe blocked your card" if the issuer was the blocker. Do not imply the customer entered bad information.
card_declined or generic_decline - the unclear decline
Average frequency in SaaS benchmarks: ~1.6%.
Average recovery rate with targeted follow-up: ~28%.
What it means
The card was declined, but the failure does not give a precise customer-safe reason. Stripe may show a generic decline, a card_declined error type, or a failure state where the useful detail is limited.
This is the bucket where you should be careful. You do not know whether the customer needs to call the bank, use another card, wait for a retry, or authenticate.
What the customer experiences
The customer gets little context. They may assume your product is broken, their bank is broken, or they did something wrong. Your email should reduce uncertainty without pretending to know the cause.
Email recommended
Use conservative dual-option copy.
Subject:
We could not process your payment
Body:
Hi, the bank did not approve the latest [Product] payment. You can try another card here: [secure billing link], or approve the charge with your bank and we will retry the invoice.
Timing
Send on day 1 after one clean delayed retry window, unless Stripe advice says customer action is needed immediately. If the same code repeats, switch from automated retry to customer action.
What not to do
Do not over-explain. Do not diagnose the exact issue when Stripe did not give you one. Do not keep sending the same generic "payment failed" email every day.
Less frequent codes that still matter
The long tail of Stripe payment decline codes can be small in volume but painful when handled incorrectly. Here are the ones worth encoding into your recovery rules.
card_velocity_exceeded
The customer hit a transaction, spending, or card velocity limit. Do not keep retrying quickly. Space attempts out, explain that the bank limited the charge, and give the customer a chance to approve it or use another method.
Recommended copy:
Your bank limited this charge. You can approve it with the bank or switch to another card here: [secure billing link].
Recommended timing: wait at least 24 hours before retrying, and avoid repeated attempts unless the customer says the limit is resolved.
lost_card and stolen_card
Do not send an email that literally says "your card was stolen." Sensitive decline codes should usually be translated into generic customer-safe copy.
Recommended copy:
Your bank did not approve this card. Please add a different payment method to keep your subscription active.
Recommended timing: do not retry the same card. Ask for a new payment method.
expired_card vs card_expired
Stripe's decline code is commonly represented as expired_card, while marketers and founders often say card_expired. Your internal matching should accept both terms, but your customer-facing copy should avoid code names entirely. Say "your card appears to be expired."
do_not_honor vs restricted_card
Both can look like a bank-side block, but they deserve slightly different handling. do_not_honor is broad and private. restricted_card means the issuer has restrictions on that card or transaction type. In both cases, the customer may need to contact the bank or use another card. Avoid repeated automated retries without customer action.
authentication_required
The card may be fine, but the issuer requires a confirmation step such as 3D Secure or Strong Customer Authentication. A normal card-update email is not enough.
Recommended copy:
Your bank needs one extra confirmation before we can process this subscription payment. Use this secure link to approve it: [authentication link].
Recommended timing: do not retry off-session until the customer completes authentication.
incorrect_cvc, incorrect_zip, and incorrect_number
These point to wrong card details. Do not retry the same details. Ask the customer to re-enter payment information through a secure link.
Recommended copy:
The bank could not verify part of your card information. Please re-enter your payment details here: [secure billing link].
issuer_not_available and processing_error
These are often temporary processor, network, or issuer issues. In many cases, the best first step is a quiet retry later, not an immediate customer email.
Recommended copy, if an email is needed:
The payment network could not complete the charge. We will retry automatically. No action is needed right now.
Summary table
| Code | Frequency | Timing | Recovery avg | |
|---|---|---|---|---|
card_expired / expired_card | ~0.8% | Friendly update link | Day 0 | 63% |
insufficient_funds | ~1.9% | Empathetic wait | Day 3 | 31% |
do_not_honor | ~0.7% | Bank explanation | Day 0 | 44% |
card_declined / generic_decline | ~1.6% | Conservative dual-option | Day 1 | 28% |
card_velocity_exceeded | Low volume | Bank limit explanation | Wait 24h+ | Lower |
lost_card / stolen_card | Low volume | Generic new-card request | No same-card retry | Depends |
authentication_required | Market-dependent | Authentication link | After confirmation | High if handled |
incorrect_cvc / incorrect_zip | Low volume | Re-enter card details | After correction | Medium |
issuer_not_available / processing_error | Low volume | Usually no email first | Retry later | Medium |
How to turn the code into an actual dunning sequence
A code-aware recovery system does not need hundreds of templates. It needs a few clean branches.
Start with four primary tracks:
- Expired card: send a day 0 update-link email, then retry after update.
- Funds issue: wait, retry gently, and keep the update link optional.
- Bank block: explain the issuer block immediately and offer bank approval or another card.
- Generic decline: use conservative language and move to customer action if it repeats.
Then add guardrails:
- Sensitive hard declines should not expose the literal failure reason.
- Authentication failures should route to an authentication flow, not a generic billing link.
- Temporary network failures should get quiet retries before you bother the customer.
- High-value accounts should trigger a founder or support escalation before the last retry.
This is where many SaaS teams recover more revenue without sending more email. They send fewer generic messages and more precise ones.
Example recovery sequence by code
Here is a simple version a founder can implement before buying a full dunning platform.
For card_expired:
- Day 0: friendly update-link email.
- Day 2: reminder with the same link.
- Day 5: final notice before access is paused.
For insufficient_funds:
- Day 0: light notice that you will retry later.
- Day 3: retry automatically.
- Day 4: optional update-link email if the retry failed.
- Day 7: final retry and human note for high-value accounts.
For do_not_honor:
- Day 0: explain that the bank blocked the charge.
- Day 1: retry only if the customer has acted or Stripe advice allows it.
- Day 3: ask them to call the bank or use another card.
- Day 7: founder note for accounts worth saving manually.
For generic card_declined:
- Day 1: conservative dual-option email.
- Day 3: delayed retry.
- Day 5: ask for another payment method.
- Day 7: final reminder before suspension.
These are not magic numbers. They are a starting point. Your price point, customer type, geography, and invoice amount will change the best cadence. But the principle stays the same: the failure code chooses the first message and the retry posture.
FAQ
Is card_expired the same as expired_card?
For recovery purposes, yes. Stripe commonly documents and returns expired_card, while many founders search for card_expired. Treat both as the expired-card recovery branch.
Should I mention Stripe failure codes to customers?
Usually no. Codes are useful internally, but customers need plain language. Say "your card appears to be expired," "your bank did not approve this charge," or "we will retry in a few days."
Should I retry every failed payment immediately?
No. Immediate retries can work for temporary processor failures, but they are usually wrong for insufficient_funds, hard declines, authentication failures, and bank blocks. Let the code guide the retry timing.
What if Stripe gives me an advice code too?
Use it. Stripe advice codes can give more specific next-action guidance. The failure code explains what happened; advice can help decide whether to retry, wait, or request customer action.
Conclusion
Stripe failure codes are not just technical labels. They are recovery instructions.
card_expired needs a calm update link. insufficient_funds needs empathy and time. do_not_honor needs bank-context copy before another retry. Generic card_declined needs conservative language and clear options. Sensitive codes need customer-safe wording. Authentication codes need an authentication flow.
This is exactly what Dunlo automates: it reads the failure code first, then sends the right email at the right time. If you want the benchmark and recovery logic handled for you, connect Stripe to Dunlo. It is free during beta.