← Blog

Stripe decline codes explained: what each means and whether to retry

June 6, 2026 · 5 min read

When a recurring charge fails, the decline code is the only fact that should decide what you do next. Get Stripe decline codes explainedthe right way and recovery becomes simple: retry the failures that can succeed on the same card, and stop wasting attempts on the ones that never will. Treat every code the same — which is what most default setups do — and you both burn issuer trust and throw away recoverable revenue.

Why the decline code should decide your next move

There are only two wrong moves after a failed payment, and a fixed retry schedule makes both. Retrying a harddecline — a closed account, a card reported stolen — burns an attempt that was never going to clear and signals to the issuer that you keep hammering a dead card. Not retrying a softdecline — a momentary insufficient-funds balance — throws away money that a single well-timed attempt would have collected.

Default Stripe dunning re-attempts on the same cadence regardless of why the charge failed. That is the core mistake: the reason for the failure, not a calendar, should drive the response. Once you read the decline code, the right action is usually obvious.

Soft vs hard declines: the one distinction that matters

Almost every code sorts into one of two buckets, and that split decides retry-or-not:

  • Soft decline— temporary. The same card can succeed on a later attempt: insufficient_funds, processing_error, an issuer's velocity or fraud hold. These are the ones worth retrying.
  • Hard decline — the card or account is unusable, and only a new payment method fixes it: lost_card, stolen_card, pickup_card, fraudulent, often expired_card. Retrying these does nothing but cost you attempts.

Card networks penalize merchants who repeatedly retry hard declines — some schemes levy per-attempt fees on certain reason codes, and a high reattempt-to-approval ratio can hurt your standing with issuers over time. So the cost of retrying a hard decline is not zero; it compounds.

The gray zone is the catch. do_not_honor and generic_declineare deliberately vague — the issuer declined but won't say why. Treat them as soft-ish: one or two spaced retries are reasonable, but if they keep failing, switch to asking for a new card rather than grinding. For the broader retry-and-dunning playbook, see Stripe dunning best practices alongside the table below.

The decline-code reference table

These are the codes you'll see most. Each row is meaning → soft/hard → the move (retry, send a card-update email, or stop):

  • insufficient_funds — no balance right now. Soft. Retry, ideally near a payday (see timing below).
  • processing_error — a transient error at the network or issuer. Soft. Short-delay retry within hours to a day.
  • do_not_honor — issuer declined, no reason given. Gray. One or two spaced retries, then a card-update email.
  • generic_decline — unspecified decline. Gray.Same as above — retry cautiously, then escalate to the customer.
  • card_declined— broad “not this card” signal; inspect the decline_code for the real reason. Depends. Classify, then act on the specific code.
  • expired_card — the card on file has expired. Hard. Don't retry — trigger Account Updater or send a card-update email.
  • incorrect_cvc / incorrect_number — bad card data. Hard for that data. A retry of the same details fails; request corrected details.
  • lost_card / stolen_card— card reported lost or stolen. Hard. Stop. Retrying risks fraud flags and disputes.
  • pickup_card — the issuer wants the card pulled. Hard. Stop and request a new method.
  • fraudulent — issuer suspects fraud. Hard.Stop entirely — further attempts invite chargebacks.
  • currency_not_supported— the card can't be charged in that currency. Hard.Don't retry; the customer needs a different card.
  • card_velocity_exceeded — too many attempts in a window. Soft. Back off, then retry later.

The pattern: soft codes go to a retry schedule, expired/incorrect-data codes go to a card-update email, and fraud/lost/stolen codes mean stop — chasing those is how you collect disputes instead of revenue.

Where to read the decline code in Stripe

The detailed code lives one level deeper than people expect. On a PaymentIntent it's last_payment_error.decline_code; on the charge it's reflected in charge.outcome plus the network reason. Don't confuse the high-level code / error type (e.g. card_declined) with the granular decline_code (e.g. insufficient_funds) — the second is the one that tells you what to do.

  • Read it off the invoice.payment_failedwebhook payload, via the attached PaymentIntent's last_payment_error.decline_code. Our webhooks reference shows where it sits.
  • Log every code you receive. Over a few weeks your own decline mix tells you whether you're mostly fighting soft funding gaps or hard dead cards — and that changes your whole strategy.

Code-specific retry timing that recovers money

Timing is where soft declines turn back into revenue. The schedule should fit the reason:

  • insufficient_funds— align retries with common payroll dates (around the 1st and 15th). A balance that's empty on the 9th is often funded by the 15th.
  • processing_error / temporary issues— a short-delay retry, hours to a day, since the condition usually clears fast.
  • expired_card— don't retry at all. Trigger Account Updater or request a new card; the expiry won't fix itself.
  • Respect the network reattempt caps. Spacing attempts out keeps you under issuer throttling — more retries is not more recovery.

Why one retry schedule for every decline leaves money behind

Stripe Smart Retries optimize whento retry and will pause on hard declines until a new payment method appears — but they don't let you tune the cadence to the specific reason a charge failed, and they treat your soft declines with a single approach. That is the gap: a temporary insufficient-funds balance and an issuer velocity hold get the same generic schedule, and a hard decline simply waits rather than triggering a card-update email. The fix is decline-aware routing — soft codes to a cadence tuned per reason, hard codes straight to a card-update ask. The difference between a one-size schedule and decline-aware retries is real recovery-rate, not a rounding error. For the broader playbook, see Stripe dunning best practices and how to reduce involuntary churn.

Where Backstop fits

Backstop reads the decline code on every failed charge and routes accordingly — soft declines go to a code-tuned retry schedule, and hard declines skip the pointless retries and go straight to a hosted card-update email. It sits on your existing Stripe account with no engineering, classifies your declines automatically, and reports the mix back so you can see what you're actually losing to. See how it works, then start free and let it sort your failures from day one.

Backstop recovers failed Stripe payments and saves canceling subscribers.

Smart retries, a visual cancel flow, and a hosted portal — flat $79/mo, a free tier, and 0% revenue share.