Stripe decline codes: which failed payments to retry, and which never to touch
The invoice.payment_failed event says that a payment failed, not why. The reason is on the payment intent, and it decides what to do next.
Where the real reason lives
The invoice.payment_failed event carries the invoice. On older API versions the invoice carries a payment_intent id; on 2025-03-31.basil and later it does not, and you ask /v1/invoice_payments for it instead (details here). Either way the payment intent carries last_payment_error.decline_code. That last hop is the one most people never make, so they retry every failure the same way and get the same result.
Fetch the payment intent and read the decline code. That is the whole trick. A free n8n workflow that does exactly this, including the fetch and the classification below, is on GitHub, MIT licensed.
The four groups, and what each one wants
1. Hard declines: never retry
expired_card, lost_card, stolen_card, pickup_card, invalid_number, incorrect_number, invalid_account, account_closed, no_such_card. The card is gone. Retrying does nothing except raise your decline rate with the card networks, which eventually raises your costs. The only useful action is an email with a fresh card-update link, sent the same hour.
2. Insufficient funds: retry near payday
insufficient_funds is the largest bucket for most consumer and prosumer products. The card is fine, the balance is not. Retrying at 3am on day two rarely helps. Retrying on the 1st or the 15th, when salaries land, recovers a large share of these on its own. Wait 24 hours, then retry on the next payday, then email.
3. Technical errors: retry within hours
processing_error, issuer_not_available, try_again_later, reenter_transaction. Something between Stripe and the issuing bank blinked. Retry in one hour, six hours, and 24 hours. Most clear on the first attempt. Do not email the customer for these; they did nothing wrong and a "your payment failed" email costs you trust for no reason.
4. Everything else: soft retry, then email
generic_decline, do_not_honor, card_velocity_exceeded, authentication_required and the long tail. Retry on days 1, 3 and 7, then send the sequence. One exception inside this group: authentication_required means the bank wants the customer present, so skip the retries and send the card-update link straight away.
The field that tells you Stripe has given up
On the failed invoice, look at next_payment_attempt. If it is a timestamp, Stripe's own Smart Retries will try again then. If it is null, Stripe is done with that invoice. For some cards and some issuing banks Stripe never schedules a retry at all, so null shows up on the very first failure. Those customers are yours to recover or yours to lose; nothing automatic is coming.
Three mistakes that undo all of this
- Deduplicating on the invoice id. One invoice fails several times. Deduplicate on the event id, or you process the same failure three times and email the customer three times.
- Running your own retries with Smart Retries still on. The customer's card gets hit twice. Turn one of them off.
- Emailing without re-checking. Fetch the invoice again right before every email. A customer who updated their card an hour ago and then receives "your payment failed" is a customer who now doubts your product.
What this looks like as a running system
The classification above is the first four nodes of RevGuard's Stripe recovery workflow. The rest of it schedules the retries per group, sends a five-email sequence with a fresh billing-portal link in every message, re-checks the invoice before each action, and writes one record per case to a dashboard that shows recovered, open and lost in dollars. It runs on your own machine, costs $79 once for the Stripe edition, and the walkthrough shows a decline going through it end to end.