The invoice.payment_failed event fires when a subscription renewal charge fails. Your handler should send a payment failure email to the customer, restrict access based on your dunning policy, and update subscription status in your database. If this webhook fails to deliver, Stripe retries silently for up to 72 hours — meaning customer access and billing state drift from reality for days.
Stripe invoice.payment_failed Webhook: Handler Guide and Payload Reference
A subscription renewal fails. Stripe marks the invoice as unpaid and fires invoice.payment_failed to your webhook endpoint. Your handler runs dunning logic: sends the customer a payment failure email, applies your grace period or access restriction, and updates the subscription state in your database.
If the webhook delivery fails — your handler returns a 500, times out, or your endpoint is temporarily down — none of that happens. The customer's card has been declined, but your system still shows them as active. The longer the delivery fails and Stripe retries silently, the further your billing state drifts from reality.
What Is the Stripe invoice.payment_failed Webhook Event?
invoice.payment_failed is a Stripe webhook event emitted when a charge attempt on a subscription invoice fails. It is distinct from a one-time payment failure: this event specifically relates to recurring billing — the automatic renewal charge that Stripe attempts at the start of each billing cycle.
Stripe fires this event on each failed charge attempt, not just the first. If Smart Retry is enabled, Stripe may attempt the charge again 1–7 days later. Each attempt generates a new invoice.payment_failed event, and the attempt_count field increments with each failure.
When Does Stripe Fire invoice.payment_failed?
Stripe fires invoice.payment_failed when:
- A subscription renewal invoice is finalized and the automatic charge fails (insufficient funds, card declined, expired card, etc.)
- A Smart Retry attempt fails
- A manually triggered payment attempt on a past-due invoice fails
It does not fire when a one-time charge fails — those generate charge.failed. It also does not fire when a customer updates their payment method during a dunning flow and the next attempt succeeds — that path leads to invoice.payment_succeeded.
What Should Your invoice.payment_failed Handler Do?
A complete handler should:
- Verify the signature before processing. Reject with 401 if invalid.
- Return 200 immediately after receiving the event. Queue the actual processing work — don't do it synchronously if it might take more than a few seconds.
- Send a payment failure email to the customer using
data.object.customerto look up their email. Include a link to update their payment method. - Apply your dunning policy. Options: start a grace period (keep access, retry later), restrict access to read-only, or cancel immediately. The right choice depends on your product and plan terms.
- Update subscription status in your database. Don't rely on polling Stripe to sync state — act on the event when it arrives.
- Handle
attempt_count. You may want different behavior on the first failure (gentle email) versus the third (access restriction warning) versus the fourth (cancel and notify). - Check
next_payment_attempt. If this field isnull, Stripe has exhausted its retry schedule and will not attempt again — this failure is final until a customer action occurs.
What Is the invoice.payment_failed Payload?
The event object follows the standard Stripe event envelope with the invoice as data.object. Key fields:
{
"id": "evt_1AbCdEfGhIjKlMnO",
"type": "invoice.payment_failed",
"data": {
"object": {
"id": "in_1AbCdEfGhIjKlMnO", // Invoice ID
"customer": "cus_AbCdEfGhIjKl", // Customer ID
"subscription": "sub_AbCdEfGhIjKl", // Subscription ID — null for one-time invoices
"attempt_count": 2, // Number of charge attempts so far
"next_payment_attempt": 1748908800, // Unix timestamp of next retry; null if exhausted
"amount_due": 7900, // Amount in smallest currency unit (cents)
"currency": "usd",
"status": "open",
"hosted_invoice_url": "https://invoice.stripe.com/i/...",
"payment_intent": "pi_1AbCdEfGhIjKlMnO"
}
}
} Fields to pay close attention to:
attempt_count— how many times Stripe has attempted this invoice. Use this to escalate your dunning response as attempts increase.next_payment_attempt— Unix timestamp of the next scheduled retry. Ifnull, Stripe is done retrying. This is your signal to take final action.hosted_invoice_url— include this in your payment failure email so the customer can pay directly.subscription— use this to look up which features or plan the customer is on, so you know what to restrict or cancel.
What Happens If invoice.payment_failed Fails to Deliver?
Stripe retries failed webhook deliveries on an exponential backoff schedule: immediately, then at 5 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 8 hours, 16 hours, 24 hours, 48 hours, and 72 hours — up to 11 attempts. During this entire window, you receive no notification that the delivery is failing.
The real-world consequence: your dunning flow never ran. The customer never received a payment failure email. Their subscription is still marked as active in your database. If you rely on invoice.payment_failed to trigger access restriction, their access remains unrestricted even though the payment failed days ago.
When Stripe finally exhausts all retries, it sends you a notification email — but by then, the invoice is potentially already marked as uncollectible, the customer's next billing cycle has started, and your system state is significantly out of sync.
The fix is detecting the first delivery failure within minutes, not 72 hours. See How to Know When a Stripe Webhook Fails and the SaaS billing use case.
How Do You Test invoice.payment_failed Locally?
The Stripe CLI is the fastest path:
# Terminal 1: Start forwarding Stripe events to your local handler stripe listen --forward-to localhost:3000/webhook # Terminal 2: Trigger a test invoice.payment_failed event stripe trigger invoice.payment_failed
The CLI prints the webhook signing secret (whsec_). Use this secret — not your production endpoint secret — in your local handler to verify signatures. The triggered event is a realistic Stripe event object with populated fields, so you can test your full handler logic including signature verification, attempt_count branching, and database updates.
To test the next_payment_attempt: null path (final failure), you need to modify the event object. The easiest approach: temporarily comment out your next_payment_attempt check and unit test that branch directly, rather than trying to trigger it through the CLI.
For more detail on webhook local testing: How to Test Webhooks Locally.
How Do You Monitor invoice.payment_failed Delivery?
In the Stripe Dashboard: Developers → Webhooks → [your endpoint] → Recent Deliveries. Filter by event type to see only invoice.payment_failed deliveries. Each entry shows the response code your handler returned, the response body, and a redeliver button.
For automated monitoring: Webhook Guardian connects via read-only OAuth and polls the Stripe Events API every 5 minutes. When it detects a failed invoice.payment_failed delivery, it fires a Slack alert with the event ID, customer ID, subscription ID, attempt count, and a one-click redeliver link — so your dunning flow runs as soon as you fix the handler, without waiting for Stripe's retry schedule.
FAQ: Stripe invoice.payment_failed
What is invoice.payment_failed in Stripe?
How many times does Stripe retry invoice.payment_failed?
How do I test invoice.payment_failed?
Don't let a failed invoice.payment_failed delivery run silently for 72 hours. Connect Stripe to Webhook Guardian and get alerted within 5 minutes — with the customer ID, subscription ID, attempt count, and a one-click redeliver link. Start free for 14 days →
Also see: How to Know When a Stripe Webhook Fails · SaaS billing use case · Webhook best practices