Why Stripe isn’t retrying a failed payment
Last updated 12 October 2026
A subscription goes past_due, the invoice shows a next retry date, and it looks like Stripe has it in hand. Often it hasn’t. Some of those retries are never going to run, some invoices were never declined in the first place, and when the retries do run out, where the customer ends up depends on a setting the API can’t even read.
A retry date doesn’t mean a retry
For a hard decline, Stripe keeps a retry scheduled and keeps adding to attempt_count, but never runs the retry until the customer adds a new payment method. So next_payment_attempt stays set forever. These are the codes:
incorrect_numberlost_cardpickup_cardstolen_cardauthentication_requiredtransaction_not_allowedrevocation_of_authorizationrevocation_of_all_authorizationsfraudulentmerchant_blacklist
Whether Stripe will really try again takes both checks:
const willRetry = (invoice, declineCode) =>
invoice.next_payment_attempt != null && !HARD_DECLINE.has(declineCode);If that’s false, the customer must never get a “we’ll try again on…” email. Nothing happens until they give you a new card, so the email has to ask for one. The decline code is on the PaymentIntent; the decline codes guide shows where to find it.
Hard declines also don’t clear themselves. A soft decline leaves the past-due list once a retry succeeds. A hard decline sits there until someone acts, so decide when it counts as gone. Gradsey counts a hard-declined invoice as failing for its first 14 days, then as churn.
No retry date doesn’t mean Stripe gave up
It works the other way too. Two kinds of past-due invoice have no next attempt and no decline code, because nothing was declined:
- Waiting on 3-D Secure. The PaymentIntent is at
requires_action,attempt_countis 0 andlast_payment_erroris empty. The customer only needs to approve the payment. An email that opens with “your card was declined” is wrong for them. - Not attempted yet. A new cycle’s invoice sits for a while before its first attempt.
If you read next_payment_attempt == null as “Stripe gave up”, both of these land on your churn list while they’re still paying customers. Check attempt_count first: if nothing has been attempted, nothing has failed.
How Stripe retries when it does
- Smart Retries defaults to 8 attempts over 2 weeks. You can set anything from 1 week to 2 months under Billing → Revenue recovery → Retries. Read that setting before you write a retry date into an email.
- In testing, the first retry came about 44 hours after the failure.
past_duedoesn’t promise another attempt either. That’s Stripe’s own wording, andwillRetryabove is the check that does.- If you drive retries yourself, stop at eight per charge. Stripe warns that more can read as fraud and get legitimate charges declined.
When the retries run out
What happens next is a Dashboard setting: Billing → Revenue recovery → Retries → “If all retries for a payment fail”. The API can’t read it, so anything that watches your account has to handle all three:
| Setting | The subscription becomes | Later invoices |
|---|---|---|
| Cancel the subscription | canceled, with cancellation_details.reason set to payment_failed | None |
| Mark as unpaid | unpaid | Created as drafts, never attempted |
| Leave past-due | past_due, with no next attempt | Created open and charged on the retry schedule |
“Leave past-due” isn’t the end. At the next billing cycle, the new invoice gets a fresh retry date and the customer looks like a failing payment again, then lands back here when those retries run out, roughly every month. If you email both groups, keep track of who got what, or the same person gets “your card failed” and “we miss you” in alternating months.
Check what your own app does
An access check like status !== 'canceled' keeps serving unpaid and dead past_due customers for free, indefinitely. Nobody notices that leak either.
Customers you invoice by hand
A subscription with collection_method: send_invoice has no retry schedule at all: its next_payment_attempt is always null. “Your card failed” is the wrong sentence for someone who was emailed an invoice and hasn’t paid it yet, so keep them off a failed-payments list. They need a rule based on the invoice’s due date instead.
Every customer on exactly one list
Put together, each past-due customer belongs in one place. If Stripe will really retry, they’re a failing payment, and the email asks them to fix the card before the retry. If Stripe never will (a hard decline that has aged out, or retries that ran out), they’re churned, and the email is a win-back. If nothing was attempted, they haven’t churned; they’re waiting on a charge or on the customer, and the email says only that.
Gradsey splits your Stripe account this way on every scan. Failed payments by decline code lists who Stripe is still trying. Worth winning back lists who it gave up on, with how long they’ve been gone and what they were paying. Nobody is on both.