Stripe Can Bundle a Cancellation Into a Pending Update Now. If the Invoice Fails, So Does the Cancellation.
Stripe's Sept 30 release lets a scheduled cancellation ride inside a pending update — and vanish with it if the triggering invoice never gets paid.
A customer on your $299 plan asks to drop to $99 and leave at the end of the current period — a common ask, and until the September 30 Endive release, an awkward one to model in a single Stripe call. The price change could be gated on payment through payment_behavior=pending_if_incomplete. The cancellation couldn't. cancel_at_period_end applied the instant you set it, invoice or no invoice. Bundle both into one request and you got two changes running on two different clocks: a scheduled cancellation that took effect immediately, sitting next to a plan change that might never happen if the card behind it declined.
Stripe closed that gap in the same release that reached general availability for Trial Offers. It's a one-line changelog entry — "Adds support for scheduling Subscription cancellation as part of pending update" — sitting next to a dozen other Billing changes from the same day. It's also the kind of change that only matters to you if your cancellation flow ever lets someone downgrade and schedule an exit in the same motion, which, for a lot of save-flow designs, is exactly what a "switch to a cheaper plan instead of leaving" offer does.
What a pending update actually gates
Updating a subscription with payment_behavior=pending_if_incomplete doesn't apply changes and hope for the best. It generates an invoice when the update requires one — a proration invoiced with proration_behavior=always_invoice, the end of a trial, a reset billing-cycle anchor — and holds every requested change in a pending_update hash until that specific invoice is paid. Fail the payment, and the subscription keeps its current values exactly as they were before the call. The hash sits there with an expires_at timestamp, visible on the subscription object, until you fix the payment method and pay the invoice, submit a new update that replaces it, or the window runs out and Stripe voids the invoice on your behalf.
Not every update qualifies for this treatment. Stripe's own list of what doesn't trigger a pending update includes configuration changes, billing threshold adjustments, add_invoice_items, and — until this release — setting cancel_at_period_end to true. Those apply the moment you call the API, no invoice, no gate, no waiting on anything. That's correct behavior when a cancellation is the only thing you're changing. It stopped being correct the moment a cancellation request rode along with a plan change that genuinely needed to wait on payment.
The new piece: a cancellation that waits on the same invoice
Stripe's supported-attributes list for pending updates now explicitly includes cancel_at_period_end alongside items, trial_end, billing_cycle_anchor, discounts, and coupon. Practically, that means a single update subscription call with payment_behavior=pending_if_incomplete, a price or quantity change, and cancel_at_period_end=true stores all of it in one pending_update hash. Nothing applies — not the new price, not the scheduled cancellation — until the resulting invoice is paid.
| Scenario | Before Sept 30, 2026 | Now |
|---|---|---|
| Customer downgrades and schedules a cancel-at-period-end in one request, invoice pays | Cancellation takes effect immediately; plan change applies separately once (if) the invoice clears | Both apply together, the moment the single invoice is paid |
| Same request, invoice fails | Customer is already scheduled to cancel even though the new price never took effect — two inconsistent states | Neither change applies; subscription stays exactly as it was pre-request |
| Pending update expires unresolved after 23 hours | Not applicable — cancellation had already taken effect regardless of the invoice | Stripe voids the invoice and discards the whole hash, cancellation included; the customer is not scheduled to leave |
| Your webhook handler checks for a scheduled cancellation | Read cancel_at_period_end directly off the subscription at the moment of the call | Must also check pending_update for a cancellation riding inside it — the top-level field won't reflect it until the invoice resolves |
Row two is the one worth sitting with. Before this release, a downgrade-and-leave request could put a customer into a state nobody designed on purpose: scheduled to cancel, but still being billed at the old price because the plan change itself never cleared payment. Your cancellation confirmation email would say one thing; your billing records would say another. That's not a hypothetical edge case for any product that offers "switch to our cheaper plan and we'll still let you go at the end of the term" as a retention offer — it's the exact shape of that offer, built on exactly the API call that used to split in two.
Why the 23-hour window gets more consequential with a cancellation inside it
A pending update's default expiration is tight by design — 23 hours from the request, or sooner if it lines up with a trial end or the earliest item's period end within that window. That's a reasonable default when the only thing at stake is whether a price change goes through. It's a different kind of deadline once a scheduled cancellation is riding inside the same hash, because a customer who doesn't fix a failed card in time doesn't just keep their old price — they also silently lose whatever cancellation date your UI told them was set.
The mechanics matter here because off-session charges tied to a plan change are a documented weak point for first-attempt authorization, not just an unlucky coincidence. Recurring charges that don't match a card's usual billing pattern — a different amount, triggered outside the normal renewal date — get flagged by issuing banks more often than a routine renewal does.
Source: RecurFlux, SaaS Payment Failure Report 2026, aggregating Recurly Subscription Economy Index, Baremetrics, and ProfitWell data
Even at Stripe's own, comparatively low end of that range, a meaningful share of these bundled requests are going to hit a decline on the very first attempt — and unlike a routine renewal invoice, a pending update's proration invoice doesn't automatically get the benefit of Stripe's longer Smart Retries window stretched across weeks the way we've covered in our Stripe dunning guide. It has 23 hours by default. If your cancellation flow routes a customer into this path and then doesn't surface "your plan change and cancellation are still pending — update your payment method to confirm it" somewhere they'll actually see it, you're relying on a very short clock to carry a message your UI already implied was final.
The conflict rule that can quietly wipe a scheduled cancellation
There's a second mechanic worth building around deliberately. Stripe's pending-updates documentation is explicit that a later subscription update conflicts with — and discards — an existing pending update if it includes discounts, the legacy coupon or promotion_code parameter, cancel_at_period_end, or cancel_at. Resubmitting the same discount code counts as a new discount for this purpose, even though it's identical to the one already pending.
That's a direct hit on a common cancellation-flow pattern: offer a downgrade first, and if the customer still wants to leave, follow up with a one-time discount to try to save them anyway. If the downgrade-plus-scheduled-cancellation request already created a pending update, and your flow's second step fires a separate API call that includes a coupon or promotion code to sweeten the deal, that second call doesn't add to the first pending update — it replaces it, voiding the original invoice and wiping the scheduled cancellation along with it. The customer who accepted your "take 20% off and stay" offer after already being told they were set to leave at period end may now find neither request fully applied, waiting on whichever invoice came last.
The fix isn't complicated once you know the rule exists: treat a subscription with a populated pending_update hash as a hold state for your entire flow, not just for the specific change that created it. Check for it before firing a second update, and if one exists, resolve or explicitly replace it in the same call rather than layering a second, unrelated change on top and hoping Stripe reconciles them for you. It won't — it'll pick the most recent request and discard whatever came before.
What to actually check this week
- Read pending_update, not just cancel_at_period_end, when deciding what a subscription is scheduled to do. A top-level field that still says
falsecan be hiding a cancellation that's bundled and waiting on an invoice — your churn dashboard and your support tooling both need to look one level deeper now. - Listen for customer.subscription.pending_update_applied and customer.subscription.pending_update_expired separately, not just customer.subscription.updated. The first tells you a bundled plan change and cancellation both actually went through; the second tells you they didn't, and the customer's account is exactly as it was before they asked to leave.
- Tell the customer their exit is conditional, not confirmed, the moment you bundle a cancellation with a billed change. "You're set to move to Starter and cancel on March 4" is true only once the plan-change invoice clears — say so, with a real deadline, not a vague "we'll email you if there's an issue."
- Audit any multi-step offer sequence in your cancellation flow for stacked API calls against the same subscription. If step one bundles a downgrade with a scheduled cancellation and step two can fire a discount on its own, you have the exact conflict pattern described above, and it's worth testing deliberately rather than finding it from a confused support ticket.
None of this is an argument against bundling a cancellation into a pending update — it's a better primitive than what existed before, because it finally makes a downgrade-and-leave request atomic instead of two changes on two different clocks. It does mean the state a subscription can sit in for up to a day just got more interesting, and anything reading cancel_at_period_end as the whole truth about whether someone's leaving needs an update of its own. If you're already tracking how much downgrade activity happens outside your main cancel page, this is one more reason that data needs to include the pending, not-yet-applied kind — a request that never clears isn't a downgrade and isn't a cancellation, it's a subscription that's about to quietly stay exactly where it started. We've covered the same underlying theme from the other direction in our piece on Stripe gating a downgrade credit on invoice payment — Stripe keeps finding places in Billing where something used to happen unconditionally and now correctly waits its turn.
A cancellation flow like CancelFlow is exactly the kind of place this shows up first, since "offer a cheaper plan instead of losing them entirely" is one of the most common save offers there is. If your flow builds that offer as a single bundled request, you get the atomicity for free now — but only if the confirmation screen you show the customer, and the webhook logic behind your churn numbers, both account for the fact that "pending" and "done" are no longer the same thing for a day. Our churn calculator is a quick way to check how much a silently-expired pending update — a customer who thinks they downgraded and scheduled an exit, and actually did neither — would cost you if it's happening more than once in a while.
Frequently asked questions
What is a "pending update" on a Stripe subscription?+
A pending update is how Stripe gates a subscription change on successful payment. When you update a subscription with payment_behavior=pending_if_incomplete and the change generates an invoice — a price change invoiced with proration_behavior=always_invoice, the first charge after a trial, a billing-cycle-anchor reset — none of the requested changes apply immediately. Stripe stores them in a pending_update hash and only applies them once that invoice is paid. If payment fails, the subscription keeps its original values and the hash stays until you resolve the invoice or it expires.
Can you schedule a cancellation and a plan change in the same Stripe API call?+
Yes, as of the September 30, 2026 Endive API release. cancel_at_period_end is now one of the attributes Stripe's pending-updates system explicitly supports. Call update subscription with payment_behavior=pending_if_incomplete, a price or quantity change, and cancel_at_period_end=true in the same request, and the cancellation gets stored inside the same pending_update hash as the plan change, not applied on its own. Before this release there was no defined way to make a cancellation wait on the same invoice as a bundled plan change.
What happens to a bundled cancellation if the pending update's invoice never gets paid?+
It's discarded along with everything else in the pending_update hash. Stripe automatically voids the invoice and drops the pending update once it expires — by default 23 hours after the request, or sooner if that lines up with a trial end or period end inside that window. A customer told 'you're moving to Starter and you're set to cancel at period end' may end up neither switched nor scheduled to cancel, if the invoice behind that request was never collected.
Does this change how cancel_at_period_end works on its own?+
No. Setting cancel_at_period_end=true by itself, with nothing else in the request that triggers an invoice, still applies immediately — Stripe's docs list it explicitly as one of the updates that doesn't generate an invoice or create a pending update on its own. The bundling behavior only activates when you combine it with something that does trigger one, like a price change invoiced with proration_behavior=always_invoice.
Stop losing subscribers today
One script tag. One function call. A live cancellation flow in under 10 minutes.
Start free trial →