← Dunning Engine

Stripe decline codes: which to retry, which never to

When a Stripe subscription payment fails, the decline_code says what recovery can do. Some codes mean a retry may work tomorrow. Others mean every retry is wasted and the only fix is a new card. This is the table Dunning Engine, a free self-hosted recovery server, acts on.

The five classes and the default plan

Day numbers count from the failed payment, using the default policy (14-day grace period). Everything is configurable.

ClassRetriesWhyEmailsRetry days
hardNever retriedHard decline. Retrying cannot help and can look like card testing. Customer is asked for a different card.day 0, day 3, day 7, day 11none
updateNever retriedThe card itself is unusable until it is replaced or corrected. Customer is asked to update it.day 0, day 3, day 7, day 11none
fundsRetried on a spread scheduleMay clear later. Retries are spaced out and, by default, nudged toward payday.day 0, day 3, day 7, day 11day 4, day 5, day 10, day 13
authCustomer onlyOnly the cardholder can complete authentication. The email asks them to confirm the payment with their bank.day 0, day 3, day 7, day 11none
softRetried on a spaced scheduleGeneric or temporary. Retries 1, 3, 6 and 10 days out by default.day 0, day 3, day 7, day 11day 1, day 3, day 6, day 10

Retry days for funds are shown after the optional payday nudge (the 1st, 2nd, 15th, 16th or a Friday), so they shift with the calendar. The days above are for a failure on Monday 5 January.

Every decline code it knows

decline_codeWhat it meansClass
stolen_cardThe issuer reports the card stolen.hard
lost_cardThe issuer reports the card lost.hard
pickup_cardThe issuer wants the card taken out of use.hard
fraudulentThe issuer suspects fraud on this payment.hard
restricted_cardThe card is restricted and cannot be used for this kind of payment.hard
invalid_accountThe card or the account it is linked to is not valid.hard
new_account_information_failedThe issuer rejected the new account information.hard
expired_cardThe card has expired.update
incorrect_numberThe card number is wrong.update
invalid_numberThe card number is not valid.update
invalid_expiry_monthThe expiry month is not valid.update
invalid_expiry_yearThe expiry year is not valid.update
invalid_cvcThe security code is not valid.update
incorrect_cvcThe security code is wrong.update
incorrect_zipThe postal code does not match the card.update
card_not_supportedThe card does not support this type of purchase.update
currency_not_supportedThe card does not support this currency.update
insufficient_fundsNot enough available balance or credit.funds
withdrawal_count_limit_exceededThe card hit its transaction-count limit.funds
card_velocity_exceededThe card hit a spending or velocity limit.funds
authentication_requiredThe bank needs the customer to authenticate (3D Secure / SCA).auth
authentication_not_handledThe payment needed authentication that was not completed.auth
generic_declineThe issuer declined without saying why.soft
do_not_honorThe issuer declined without saying why (the most common vague decline).soft
try_again_laterThe issuer asked for the payment to be tried again later.soft
issuer_not_availableThe issuer could not be reached.soft
processing_errorAn error occurred while processing the card.soft
reenter_transactionThe issuer could not process it and asks for a retry.soft
call_issuerThe issuer wants the cardholder to contact them.soft
transaction_not_allowedThe issuer does not allow this transaction on the card.soft

Any code not listed, and a failure with no decline code at all, is treated as soft. call_issuer and transaction_not_allowed also land there: the bank has to act, so if the spaced retries keep failing the customer's emails ask for another card.

Meanings are paraphrased from Stripe's public documentation of decline codes and can change; Stripe's own page is the authority, and the classification above is a policy choice, not a Stripe statement. Not affiliated with Stripe.

Run this plan on your own Stripe account

Dunning Engine is MIT-licensed, one Node process with no dependencies. It starts in dry-run, plans recovery per decline class, sends neutral emails with a never-expiring update-card link, and runs in your account with your keys, with no share of what it recovers. Preview the plan for any code in the browser simulator.

Common questions

Which Stripe decline codes should I retry?

Retry only decline codes where the payment can succeed later without the customer doing anything: insufficient_funds, the velocity and withdrawal-limit codes, and the generic or temporary issuer declines such as generic_decline, do_not_honor, try_again_later and processing_error. Do not retry lost_card, stolen_card, pickup_card, fraudulent or restricted_card, and do not retry expired or incorrect card details until the customer has replaced the card.

What does do_not_honor mean in Stripe?

do_not_honor is the issuer's vague refusal: the bank declined the payment and chose not to say why. It is not proof the card is bad. Dunning Engine treats it as a soft decline: it retries on a spaced schedule and emails the customer an update-card link in case the bank keeps refusing.

Where do I find the decline code for a failed Stripe payment?

Open the failed payment in the Stripe Dashboard, or read last_payment_error.decline_code on the PaymentIntent behind the invoice, or the same field on the charge. Some failures have only an error code and no decline code; Dunning Engine treats a missing code as a soft decline.

Use it safely

No warranty. This page is provided free and as-is, without warranty of any kind. It is not legal, tax, or financial advice. You are responsible for verifying anything before relying on it, including the email and consumer-protection rules for payment-failure notices in your country. To the maximum extent permitted by law, the authors accept no liability for any loss arising from its use.

Best-effort hosting, no SLA. These pages are free and experimental and may be moved or taken offline without notice.

Free and open source under an MIT licence. Sponsoring is optional and never changes what the tool does.