Guide
invoice.payment_failed vs payment_intent.payment_failed
Last updated 13 August 2026 · written by Bruce McGinley, who builds RecoverFlow
Same failure, different layers. Stripe's card declines documentation describes payment_intent.payment_failed as triggering when a payment attempt is unsuccessful. It speaks at the level of the individual payment.
invoice.payment_failed fires when a payment for an invoice fails, and it is the event Stripe's Smart Retries documentation tells you to use to receive subscription payment failure events and retry attempt updates.
So the rule of thumb: subscriptions and invoices on Stripe Billing, build on invoice.payment_failed. One-off payments created directly as PaymentIntents, with no invoice in the picture, listen to payment_intent.payment_failed.
On this page
Two layers, two vocabularies
In Stripe Billing, the invoice is the thing being collected and a payment attempt is how collection happens. When an attempt fails, the failure is visible at both layers: the payment layer, where the PaymentIntent holds the error, and the billing layer, where the invoice tracks the state of collection. Stripe also documents that invoice.updated is sent when a payment succeeds or fails, and that payment failures additionally trigger invoice.payment_failed.
The decline code itself lives at the payment layer. Stripe's declines documentation points to last_payment_error.decline_code on the PaymentIntent as the place to read why the issuer declined. So even an invoice-driven dunning flow reaches into the PaymentIntent for the reason, and then decides what to do based on it.
Why the invoice event wins for subscriptions
The invoice event carries the retry state, which is the context dunning decisions need. Per Stripe's Smart Retries documentation, attempt_count on the invoice.payment_failed webhook indicates how many attempts have been made so far, and next_payment_attempt indicates when Stripe will attempt the next collection. A PaymentIntent knows it failed; the invoice knows where you are in the schedule.
Stripe's subscription webhooks documentation also attaches its recommended failure playbook to this event: notify the customer, collect new payment information and confirm the PaymentIntent, update the default payment method on the subscription, and consider enabling Smart Retries.
One current caveat from the same documentation: for users of Billing automations, next_payment_attempt is no longer set in invoice.payment_failed webhooks and is set in invoice.updated webhooks instead. If that field reads null and you use automations, that is why.
Listening to both without emailing twice
There is nothing wrong with subscribing to both events; problems start when both drive the same side effect. A subscription payment failure exists at both layers, so a pipeline that sends an email on each event you receive will greet one failure with two emails.
The clean shape is one trigger and one enrichment. Let invoice.payment_failed be the trigger for anything customer-facing, read the decline code from the invoice's PaymentIntent, and keep payment_intent.payment_failed for the payments that have no invoice, where it is the only signal you get.
Our field-by-field webhook guide covers what to read once the event arrives, including the attempt_count trap on hard declines, and the test cards guide shows how to fire both events on demand before you trust the pipeline.
Questions people actually ask
Which webhook should I use for failed subscription payments?
invoice.payment_failed. Stripe's Smart Retries documentation says to use it to receive subscription payment failure events and retry attempt updates, and it carries attempt_count and next_payment_attempt, the state a dunning flow needs.
When does payment_intent.payment_failed fire?
Stripe's card declines documentation describes it as triggering when a payment attempt is unsuccessful. It is the payment-layer signal, and for one-off PaymentIntents with no invoice it is the failure event to build on.
Where do I find the decline code?
On the PaymentIntent: Stripe's documentation points to last_payment_error.decline_code. Invoice events tell you collection failed and where the schedule stands; the PaymentIntent holds the issuer's reason.
Why is next_payment_attempt null on my invoice.payment_failed event?
If you use Billing automations, Stripe documents that next_payment_attempt is no longer set in invoice.payment_failed webhooks and is set in invoice.updated webhooks instead. Otherwise, on invoices that are not being automatically collected there is no scheduled attempt to report.
Do both events fire for one subscription payment failure?
The failure exists at both layers, the invoice and the PaymentIntent, so an endpoint subscribed to both event types will hear about it from both directions. Drive customer-facing actions from one of them, invoice.payment_failed, to avoid duplicate emails.
Where this came from
Checked against primary sources on 13 August 2026. If Stripe changes something and this page has not caught up, tell us and it gets fixed.
- Stripe: Smart Retries — the source for the nine hard decline codes and what happens to scheduled retries after one.
- Stripe: Using webhooks with subscriptions — event descriptions and Stripe's recommended actions on payment failure.
- Stripe: Card declines — advice codes, retry limits, and Stripe's guidance on reducing declines.
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 can attribute to a specific action it took, with a $29 monthly floor and a $299 monthly ceiling, and the floor is waived for the first 30 days.
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