DEV Community

Mike Badger
Mike Badger

Posted on

Every Stripe decline code, what it actually means, and whether retrying will help

When a card payment fails in Stripe, you get a short string back — insufficient_funds, do_not_honor, authentication_required. Most teams glance at it and let Smart Retries hammer the card on a schedule.

That's the mistake. The right move depends entirely on the code:

  • Some failures clear themselves if you retry in a few days.
  • Some will never clear — and retrying them can get you fined by the card networks.
  • Many need the customer to act, so retrying the same card is pointless.

A big slice of "failed" subscription payments are recoverable — but only if you act on the reason. Here's every code that matters and what to do about it.

First: code vs decline_code

Stripe returns two fields, and they're not the same:

  • code — the high-level category, usually card_declined. Tells you almost nothing.
  • decline_code — the granular reason from the issuing bank (insufficient_funds, lost_card, do_not_honor). This is the one you act on.

Caveat: issuers don't always report the true reason — anti-fraud blocks often hide behind a vague do_not_honor. The granular code is a strong hint, not gospel.

The mental model: five buckets

Every decline maps to one action: retry, email the customer, stop, or fix your checkout.

1 — Auto-recoverable (retry will likely work)

Timing problems. The card is fine; the money wasn't there that moment, or the network hiccuped.

decline_code Meaning Action
insufficient_funds No money right now Retry — time it near payday
try_again_later Temporary issuer issue Retry in a few hours
processing_error Transient error Retry shortly
issuer_not_available Couldn't reach the bank Retry — clears fast

This is the only bucket where blind retries earn their keep.

2 — Needs customer action (retrying is pointless)

The card can't complete the charge as-is. Email the customer with the specific ask.

decline_code Meaning Ask the customer to
expired_card Card expired Update their card
incorrect_cvc Wrong CVC Re-enter card details
incorrect_number Wrong number Re-enter card details
authentication_required Bank needs 3DS / SCA Confirm the payment (see below)

The fix lives with the customer, not in your retry logic. A retry-only tool fails every one of these silently.

3 — Lost cause (stop retrying)

Card is dead, blocked, or flagged. Retrying won't help — and retrying network-flagged cards can trigger Visa/Mastercard penalties.

decline_code Meaning
lost_card / stolen_card Reported lost or stolen
pickup_card Bank wants it seized — never retry
fraudulent Flagged as fraud
revocation_of_authorization Customer told their bank to stop

Action: stop. Ask for a different card, or let it churn.

4 — Hard declines (the ambiguous middle)

The bank declined without saying why — often deliberately.

decline_code Meaning
do_not_honor Catch-all decline; often anti-fraud
generic_decline / card_declined No reason given
call_issuer Customer must call their bank
card_velocity_exceeded Too many charges, short window

Action: a couple of retries max, then email: "Your bank blocked this — a quick call to them, or a different card, usually fixes it." Many do_not_honor blocks lift in 30 seconds — if the customer knows to ask.

5 — Structural (fix checkout, not the payment)

Not about one customer — a recurring leak.

decline_code Meaning Fix
currency_not_supported Card can't be charged in that currency Offer a supported currency
card_not_supported Card type unsupported Check your Stripe capabilities
transaction_not_allowed Transaction type blocked Often regional — review where you sell

Fix it once, recover every future customer who'd hit the same wall.

The European trap: SCA and authentication_required

If you sell in Europe, this is what most US-built dunning tools get wrong.

  • Under PSD2 / SCA, many recurring EU payments need the customer to authenticate via 3D Secure.
  • When they don't, Stripe returns authentication_required.
  • Most tools retry the card — which can never succeed without authentication.
  • The fix: send a confirm-payment link (Stripe hosts the flow), not a retry.

For EU-facing SaaS, authentication_required is often a large, fully recoverable chunk that's silently written off.

One-page playbook

Bucket Do this
Auto-recoverable Time-aware retry (near payday)
Needs customer action Email the specific ask; for SCA, send a confirm link
Lost cause Stop retrying; ask for a new card
Hard decline A few retries, then "call your bank / try another card"
Structural Fix checkout config once

Takeaway

Blind retries only fix one of these five buckets. The rest need an email, a config fix, or a hard stop — so "just turn on Smart Retries" quietly leaves money on the table.

Try this: pull your last few months of failed payments and group them by decline_code. The split usually surprises people — especially how much is recoverable customer-action, not dead cards.

I'm building a tool that does this bucketing and routing automatically. Got war stories about declines, or opinions on what actually drives recovery? Drop them in the comments.

Top comments (2)

Collapse
 
mihirkanzariya profile image
Mihir kanzariya

The authentication_required section is the most important part of this post. In my experience, that single decline code is responsible for more recoverable revenue loss than all the "card_declined" variants combined, at least for products with European customers.

One thing worth adding to the playbook: the timing of your dunning email matters almost as much as what it says. For insufficient_funds, sending the "please update your card" email at 9am on a weekday gets roughly double the response rate compared to sending it at the moment the webhook fires (which is often 3am in the customer's timezone, when the renewal ran). Stripe's Smart Retries handle some of this, but your email layer probably doesn't.

The other pattern I've hit repeatedly: do_not_honor on the first charge after a trial ends. The customer's bank has never seen your merchant descriptor before, so it flags the charge as suspicious. The customer has no idea this happened because they never check their bank alerts. Two practical fixes: (1) run a $0 or $1 auth-and-void during the trial so the bank sees your descriptor before the real charge, (2) send the customer a "your trial ends tomorrow, here's what you'll be charged" email so they're not surprised and can whitelist the charge if their bank asks.

The five-bucket model is a clean way to think about it. Most teams I've seen just dump everything into "retry 3 times and then send a generic email," which is basically optimizing for bucket 1 and ignoring the other four.

Collapse
 
mike_badger profile image
Mike Badger

Thanks for your reply @mihirkanzariya — the auth-and-void trick to warm up the merchant descriptor before the first real charge sounds pretty new and interesting… Makes a lot of sense. 

The customer has zero visibility into why their bank flagged it — from their side it's "card declined for no reason," from the merchant's side it's just unexplained churn, until someone actually looks at the decline_code distribution.

Completely agree regarding authentication_required too — I called it out because it's the one bucket where "just retry harder" is actively useless, and most dunning advice is written from a US-centric view that doesn't deal with SCA at all.

The send-time point is a good one too. I don't have data on the 9am-vs-3am gap myself, but it tracks with general email behaviour, and it's an easy lever since it's just delaying a job — no payment logic to touch.

What ties all of this together for me: none of these fixes are visible if you're just retrying blindly and logging "failed" / "retrying" / "gave up." They only show up once someone's actually monitoring why things fail, not just whether they did.

Appreciate the detail — this is exactly the kind of stuff that doesn't show up in the Stripe docs.