Skip to Content
Supported subscriptionsBlocked subscription shapes

Blocked subscription shapes

Reference table. The comprehensive list of subscription shapes that block automated cancel and route to a manual cancellation request.

For per-offer eligibility (which dimensions block which retention offer), see per-offer eligibility. This page is the single answer to “why isn’t this subscription getting automated cancel?”

Shapes that route to manual cancellation request

ShapeWhat we readWhy it’s blockedStripe docs
Multi-item or mixed-interval subscriptionsubscription.items.data.length > 1 or items.has_more === truePer-item billing periods break a single-date cancellation copy. Mixed-interval items have separate final-invoice semantics. Paginated single-page subs (length === 1, has_more === true) are semantically multi-item and the cancel resolver fails closedpagination , subscriptions overview 
Schedule-attached subscriptionsubscription.schedule !== nullA direct cancel_at_period_end may be overwritten by the next phase boundary or conflict with the schedule’s end_behavior. Stripe’s recommended path is subscriptionSchedules.release first, then cancel — on the roadmapsubscription-schedules 
Cadence-attached subscriptionsubscription.cadence !== null (Stripe v2 preview)Cadence is a new Stripe v2-preview billing primitive. Cancel behavior on cadence-attached subs is undocumented. Pause docs explicitly forbid first-class pause on cadence-attached subs, so we fail closed across all five offersbilling-cadences 
Pause collection set by another toolsubscription.pause_collection !== null AND not self-ownedCancel semantics on a paused-collection sub set by another tool are outside the launch contract. Self-owned pauses use the resume pathpause-payment 
First-class paused statussubscription.status === 'paused'Stripe’s resume path may create a resumption invoice and leave the sub paused if payment is not handled. Cancel semantics on a true first-class paused sub are not part of the launch contractpause-payment 
Pending update queuedsubscription.pending_update !== nullOut-of-band pending updates (typically from payment_behavior='pending_if_incomplete') interleave unsafely with retention mutations. Per-mutation interaction is unverifiable in Stripe docssubscriptions overview 
Past-due subscriptionsubscription.status === 'past_due'Open invoices, dunning settings, and period boundaries make “scheduled for period end” ambiguous. We do not mutate dunning state automaticallysubscriptions overview 
Unpaid subscriptionsubscription.status === 'unpaid'Same dunning-state concern as past-due, plus access has typically already been revoked by the merchant’s collection settingssubscriptions overview 
Incomplete subscription (Checkout origin)subscription.status === 'incomplete' from a Checkout SessionStripe’s docs explicitly forbid subscriptions.update on an incomplete Checkout-origin sub: “you can’t update the subscription or its invoice if the session’s subscription is incompletesubscriptions overview 
Incomplete subscription (non-Checkout origin)subscription.status === 'incomplete' from a non-Checkout originPublic Stripe docs do not provide enough webhook or merchant-callback guarantees for the launch contract. Both Checkout and non-Checkout incomplete subs are blockedsubscriptions overview 
Already terminalsubscription.status === 'canceled' or 'incomplete_expired'”After it’s canceled, you can no longer update the subscription or its metadata.” No mutation is valid; widget shows the truth instead of a manual requestsubscriptions overview 
Already scheduled to cancelcancel_at_period_end === true or cancel_at !== nullAlready canceling. Widget shows the existing scheduled-cancel date and records the visit; no new mutation, no new requestsubscriptions overview 
Unrecognized future Stripe statusAny status not matched by the audited resolver lanesThe cancel resolver is fail-closed: it must not call Stripe when it cannot prove which other rule applies. Unknown future Stripe statuses, zero-item defensive shapes, and any unaudited state route to manualsubscriptions overview 

Shapes that block retention offers but allow automated cancel

These shapes route to manual for retention offers (discount, pause, plan-switch, trial extension) but allow automated cancel through the standard cancel_at_period_end path. The cancel confirmation may carry a final-billing caveat depending on the shape.

ShapeWhat we readWhy it blocks retention offersStripe docs
automatic_tax.enabled === truesubscription.automatic_tax.enabledThe widget tile shows pre-tax unit_amount; the customer would pay unit_amount + tax. This is a copy lie. Automated cancel does not quote a future amount, so it is allowedsubscriptions overview 
Multi-currency price on either side (current or target)currency_options non-empty on current.price or target.pricePer-currency unit_amount can drift independently. The customer settles in subscription.currency; if that is a currency_options entry rather than the price’s default, our default-currency comparison can be inverted in the settlement currencysubscriptions overview 
Async payment methoddefault_payment_method.type in {us_bank_account, sepa_debit, au_becs_debit, bacs_debit, acss_debit, customer_balance, upi, klarna, paypal, link}Async PMs keep status='active' after a failed payment, so active is not a reliable “paid” signal for retention copy. Klarna, PayPal, and Link have insufficiently documented renewal-failure semanticssubscriptions overview 
India recurring carddefault_payment_method.card.country === 'IN'RBI-mandated 24h pre-debit notification + Stripe’s 26h delay + AFA on each charge ≥ 15,000 INR. Retention timing assumptions do not matchsubscriptions overview 
Multi-item subscription (retention offers)items.data.length > 1Retention copy is single-item per sub. Multi-item subs route to manual on cancel intent (see above table)subscriptions overview 
Multi-seat (quantity > 1)items.data[0].quantity > 1Retention copy needs seat-aware economics. Plan-switch silently resets quantity to 1 unless explicitly preserved. Cancel may proceed because it cancels the whole itemsubscriptions overview 
Metered usageitems.data[0].price.recurring.usage_type === 'metered'Metered usage has final-usage and billing-meter semantics that break “next invoice” or “free until” copy. Cancel may proceed with a final-usage caveatsubscriptions overview 
Tiered or non-per_unit pricingitems.data[0].price.billing_scheme !== 'per_unit'Tiered pricing requires tiers[] math for “show price” UI; not in scope for retention copysubscriptions overview 
Decimal-only, custom-amount, or transformed priceAny of: unit_amount === null, custom_unit_amount !== null, transform_quantity !== null, tiers_mode !== null, type !== 'recurring', target.active === false”Strictly cheaper” is only deterministic for integer per-unit licensed prices. Decimal-only, customer-entered, transformed, and tiered prices require invoice previewsubscriptions overview 
pending_invoice_item_interval setsubscription.pending_invoice_item_interval !== nullStripe can invoice pending items on a recurring interval that is not the normal recurring price. Subscription-update prorations do not include pending invoice items. “Next invoice” copy can be falsesubscriptions overview 
Pending one-off invoice itemsinvoiceItems.list({ customer, pending: true }) returns rows for this subPending invoice items can be added to future invoices or pulled into one-off invoices. “Next invoice”, “billing paused”, and “free until” copy can be falseinvoiceitems list 
Unresolved subscription invoicesAny sub invoice with status not in {paid, void}pause_collection only voids invoices generated after the pause takes effect; existing invoices continue retrying. Discount applies to future invoices and does not rewrite already-finalized draft or open invoicespause-payment , coupons 
Existing active discount on any surfacecustomer.discount, subscription.discounts, or any subscription.items[].discounts non-emptyStacking is not supported. Customer-level, subscription-level, and item-level discount surfaces are all checked. Pause does not extend coupon validity, so a discount can expire unused during a pausecoupons 
Coupon duration misaligned with cadenceNon-monthly sub + repeating coupon where duration_in_months < billing_interval_in_monthsCoupon duration is time-based, not invoice-count-based. A 3-month repeating coupon on a 9-month-out annual renewal reduces zero invoicescoupons 
Trialing + repeating discountstatus === 'trialing' + coupon duration === 'repeating'A repeating coupon can expire before the first paid invoice if applied during a long trial. Trialing subs accept duration='once' onlycoupons 
Attached Trial Offer markerAny subscription.items[].current_trial.trial_offerStripe treats Trial Offers as a separate trial model from legacy subscription.trial_end. Unchurn extends only legacy trialssubscriptions overview 
send_invoice collection method (price-bearing offers)subscription.collection_method === 'send_invoice'Different invoice finalization, payment timing, and dunning semantics from charge_automatically. Discount, pause, plan-switch, and trial-extension are price-bearing and require automatic card collection. Cancel may proceedsubscriptions overview 
No payment method on fileBoth subscription.default_payment_method and customer.invoice_settings.default_payment_method are nullNo PM on file means renewal will fail. Retention offers are meaningless until a PM is attachedsubscriptions overview 

Plan-switch-specific blocks

In addition to the global blocks above, plan-switch enforces these checks against the target price.

BlockWhat we readWhy
Currency mismatchtarget.currency !== current.currencyStripe rejects mid-sub currency change
Cadence mismatchtarget.recurring.interval !== current.recurring.interval or interval_count differsCross-cadence switch resets billing_cycle_anchor in classic mode and may bill immediately
Tax-behavior mismatchtarget.tax_behavior !== current.tax_behaviortax_behavior is immutable per price; switching inclusiveexclusive changes the actual amount paid at the same headline price
Target inactivetarget.active === falseCannot move customers onto an inactive price
Target not strictly cheapertarget.unit_amount >= current.unit_amountPlan-switch is downgrade-only
Target not on merchant’s allowed_transitions listMissing plan_switch.allowed_transitions[current_price_id] ⊇ target_price_idProduct names and metadata are not entitlement proof. Each downgrade path must be explicitly approved by the merchant
Multi-currency on targettarget.price.currency_options non-emptyPer-currency unit_amount can drift independently — see the multi-currency row above

See also

Last updated on