Blog / Stripe

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

· 6 min read · Stripe monitoring

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:

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:

  1. Verify the signature before processing. Reject with 401 if invalid.
  2. 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.
  3. Send a payment failure email to the customer using data.object.customer to look up their email. Include a link to update their payment method.
  4. 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.
  5. Update subscription status in your database. Don't rely on polling Stripe to sync state — act on the event when it arrives.
  6. 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).
  7. Check next_payment_attempt. If this field is null, 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:

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?
An event that fires when a subscription renewal charge attempt fails. Stripe tried to collect payment for a subscription invoice, the charge was declined or otherwise failed, and Stripe sends this event to your webhook endpoint so you can run dunning logic: notify the customer, restrict access, and update billing state in your database.
How many times does Stripe retry invoice.payment_failed?
Stripe retries the charge itself up to 4 times depending on Smart Retry settings, with each attempt generating a new invoice.payment_failed event. The attempt_count field increments with each failure. When next_payment_attempt is null, Stripe has exhausted its retry schedule.
How do I test invoice.payment_failed?
Stripe CLI: run "stripe listen --forward-to localhost:3000/webhook" in one terminal, then "stripe trigger invoice.payment_failed" in another. The CLI sends a test event with a valid signature to your local handler so you can test your full dunning logic end-to-end.

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