ProductAugust 22, 20266 min read

invoice.payment_succeeded: The Stripe Webhook That Closes Your Recovery Loop

invoice.payment_succeeded fires on every successful charge, including recovered ones. Here's why your dunning system is broken without it.

Diagram of the recovery loop closing: payment_failed starts the sequence, the customer updates their card, payment_succeeded fires, and the sequence stops itself
Without a stop condition, your dunning sequence chases money that's already in your account.

What invoice.payment_succeeded tells you

invoice.payment_succeeded fires every time Stripe successfully collects on an invoice: the first subscription payment, every renewal, and, critically, every successful retry after a failure. It's the all-clear signal for your revenue pipeline. Everyone writes about invoice.payment_failed. Its quieter sibling gets ignored, and that's a bug in most homegrown billing setups, because payment_succeeded is the event that tells your system to stop recovering.

The recovery loop doesn't close itself

Picture the common setup. A renewal fails, your webhook handler catches invoice.payment_failed, and your dunning sequence starts: email day 1, email day 4, retry day 7. On day 5, the customer updates their card. Stripe retries, the charge goes through. If nothing in your system listens for invoice.payment_succeeded, day 7's email still goes out. Congratulations: you just sent a your payment failed email to someone who paid two days ago. That's not a recovery email. That's an unsubscribe reason.

Every dunning sequence needs a kill switch, and this webhook is it. The moment it fires for a customer in an active recovery flow, that flow stops. No exceptions, no end-of-sequence cleanup. Immediately.

Reading the payload

The payload is the invoice object. Three fields do most of the work:

  • billing_reason. subscription_create means first payment, subscription_cycle means a normal renewal, and subscription_update or manual mean something off the happy path. This is how you keep welcome logic away from renewal logic.
  • subscription, which links the invoice back to the subscription so you can flip the right account back to active.
  • amount_paid and hosted_invoice_url, useful for your own receipt or confirmation email if you send one.

The field most people miss is billing_reason. Without it, you can't tell a $0 trial invoice from a $500 annual renewal. Both fire this event. Treat them the same and your metrics, and possibly your access logic, get weird fast.

First payment vs renewal vs recovery

Three very different business moments arrive through the same event. A first payment means a new customer, so your onboarding flow should kick off. A routine renewal means business as usual, so update state and move on. A successful retry after a failure means a recovery, so stop the dunning sequence and consider a personal touch. billing_reason separates the first two. The third one, the recovery, you detect yourself: was there a recent invoice.payment_failed for this subscription? If yes, this payment_succeeded is a save, and it's worth logging as such. Recovery rate is one of the most important dunning metrics you have, and this is where it's born.

What your app should do when it fires

  • Stop any dunning sequence for that customer. This is the big one, covered above.
  • Restore or confirm access. If the account was limited while past due, this is the trigger to open it back up.
  • Update your own records. MRR, last successful payment date, account status. Stripe is the source of truth for money, but your app needs its own copy of state.
  • Optionally, send your own confirmation. Stripe can send receipts, but a short personal note after a recovered payment, thanks for sorting that out, does more for retention than any receipt.

Edge cases that bite

A few worth knowing before you ship the handler. Zero-dollar invoices fire this event too, so guard your thank-you logic with an amount check. Out-of-band payments, where the customer pays by bank transfer and you mark the invoice paid manually, also fire it. And events can arrive out of order: payment_succeeded can land before the payment_failed from the same invoice in edge cases, so write handlers that set state idempotently instead of assuming sequence. That last point deserves its own deep dive, and we have one: the webhook retry and idempotency playbook covers duplicates and ordering in detail.

Handling duplicates and ordering

Stripe retries webhook delivery until your endpoint acknowledges, which means the same event can arrive more than once. Write the handler so a duplicate is a no-op: if the invoice is already marked paid in your system, acknowledge and exit. Five lines of guard code, hours of debugging saved. Ordering is the subtler issue. Events about the same invoice don't guarantee arrival order, so never write logic that assumes payment_failed always precedes payment_succeeded. Set state based on what the event says, not on what you think must have happened before it.

Testing the handler

With the Stripe CLI: stripe listen --forward-to your endpoint in one terminal, stripe trigger invoice.payment_succeeded in another. Then do it properly in test mode with a real subscription: let a renewal succeed, let one fail and then succeed on retry, and confirm your dunning sequence stops in that second case. That specific test, failure followed by success, is the one that validates the whole loop.

The boring event that pays for itself

invoice.payment_succeeded isn't glamorous. It announces money you expected to collect. But wire it up properly and it quietly does three jobs: closes your recovery loops, keeps your access logic honest, and gives you the moment to turn a recovered payment into a stronger customer relationship. Skip it and your dunning system has no brakes.

"invoice.payment_failed tells you a payment broke. invoice.payment_succeeded tells you whether you fixed it. You need both to run a recovery loop that actually closes."

The metrics hiding in this event

Beyond account state, this event is your best raw material for understanding the health of your billing. Count how many payment_succeeded events follow a failure within the same invoice cycle and you have your true recovery rate, not the vanity version. Track the time between failure and success and you know whether your retry timing and email cadence are working or just making noise.

There's a subtler one too: customers who recover after a failure have a slightly elevated churn risk over the next few months, because the payment problem was sometimes a symptom of waning intent. Tagging recovery events lets you watch that cohort separately instead of finding out at renewal.

One last operational note: this event is also your reconciliation trail. When a customer disputes what they were charged and when, the payment_succeeded events with their billing_reason and amount_paid fields are the cleanest record you have. Timestamped, unambiguous, and exportable. Future you, mid-dispute, will be glad these live somewhere queryable instead of only in a Stripe dashboard filter. It takes ten minutes to set up and pays for itself the first time a dispute lands.

Common mistakes

First mistake: doing nothing with the event at all. Plenty of apps only listen for payment_failed and let Stripe's own state carry the good news. That works right up until your dunning sequence emails someone who already paid, which is the single most embarrassing billing email you can send.

Second: treating every success identically. A $0 trial invoice and a $500 annual renewal both fire this event, and if your welcome flow triggers on both, new trial users get onboarding emails meant for paying customers. Check billing_reason before you celebrate.

Third: no stop condition in the recovery sequence. Your dunning automation should subscribe to this event and halt the moment it fires for an invoice it was chasing. If the sequence only checks failure events, it will happily keep chasing money that's already in your account.

FAQ

When does invoice.payment_succeeded fire?

Every time an invoice is successfully paid: first subscription payment, every renewal, and every successful retry after a previous failure.

Does invoice.payment_succeeded fire on the first payment?

Yes. It fires for the initial subscription payment and for every renewal after that. Check the billing_reason field to tell them apart: subscription_create versus subscription_cycle.

Do I need invoice.payment_succeeded if Stripe already sends receipts?

Stripe's receipts are customer-facing emails. The webhook is for your system: restoring access, updating your database, and stopping any dunning sequence for that customer.

Why does my dunning tool need to listen for invoice.payment_succeeded?

Because a recovered payment is how a failed payment ends. Without this event, your recovery emails keep sending to people who already paid.

R

Robert

Founder at StayPaid

Want to recover failed payments like a founder?

Start Free — First 3 recoveries