Guide
Which Stripe webhooks tell you a payment failed
Last updated 28 July 2026 · written by Bruce McGinley, who builds RecoverFlow
invoice.payment_failed is the event to build on. It fires when a payment for an invoice fails, and it carries everything you need: the decline code, the attempt count, and when Stripe will try again.
Pair it with invoice.payment_action_required for payments that need customer authentication, and customer.subscription.updated to catch the status change from active to past_due.
On this page
The events worth handling
| Event | When Stripe sends it |
|---|---|
invoice.payment_failed | A payment for an invoice failed. This is your primary trigger for everything dunning related. |
invoice.payment_action_required | The invoice requires customer authentication. Somebody has to complete a 3D Secure step and only the cardholder can do it. |
invoice.paid | The invoice was successfully paid. Provision access here, checking that the subscription status is active. |
invoice.upcoming | Sent a few days before renewal. How many days is set by "Upcoming renewal events" in the Dashboard. |
customer.subscription.updated | A subscription started or changed. Renewals, coupons, discounts, invoice items and plan changes all fire this. |
customer.subscription.deleted | A customer's subscription ended. |
Stripe's own recommended actions on invoice.payment_failed are worth repeating because they are in the right order: notify the customer, collect new payment information, update the default payment method on the subscription, and consider enabling Smart Retries.
Notice that "retry it yourself immediately" is not on that list.
The fields to read once you have the event
The invoice object carries the state of the retry process. Four fields do most of the work.
attempt_countis the number of payment attempts from the perspective of the retry schedule. The first attempt counts, then only automatic retries increment it. Manual payment attempts after the first do not affect the schedule.next_payment_attemptis when Stripe will next try. It isnullfor invoices wherecollection_method=send_invoice, because nothing is being charged automatically.attemptedis whether any attempt has been made at all.billing_reasontells you why the invoice exists:subscription_create,subscription_cycle,subscription_update,subscription_threshold,subscription,manual,quote_accept,upcoming, orautomatic_pending_invoice_item_invoice.
billing_reason is the field most people ignore and it is the one that lets you write good emails. A failure on subscription_create is a person who never got started. A failure on subscription_cycle is an existing customer whose card broke. Those deserve completely different messages, and the same generic "your payment failed" for both is why dunning email performance is usually mediocre.
The attempt_count trap
Stripe's API reference spells out something that catches people building their own recovery logic. If a failure returns a non-retryable code, the invoice cannot be retried unless a new payment method is obtained. But, in Stripe's words, retries continue to be scheduled and attempt_count continues to increment, and retries are only executed if a new payment method is obtained.
What this means in practice: a rising attempt_count is not evidence that anything is being attempted. On a stolen card you can watch the counter climb to eight without a single charge ever reaching the issuer.
If your escalation logic is "after four attempts, send the stronger email", it will behave sensibly for soft declines and nonsensically for the nine codes that block execution. Branch on the decline code first, then on the count. The list of those nine codes is short and worth memorising.
The one hour delay nobody expects
Stripe notes that an invoice is not attempted until one hour after the invoice.created webhook. Their own suggestion is that you might not want to show that invoice as unpaid to your users during that window.
This is a small thing that produces silly bugs. If you build a dashboard that flags every unpaid invoice the moment it appears, you will show customers a scary "payment failed" banner for an hour before anyone has tried to charge them. Check attempted before you tell anybody anything.
First payments behave differently
When the failure is on a subscription's first invoice, the subscription goes to incomplete, not past_due, and the customer has 23 hours before it expires permanently. Renewal failures go to past_due and get the full retry window.
So the same invoice.payment_failed event can mean "you have 23 hours" or "you have two weeks" depending on billing_reason. Handling both with one code path is the single most common mistake in home built dunning. The status guide covers what each state means.
Questions people actually ask
Which Stripe webhook fires when a subscription payment fails?
invoice.payment_failed. It fires when a payment for an invoice fails, and it is the correct trigger for notifying the customer and starting a dunning sequence. For failures that need customer authentication rather than a new card, invoice.payment_action_required fires instead.
Does attempt_count tell me how many times the card was actually charged?
No. Stripe increments attempt_count according to the retry schedule even when it is not executing the retry, which happens on the nine decline codes that require a new payment method. It also does not increment for manual payment attempts after the first. Treat it as a position in the schedule, not a count of network requests.
What does next_payment_attempt being null mean?
For invoices with collection_method=send_invoice it is always null, because Stripe is not charging a card automatically. On an automatically charged invoice, null generally means there is no further attempt scheduled.
How do I tell a signup failure from a renewal failure?
Read billing_reason on the invoice. subscription_create is the first payment, and the subscription will be incomplete with a 23 hour window. subscription_cycle is a renewal, and the subscription will be past_due with the full retry window. They need different emails and different urgency.
Should I retry the payment myself when I get invoice.payment_failed?
Usually not. Stripe's own recommended actions are to notify the customer, collect new payment information, update the default payment method, and consider enabling Smart Retries. If Smart Retries is on, Stripe is already retrying on a schedule chosen by a model trained on far more data than you have. Manual attempts after the first also do not affect that schedule.
Where this came from
Checked against primary sources on 28 July 2026. If Stripe changes something and this page has not caught up, tell us and it gets fixed.
- Stripe: Subscription webhooks
- Stripe: Invoice object, API reference — the field definitions for attempt_count, next_payment_attempt, attempted and billing_reason.
- Stripe: Subscription lifecycle and statuses — the source for every status definition quoted on this page.
If you would rather not build this yourself
RecoverFlow watches your Stripe account for failed subscription payments, stops retrying the ones that cannot succeed, and emails the customers whose card simply needs replacing. It charges 25% of what it actually recovers, with a $29 monthly floor, and nothing at all if it recovers nothing.
It is early. It is run by one person. If Stripe's own free retry settings are enough for you, use those instead, and there is a page on this site that says exactly when that is the right call.
See how the pricing works
Recover