stripeinvoicingrevenue leakageinvoluntary churn

Stripe Can Now Hold a Downgrade Credit Until the Invoice It Credits Is Actually Paid

Stripe's new invoicing_rules field stops a proration credit from posting before the invoice it credits gets collected. Here's the leak it closes.

XY
18 September 2026 · 7 min read

A customer on your $299 plan downgrades to $99 on day 18 of a 30-day cycle. Stripe does what it always does: it prorates. A credit line item appears for the unused portion of the $299 plan, netted against a new charge for the $99 plan going forward. Ordinarily that's a non-event — the math nets out, the next invoice reflects it, nobody notices. It stops being a non-event the moment the invoice that credit is supposed to offset never actually got paid. A failed card. A dispute still open. An invoice sitting three retries deep in your dunning cycle. Until a Stripe API update that shipped in late August, nothing checked for that condition. The credit posted anyway, on schedule, completely disconnected from whether you'd ever collected the money it was crediting against.

Key stat
31.8%
Of annual B2B revenue that can leak through gaps between quoting, billing, and collections — exactly the kind of gap a credit posted before its invoice is paid falls into
Source: Zilliant, quote-to-cash revenue leakage research

This is a narrow, technical fix, and it's worth understanding exactly what it changes, because most teams have no idea this failure mode exists in their own billing until they go looking for it.

Why a credit and a collection can end up on different clocks

Stripe's proration math and Stripe's dunning retries are two separate systems that happen to touch the same invoice. When a subscription changes mid-cycle, Stripe generates a credit invoice item for the unused time on the old price and a charge invoice item for the new price, both attached to the customer's next invoice. That generation happens the moment the plan changes — it doesn't ask whether the invoice being credited against was ever paid. Dunning, meanwhile, runs on its own retry schedule against whatever invoices are sitting in open status, completely unaware that a downgrade just created a credit that assumes the very invoice it's retrying will eventually get collected.

Run those two processes far enough apart and you get a specific, expensive coincidence: a subscriber whose card started failing goes to your billing page and downgrades before the failed invoice ever recovers. The downgrade generates its credit immediately. The original invoice keeps retrying, and if it ultimately goes uncollectible, you're not just out the original charge — you've also issued a credit against it, which nets to a bigger loss than the failed payment alone would have caused. Nobody approved that outcome. It fell out of two systems that didn't check in with each other.

ScenarioBefore this updateNow
A downgrade credit is generated while the prior invoice is still mid-retryCredit posts on schedule, regardless of whether the prior invoice ever gets paidinvoicing_rules holds the credit until the referenced invoice is paid
A script edits the quantity or price on a proration line item after the factUpdate succeeds silently, even though the change can desync the invoice totalfrozen_fields blocks the update and returns a 400 error
Support tries to hand-correct a miscalculated creditDirect PATCH to the invoice item, no signal it was a derived valueItem must be deleted and recreated — the API forces the safer path
Finance wants to know which invoice items are safe to edit programmaticallyNo way to check ahead of time — you find out when the update fails or silently corrupts datafrozen_fields is inspectable on the object before you attempt anything

Two locks, shipped in the same release

Stripe added both changes to the Invoice Item object in its August 26, 2026 API update (the "Dahlia" release). They solve related but distinct problems, and it's worth treating them separately.

frozen_fields: locking the numbers that make proration math hold together

frozen_fields is a read-only array that lists which properties on an invoice item can't be modified — the possible values are discounts, pricing, and quantity. Attempting to update a field that shows up in that array returns a straight 400 error instead of silently applying the change. Stripe's documentation is specific about scope here: only invoice items with derived values get anything in frozen_fields at all, which in practice means prorations — the credit and charge items Stripe computes automatically when a plan changes mid-cycle. A manually created line item with a price you typed in yourself is untouched by this.

The reason this matters: a proration's price and quantity aren't arbitrary numbers a human picked. They're the output of a calculation involving the old plan, the new plan, the exact second the change happened, and the length of that billing cycle. Editing one field after the fact — bumping the quantity to "fix" what looks like a rounding issue, say — doesn't correct the math, it breaks it, because the other fields on that item were computed assuming the original value. Before frozen_fields existed, Stripe would let that edit through. Now it won't, and the correct move if a proration genuinely needs to change is to delete the item and recreate it with the right numbers from scratch, which keeps the whole item internally consistent instead of half-updated.

invoicing_rules: making a credit prove the invoice it offsets got paid

The second change is a new invoicing_rules array, and the piece that matters for churn and dunning is the defer_until_credited_items_resolved rule type. It attaches to a credit proration item and blocks that item from being invoiced until the item it's crediting is resolved. The resolution condition depends on what's being credited: if it's an invoice line item, the invoice that line item belongs to has to be marked paid. If it's a plain invoice item, that item needs to be invoiced alongside the credit itself, or already sitting on an invoice that's been paid. Either path closes the same gap — a credit can no longer post while the invoice it's supposed to offset is still open, still disputed, or still working through retries.

Where quote-to-cash gaps eat SaaS revenue
Revenue leaking through quote-to-cash gaps31.8%
EBITDA lost annually to revenue leakage, high estimate5%

Source: Zilliant (quote-to-cash gaps); MGI Research (EBITDA lost to revenue leakage, 1–5% range)

Neither of those figures is specific to Stripe or to this one failure mode — nobody's published a number for "revenue lost specifically to credits that outran collection," because until now there was no field to even flag that it had happened. That's part of the point. This kind of leak doesn't show up as a line item on a P&L; it shows up as a slightly worse bad-debt write-off rate that nobody traces back to a timing bug in how credits and collections interact. The category it falls into — money lost in the gap between billing and getting paid — is large enough, and common enough, that Stripe building a native guardrail against one specific instance of it is worth taking seriously even without a bespoke stat to hang on it.

What to actually change in your integration

  • Check frozen_fields before any programmatic update to an invoice item. If a billing-ops tool, a support macro, or an internal script ever patches invoice items directly, add a check for whether the field you're about to touch is in that array, and route to a delete-and-recreate call instead of letting the PATCH fail at runtime.
  • Don't assume Stripe's own proration credits need extra logic — they don't anymore. If you were previously writing custom reconciliation to catch credits issued against unpaid invoices, the native invoicing_rules behavior now handles the case where Stripe generated the credit automatically. Your reconciliation effort is better spent elsewhere.
  • Apply the same principle to credits you generate yourself. If your billing system creates its own credit or refund-to-credit conversions outside Stripe's automatic proration — a manual goodwill credit, a support-issued adjustment — nothing forces those to wait on payment confirmation. Gate them the same way: don't let a credit post against an invoice that hasn't cleared.
  • Route downgrade-triggered credits through the same visibility as your involuntary churn tracking. A subscriber downgrading while already mid-dunning is a specific, identifiable segment — worth flagging separately from a routine downgrade, since the underlying invoice risk hasn't resolved just because the plan changed.

The connection to downgrade churn is direct: any credit generated by a self-serve plan change is a downgrade event, and if your plan-change page doesn't distinguish between a healthy account downgrading and one already sitting on a failed payment, you can't tell which credits carry this risk in the first place. It also sits next to what we covered on invoice revisions — both are Stripe closing gaps in the same general area, editing a finalized invoice safely versus crediting one safely, and both exist because billing mistakes at the invoice-item level are common enough to need first-class API support, not a workaround. If you're trying to quantify how much a leak like this is worth chasing down against your actual books, the dollar retention rate calculator is a fast way to see what even a small, consistent credit-before-collection gap costs against your gross revenue retention over a year.

None of this is a reason to rebuild your cancellation or downgrade flow. It's a reason to check whether anything in your stack was quietly assuming a credit and a collection would always land in the right order. A cancellation flow like CancelFlow decides what a subscriber sees on the way out — pause, discount, downgrade — but every one of those offers can generate a credit behind the scenes, and until now, nothing on Stripe's side made sure that credit waited its turn. It does now. That's one less place for a retention offer to quietly cost more than it was supposed to.

Frequently asked questions

What is Stripe's frozen_fields property on an invoice item?+

frozen_fields is a read-only array on the Invoice Item object that lists which properties can no longer be edited — the possible values are discounts, pricing, and quantity. It only appears on invoice items with derived values, which in practice means prorations. Trying to update a field listed in frozen_fields returns a 400 error; to change one of those values you delete the invoice item and recreate it with the correct numbers.

What does invoicing_rules with defer_until_credited_items_resolved actually do?+

It's a rule type on a credit proration item that blocks that item from being invoiced until the thing it's crediting is resolved. If the credited item is an invoice line item, the invoice that line item belongs to must be paid before the credit can go out. If it's a plain invoice item, that item has to be invoiced alongside the credit or already sit on a paid invoice. Either way, the credit can't outrun collection anymore.

Does this apply to every invoice item, or only prorations?+

Only invoice items with derived values get anything in frozen_fields — Stripe's documentation is explicit that this means prorations, not manually created line items where you typed in your own price and quantity. If you're building invoices by hand with fixed amounts, this change doesn't touch your integration at all. It's specifically aimed at the credit and charge items Stripe generates automatically when a subscription changes mid-cycle.

What should I check in my integration after this shipped?+

Two things. First, if any code path in your billing ops tooling patches invoice items directly — a support script that adjusts a proration after the fact — check frozen_fields before sending the update, and build the delete-and-recreate path instead of assuming the PATCH will succeed. Second, if you generate your own credits outside of Stripe's automatic proration (a manual refund-to-credit conversion, for instance), look at whether that credit can currently post before the invoice it offsets is paid, and consider gating it the same way Stripe just did natively.

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