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.
Day numbers count from the failed payment, using the default policy (14-day grace period). Everything is configurable.
| Class | Retries | Why | Emails | Retry days |
|---|---|---|---|---|
| hard | Never retried | Hard decline. Retrying cannot help and can look like card testing. Customer is asked for a different card. | day 0, day 3, day 7, day 11 | none |
| update | Never retried | The card itself is unusable until it is replaced or corrected. Customer is asked to update it. | day 0, day 3, day 7, day 11 | none |
| funds | Retried on a spread schedule | May clear later. Retries are spaced out and, by default, nudged toward payday. | day 0, day 3, day 7, day 11 | day 4, day 5, day 10, day 13 |
| auth | Customer only | Only the cardholder can complete authentication. The email asks them to confirm the payment with their bank. | day 0, day 3, day 7, day 11 | none |
| soft | Retried on a spaced schedule | Generic or temporary. Retries 1, 3, 6 and 10 days out by default. | day 0, day 3, day 7, day 11 | day 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.
| decline_code | What it means | Class |
|---|---|---|
stolen_card | The issuer reports the card stolen. | hard |
lost_card | The issuer reports the card lost. | hard |
pickup_card | The issuer wants the card taken out of use. | hard |
fraudulent | The issuer suspects fraud on this payment. | hard |
restricted_card | The card is restricted and cannot be used for this kind of payment. | hard |
invalid_account | The card or the account it is linked to is not valid. | hard |
new_account_information_failed | The issuer rejected the new account information. | hard |
expired_card | The card has expired. | update |
incorrect_number | The card number is wrong. | update |
invalid_number | The card number is not valid. | update |
invalid_expiry_month | The expiry month is not valid. | update |
invalid_expiry_year | The expiry year is not valid. | update |
invalid_cvc | The security code is not valid. | update |
incorrect_cvc | The security code is wrong. | update |
incorrect_zip | The postal code does not match the card. | update |
card_not_supported | The card does not support this type of purchase. | update |
currency_not_supported | The card does not support this currency. | update |
insufficient_funds | Not enough available balance or credit. | funds |
withdrawal_count_limit_exceeded | The card hit its transaction-count limit. | funds |
card_velocity_exceeded | The card hit a spending or velocity limit. | funds |
authentication_required | The bank needs the customer to authenticate (3D Secure / SCA). | auth |
authentication_not_handled | The payment needed authentication that was not completed. | auth |
generic_decline | The issuer declined without saying why. | soft |
do_not_honor | The issuer declined without saying why (the most common vague decline). | soft |
try_again_later | The issuer asked for the payment to be tried again later. | soft |
issuer_not_available | The issuer could not be reached. | soft |
processing_error | An error occurred while processing the card. | soft |
reenter_transaction | The issuer could not process it and asks for a retry. | soft |
call_issuer | The issuer wants the cardholder to contact them. | soft |
transaction_not_allowed | The 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.
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.
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.
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.
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.
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.