When a payment fails in Stripe, you get back a cryptic code like insufficient_funds, do_not_honor, or generic_decline. Most SaaS founders see these, shrug, and move on — because Stripe's documentation describes what they mean but not what to do about them.
That's a problem, because the decline code is the single most important signal for whether a failed charge is recoverable. Retrying the wrong kind of decline wastes attempts and burns customer trust. Not retrying a recoverable one leaves money on the table.
This guide translates every common Stripe decline code into plain English and tells you exactly what to do with each: retry immediately, retry later, ask the customer to update, or write it off.
The Two Categories That Matter
Before the codes themselves, there's one framing that makes everything else click: soft declines vs. hard declines.
- Soft declines are temporary. The card itself is valid, but something blocked this specific charge at this specific moment. Insufficient funds, bank processing errors, fraud flags that need a second look. These are recoverable — usually by retrying at a better time.
- Hard declines are permanent. The card is dead, closed, reported lost, or the issuing bank has said "no" in a way that won't change. Retrying these doesn't help. You need a new card from the customer.
Stripe sends both categories back through the same API response, but they need opposite treatment. A retry strategy that ignores the difference either gives up on recoverable revenue or pesters customers whose cards will never work again.
Soft Declines: Retry These
These are the codes where a smart retry strategy — waiting 24 to 96 hours and trying again at a better time of day — recovers 60 to 94% of attempts, depending on the code and your customer base.
| Code | Meaning | Strategy |
|---|---|---|
insufficient_funds | Not enough money in the account right now | Retry in 3–5 days, typically after payday |
generic_decline | Bank refused but didn't say why | Retry 24–48 hours later, different time of day |
do_not_honor | Bank declined for unspecified reasons (often fraud suspicion) | Retry in 24–72 hours; email customer if second attempt fails |
processing_error | Temporary issuer or network error | Retry immediately, then again in a few hours |
issuer_not_available | Issuing bank's system is down | Retry in 1–2 hours, then in 24 hours |
try_again_later | Bank explicitly said to try later | Retry in 24–48 hours |
call_issuer | Bank wants to verify with the cardholder | Email the customer; retry after 2–3 days |
approve_with_id | Bank needs additional verification | Retry in 24 hours; email customer if repeats |
card_velocity_exceeded | Too many recent charges on this card | Retry in 48–72 hours |
reenter_transaction | Transaction needs to be resubmitted | Retry within 24 hours |
When to retry matters as much as whether to retry
A charge that fails at 11:47 PM on a Tuesday is much more likely to fail again if you retry at midnight. Bank batch processing, cardholder sleep cycles, and payroll timing all affect success rates. The difference between "retry every 6 hours" and "retry on the morning of day 3" can be 20+ percentage points on recovery rate.
Tip
For insufficient_funds specifically, retries tend to succeed most on the 1st, 15th, and Fridays — paydays in most of the US. Retrying on a Monday morning after a weekend failure is often worse than waiting until Friday.
Hard Declines: Don't Retry
These codes mean the card will never work. Retrying is wasted effort and, on the merchant side, can actually hurt your Stripe account health. Instead, immediately email the customer asking for a new payment method.
| Code | Meaning | Strategy |
|---|---|---|
expired_card | Card is past its expiration date | Email customer; offer update link |
incorrect_cvc | CVC doesn't match | Ask customer to re-enter card |
incorrect_number | Card number is invalid | Ask customer to re-enter card |
card_not_supported | Card type not accepted by merchant | Ask for different card |
currency_not_supported | Card can't transact in that currency | Ask for different card or offer different currency |
lost_card | Card reported lost | Do not retry; request new card |
stolen_card | Card reported stolen | Do not retry; request new card |
pickup_card | Bank has flagged the card (fraud) | Do not retry; request new card |
restricted_card | Card has restrictions preventing charges | Request different card |
fraudulent | Stripe Radar or bank flagged as fraud | Do not retry; manual review |
The retry-then-abandon anti-pattern
A very common mistake: retrying a hard-declined card 4 or 5 times over a week, then giving up and canceling the subscription. The customer never got an email. They find out three months later when they check their bank statement and wonder why their subscription vanished.
For hard declines, the right move is immediate customer notification. Send a friendly email within an hour of the failure with a one-click link to update the card. Follow up in 3 days, then 7 days. Well-timed dunning emails convert better than silent retries ever will.
The Special Cases
3D Secure and authentication required
authentication_required means the bank wants the cardholder to verify through 3D Secure (a one-time code or app approval). This is increasingly common in Europe under PSD2 rules but is appearing more often in the US too.
You can't "retry" this silently. You have to redirect the customer to an authentication flow. If you're using Stripe Checkout or Payment Element, this is handled for you. If you're using a custom flow, you need to implement the PaymentIntent confirmation step.
Fraud signals from Stripe Radar
Codes like fraudulent, pickup_card, and some do_not_honor responses come from Stripe's fraud detection. Retrying these repeatedly can damage your Radar score and lead to more false positives on legitimate charges later.
If a legitimate customer is getting fraud-flagged, the fix isn't more retries — it's asking them to re-enter the card (which often clears the flag) or contact their bank.
How ChurnShield Handles Each Code
ChurnShield's retry engine classifies every decline code into one of three buckets and takes different action on each:
- Retry immediately eligible —
processing_error,issuer_not_available. These get a retry within minutes, because the issue is transient. - Retry with delay —
insufficient_funds,generic_decline,do_not_honor, etc. These get retried on a schedule optimized for the code type (3 days for insufficient funds, 24 hours for generic decline, etc.) and time-of-day shifted to avoid repeating the original failure window. - No retry —
expired_card,stolen_card,fraudulent, etc. These trigger the dunning email sequence immediately, with copy tailored to the specific problem (an "update your card" email reads differently for an expired card vs. a fraud flag).
The result: higher recovery on the soft declines, faster customer notification on the hard ones, and no wasted retries in between. On a typical SaaS Stripe account, code-aware retry logic recovers 18–32% more revenue than Stripe's default retry schedule.
Your Own Numbers
Before you build any of this, know which codes actually show up in your Stripe account. Every SaaS has a different mix — a consumer app sees more insufficient_funds, a B2B tool sees more do_not_honor and call_issuer.
The free ChurnShield Stripe Audit shows you the top 5 decline codes in your account over the last 90 days, with a dollar amount next to each. That's usually the fastest way to figure out which of the codes above actually matter for your business.