stripedunninginvoluntary churninvoiceschurn reporting

Stripe Can Now Tell You Why an Invoice Is Uncollectible. Most Churn Dashboards Still Can't Use That.

Stripe's new status_details.uncollectible.reason field splits one churn bucket into five. Most dunning reports haven't caught up to the distinction.

XY
1 October 2026 · 8 min read

Every Stripe-billed SaaS business has the same blind spot in its churn reporting: an invoice with status: "uncollectible" tells you a customer stopped paying, but not why. Did their card get declined four times in a row? Did someone on your support team manually write off a $40 invoice from a customer who complained? Did the subscription get paused on purpose and Stripe correctly decided not to bill it? Until last week, all three looked identical in the API. A changelog entry from September 30 quietly fixes that, and it's worth more attention than it's gotten.

Key stat
5
Distinct reasons an invoice can now report for going uncollectible — previously reported as one undifferentiated status
Source: Stripe API changelog, status_details.uncollectible.reason, Sept 30 2026

What actually shipped

Stripe added a status_details property to the Invoice object. It's only populated when status is uncollectible, and it carries one meaningful field: status_details.uncollectible.reason. That's an open enum — Stripe can add values later without it being a breaking change — currently covering five cases:

  • max_payment_attempts — Smart Retries ran out of attempts without a successful charge. This is the classic failed-payment case.
  • payment_not_received — relevant to send_invoice billing, where you're waiting on a bank transfer or manual payment that never arrived.
  • subscription_canceled — the subscription behind the invoice was canceled before the invoice could be collected.
  • subscription_paused — the subscription had pause_collection set, so Stripe generated the invoice but deliberately didn't attempt to collect on it.
  • user_forgiven — someone on your team manually marked the invoice uncollectible, usually through the dashboard.

It's a small-looking addition — one nested string on an object that already had a dozen fields. But it's also the first time Stripe has given you a reliable, first-party way to separate these five situations without stitching together webhooks, dashboard notes, and your own pause_collection state to reconstruct the cause after the fact.

Why "uncollectible" has always been a lie by omission

If you've built involuntary churn reporting on Stripe before, you've probably written something like this: pull every invoice where status = "uncollectible", treat it as a failed payment, and roll it into your churn number. That query has always been wrong for a meaningful slice of the data, and until now there was no clean way to fix it from the API alone.

Consider what actually lands in that bucket today. A customer whose card got declined four times belongs there — that's a real involuntary churn event, and it's what our Stripe dunning guide is built around recovering. But so does an invoice generated behind a subscription you deliberately paused at a customer's request — Stripe still creates the invoice under pause_collection's older mark_uncollectible behavior, and that invoice reports the exact same status as the declined card. So does an invoice a support rep manually forgave because a customer complained about being double-billed. None of those last two are failed payments. All three used to be indistinguishable without reconstructing context Stripe didn't expose.

ReasonWhat actually happenedCounts as involuntary churn?
max_payment_attemptsCard declined through every Smart Retry attemptYes — this is the real signal
payment_not_receivedInvoice billing, payment never arrivedYes, but needs different remediation than card retries
subscription_canceledSubscription was already canceled before collectionNo — it's a symptom of a cancellation that already happened
subscription_pausedSubscription deliberately paused, invoice correctly not collectedNo — this is expected behavior, not a failure
user_forgivenSomeone on your team wrote off the invoice manuallyNo — this is a support decision, not churn

Two of five reasons are genuine payment failures. Three of five are not churn at all — they're the downstream paper trail of decisions you or the customer already made deliberately. If your dunning dashboard has been counting all five the same way, your involuntary churn rate has been overstated by however much pause and manual-forgiveness volume runs through your account. For a business running a healthy pause offer at scale, that's not a rounding error.

The subscription_paused case is the one worth fixing first

We wrote recently about Stripe's new dedicated pause status for subscriptions, which stops generating invoices entirely while a subscription is paused. If you've migrated to that endpoint, this problem mostly goes away for new pauses — no invoice, nothing to mark uncollectible. But pause_collection hasn't been deprecated, and plenty of cancellation flows — including setups built directly off our own dunning guide — still use it with the mark_uncollectible behavior, which generates the invoice and then marks it uncollectible rather than voiding it outright.

Every one of those invoices was, until this changelog entry, invisible as a distinct category. Now status_details.uncollectible.reason = "subscription_paused" gives you a one-line filter to exclude them from your churn calculation without needing to cross-reference each invoice's subscription against your own pause records.

Where misattributed "uncollectible" volume typically comes from
Genuine failed payments (max_payment_attempts)58%
Paused subscriptions still on pause_collection24%
Canceled before collection11%
Manually forgiven by support7%

Illustrative split based on the five status_details.uncollectible.reason values Stripe now reports; your own account's breakdown will depend on pause and manual-write-off volume.

How to actually use the new field

1. Upgrade your API version deliberately

status_details only appears starting with API version 2026-09-30.endive. Check your current version in Workbench before assuming it's already showing up in your webhook payloads or API responses — if you're pinned to an earlier version, you'll keep getting the old, undifferentiated uncollectible status until you upgrade. Stripe gives you a 72-hour rollback window after upgrading, so there's little reason to delay this one if your webhook consumers are reasonably well-tested.

2. Rebuild your involuntary churn query around the reason field, not the status alone

If your churn dashboard currently does something like WHERE invoice.status = 'uncollectible', change it to filter on status_details.uncollectible.reason IN ('max_payment_attempts', 'payment_not_received'). That single change removes paused and forgiven invoices from your involuntary churn numerator without touching anything else in the calculation. It's the invoice-level equivalent of what we've described before at the subscription level — using cancellation_details.reason instead of a bare canceled status to separate payment-driven cancellations from voluntary ones.

3. Route user_forgiven differently in your support tooling

A user_forgiven invoice is a decision your team made, not a payment outcome. If that volume is showing up anywhere in your churn or recovery reporting, it shouldn't be — it belongs in a support or billing-exceptions report instead, ideally with the agent who forgave it and the reason tagged separately in your own system, since Stripe's field only tells you that it happened, not why.

4. Watch payment_not_received as its own signal if you bill on invoice terms

Businesses running send_invoice billing — net-30 contracts, manual bank transfers — have a fundamentally different recovery motion than card-based Smart Retries. A payment_not_received invoice needs a collections follow-up, not a retry schedule. Lumping it in with max_payment_attempts volume means you're measuring two different operational processes with one number, which makes it hard to tell whether your card dunning or your invoice collections process is the one that needs attention.

MetricOld query (status only)New query (status_details.reason)
Involuntary churn rateIncludes paused and forgiven invoices as failuresIsolates max_payment_attempts + payment_not_received only
Recovery rate denominatorInflated by invoices that were never meant to collectReflects only invoices that genuinely attempted and failed
Collections vs. dunning splitNot possible without manual taggingNative split via payment_not_received vs. max_payment_attempts
Support write-off visibilityBuried inside the churn numberIsolated via user_forgiven, auditable separately

What this doesn't fix

The new field tells you why an invoice became uncollectible, not why the underlying payment method failed in the first place. For that level of detail — insufficient funds versus a bank block versus a stolen card — you still need the decline code from the payment attempt itself, which is a separate problem we cover in our guide to decline codes and retry limits. Think of status_details.uncollectible.reason as the top-level sort: it tells you which invoices are real payment failures worth investigating at the decline-code level, and which ones you can stop investigating entirely because they were never failures to begin with.

It also doesn't retroactively tag invoices created before the field existed. If you're rebuilding historical churn numbers, older uncollectible invoices won't have status_details populated — you'll need your existing reconstruction logic (cross-referencing pause records, dashboard audit logs) for anything before late September 2026, and can switch to the native field for everything after.

None of this changes how many customers actually stop paying — it changes whether you can tell the difference between a customer you lost and a customer you chose not to bill. If your cancellation flow already routes customers through pause and discount offers before they fully leave, getting this distinction right matters more over time, not less: the better your pause offer performs, the more subscription_paused invoices accumulate in your Stripe account, and the more your old involuntary churn number was quietly wrong. Running the numbers on what a cleaner split between real failures and deliberate pauses does to your churn rate is a quick exercise with our churn calculator — plug in both the old and new denominators and see how much of your "involuntary churn" was never churn at all.

Frequently asked questions

What is status_details.uncollectible.reason in Stripe?+

It's a new property Stripe added to the Invoice object on September 30, 2026. When an invoice's status is uncollectible, status_details.uncollectible.reason tells you why: max_payment_attempts, payment_not_received, subscription_canceled, subscription_paused, or user_forgiven. Before this field existed, every uncollectible invoice looked identical in the API — you had to infer the cause from surrounding events.

Does this replace Stripe's status_transitions field?+

No. status_transitions records timestamps — when an invoice moved into each status. status_details.uncollectible.reason records cause, not timing. They're separate properties and you typically want both: status_transitions tells you when an invoice went bad, status_details tells you why.

Should a subscription_paused invoice count as involuntary churn?+

No, and that's the main reason this field matters. An invoice marked uncollectible because pause_collection was set behind it isn't a failed payment — it's an invoice Stripe correctly chose not to collect on a subscription you deliberately paused. Most dunning reports built before this field existed can't separate that case from a genuine card decline, which quietly inflates the involuntary churn number.

Do I need to upgrade my Stripe API version to use this field?+

Yes. status_details is scoped to API version 2026-09-30.endive and later. You can view and upgrade your API version in Workbench, and Stripe gives you a 72-hour rollback window after upgrading if something in your integration breaks.

Try CancelFlow

Stop losing subscribers today

One script tag. One function call. A live cancellation flow in under 10 minutes.

Start free trial →
← All postsHome