Which Stripe webhooks tell you a payment failed | RecoverFlow

Guide

Which Stripe webhooks tell you a payment failed

Last updated 28 July 2026 · written by Bruce McGinley, who builds RecoverFlow

Short answer

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.

The events worth handling

EventWhen Stripe sends it
invoice.payment_failedA payment for an invoice failed. This is your primary trigger for everything dunning related.
invoice.payment_action_requiredThe invoice requires customer authentication. Somebody has to complete a 3D Secure step and only the cardholder can do it.
invoice.paidThe invoice was successfully paid. Provision access here, checking that the subscription status is active.
invoice.upcomingSent a few days before renewal. How many days is set by "Upcoming renewal events" in the Dashboard.
customer.subscription.updatedA subscription started or changed. Renewals, coupons, discounts, invoice items and plan changes all fire this.
customer.subscription.deletedA 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.

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.

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