Stripe Made billing_cycle_anchor an Object on Sept 30. Upgrade for Pause or Trial Offers and Your Old Strings Start Erroring.
Stripe's Sept 30 release turned billing_cycle_anchor into an object on 3 endpoints. Upgrade for pause/resume or Trial Offers and old strings now error.
Buried in Stripe's September 30 Endive release, alongside the headline features — Trial Offers reaching GA, on-demand pause and resume, bundling a cancellation into a pending update — is a one-line changelog entry that touches none of those features directly and every one of them indirectly. billing_cycle_anchor, the field that controls when a subscription's billing period resets, stopped accepting a plain string or timestamp. It now requires an object with a type key. Three endpoints are affected. None of them fail quietly.
That's not a Stripe-specific number, and it isn't a 2026 figure — it's an academic study of Java library releases, cited here because it's one of the few places anyone has actually measured how often a breaking change in a dependency turns into broken client code rather than a line nobody acts on. The same study found only about 2.5% of client applications were actually affected by any single breaking change, which sounds reassuring until you remember that "unaffected" just means the client never calls that specific field. If your retention flow does call it — and if you've shipped pause/resume, Trial Offers, or scheduled cancellation bundling from the same Endive release, you already do — you're not in the 97.5% that gets to ignore this one.
What actually changed, on three calls that matter to a retention flow
Before Endive, billing_cycle_anchor took one of three shapes depending on context: the string 'now', the string 'unchanged', or a raw Unix timestamp. After it, every one of those becomes an object with a type field, and the timestamp case adds a second field alongside it.
After: billing_cycle_anchor: { type: 'now' }
Before: billing_cycle_anchor: 'unchanged'
After: billing_cycle_anchor: { type: 'unchanged' }
Before: subscription_details.billing_cycle_anchor: 1762300800
After: subscription_details.billing_cycle_anchor: { type: 'timestamp', timestamp: 1762300800 }
Stripe lists this as a change to exactly three fields, which is worth naming individually because each one sits inside a different part of a typical cancellation or retention flow:
| Affected call | Where it usually fires in a SaaS product | What happens if you still pass a string post-upgrade |
|---|---|---|
| Subscription#update | Plan changes, downgrades, and any flow that resets the billing date on an upgrade | A 400-level invalid_request_error — the update call rejects the request outright |
| Subscription#resume | Resuming a paused subscription, including a "come back" save after a pause-as-retention-offer | The resume call fails the same way, which can mean a customer trying to un-pause gets an error instead of their subscription back |
| Invoice#create_preview (subscription_details.billing_cycle_anchor) | Showing a customer the prorated amount before they confirm a downgrade, upgrade, or plan swap | The preview call errors before the customer ever sees a number, which usually means your plan-change UI breaks rather than just looking wrong |
That last column is the detail worth sitting with. This isn't a date-shifting bug like the one we covered in our piece on Trial Offers and the same field, where a wrong setting quietly moves a renewal date the customer never notices until the next invoice. This fails loud, at the API layer, the moment the request is made. That's better than silent corruption in one sense — you'll see it in error logs instead of a support queue three weeks later — but only if something is actually watching those three call sites for a spike in 400s right after you flip your API version.
Why this isn't automatic, and exactly when it becomes your problem
Stripe pins every account to a specific dated API version — the version it was integrated against, or whatever your SDK defaults to — and nothing changes behind that pin without you deliberately moving it. That's the entire point of Stripe's versioning model: a changelog entry dated September 30 doesn't touch a single live request until you upgrade your account, your webhook endpoint version, or an individual request's Stripe-Version header to 2026-09-30.endive or later. If you've read nothing else about Endive and changed nothing, your integration is exactly as safe today as it was on September 29.
The catch is that Endive isn't a release you can ignore for long if you've been following the rest of what shipped in it. We've covered on-demand pause and resume and bundling a scheduled cancellation into a pending update from this same release, both genuinely useful for a cancellation flow that wants to offer a pause instead of a cancel, or hold a downgrade until an invoice clears. Adopting either one means moving your account onto Endive or later. The moment you do, every string you were passing to billing_cycle_anchor on any of the three affected calls stops being valid input — not eventually, not for some subscriptions, immediately and for all of them.
Source: Postman, State of the API Report
Stripe's own versioning discipline is better than most of the APIs that survey's respondents were describing — this is a deliberate, documented, opt-in change, not a surprise. But "opt-in" doesn't mean "someone on your team will remember." Most teams upgrade an API version to get the one feature they wanted and don't re-read the full list of related changes that shipped alongside it. Endive bundled nine separate changelog entries under one release date, and the billing-cycle-anchor format change is the only one of the nine that can take down an existing, working call with no new feature involved at all.
What to actually do about it
Grep before you upgrade, not after the first error
Search your codebase for every call that sets billing_cycle_anchor, across subscription updates, subscription resumes, and preview-invoice calls specifically — three separate call sites, and it's easy to fix two and miss the third because it lives in a different service or a different team's code. A proration preview on your billing settings page and a resume call inside your cancellation flow's "actually, keep my pause" button are often maintained by completely different people.
Test the upgrade in Workbench before you touch your live account
Stripe's Workbench lets you view your current pinned version and test requests against a newer one before committing your account to it. Run your resume and preview-invoice flows against 2026-09-30.endive there first. If a 400 surfaces, you've found it in a sandbox instead of in a customer's attempt to un-pause their subscription.
Use the 72-hour rollback window as a real safety net, not a formality
Once you perform the upgrade on a live account, Stripe gives you 72 hours to roll back to your previous pinned version. That window exists precisely for a missed call site that only shows up under real traffic — a resume button nobody clicked in staging, a preview call gated behind a feature flag. Treat the first three days after upgrading as an active watch period on error rates for those three endpoints specifically, not a generic "keep an eye on things."
Separate this error from a genuine decline in your monitoring
A 400 on Subscription#resume looks nothing like a declined card, but if your alerting bundles every non-2xx response from Stripe into one generic "payment error" bucket, it'll get triaged like one — and possibly dismissed the same way, since a spike in failed resumes right after a deploy reads as a flaky third-party dependency instead of a parameter you forgot to update. Our piece on webhook bugs quietly creating churn covers a different mechanism with the same root problem: an integration failure that your churn reporting has no label for, so it gets misattributed to the customer instead of the code.
None of this is a reason to hold off on pause/resume or Trial Offers — both are worth adopting, and we've written about why. It's a reason to treat "upgrade the API version" as its own checklist item with its own testing pass, separate from "ship the feature I actually wanted," every time a release bundles more than one change under a single date. If a resume call does fail in production before you catch it, that failed save is exactly the kind of preventable churn worth running through a churn calculator before you write it off as a customer who changed their mind — and it's exactly the gap a CancelFlow cancellation flow should never have to paper over, because the save already happened and the API just didn't let it stick.
Frequently asked questions
What exactly changed with Stripe's billing_cycle_anchor field?+
As of the 2026-09-30.endive API version, billing_cycle_anchor no longer accepts a bare string, enum, or timestamp. It now requires an object with a type field: {type: 'now'} instead of 'now', {type: 'unchanged'} instead of 'unchanged', and {type: 'timestamp', timestamp: 1234567890} instead of a raw Unix timestamp. Stripe calls this out explicitly as a breaking change, and it applies across three separate API calls, not just one.
Which Stripe API calls does this affect?+
Three: updating a subscription (Subscription#update), resuming a paused subscription (Subscription#resume), and creating a preview invoice for a subscription (Invoice#create_preview, via subscription_details.billing_cycle_anchor). Any of your code that sets a billing cycle anchor through one of those three calls needs the new object format once you move to this API version.
Does upgrading my Stripe API version break this automatically, or do I have to do something first?+
Nothing breaks until you actually move your account, your SDK, or a specific request's Stripe-Version header onto 2026-09-30.endive or later. Stripe's versioning model pins every account to whatever dated version it integrated against, specifically so a changelog entry like this one can't silently change behavior under you. The risk isn't the release itself — it's upgrading to get a feature from the same release, like pause/resume or Trial Offers, without also updating this one parameter.
How do I find every place my code might break before I upgrade?+
Grep your codebase for billing_cycle_anchor across all three call sites — subscription updates, subscription resumes, and preview-invoice calls — and check whether any of them pass a bare string or timestamp. Then use Workbench to view your current pinned version and perform the upgrade in a test environment first; Stripe gives you a 72-hour rollback window after upgrading a live account, which is enough time to catch a missed call site in production traffic if your test coverage didn't.
Stop losing subscribers today
One script tag. One function call. A live cancellation flow in under 10 minutes.
Start free trial →