Stripe Flexible Billing Mode in Laravel Cashier — What Changed and How to Migrate

Stripe defaults new subscriptions to flexible billing mode in API 2025-09-30.clover. Here's what changed, where Laravel Cashier breaks, and how to migrate.

Steven Richardson
Steven Richardson
· 8 min read

You added a new subscription product to your Laravel app. The proration on the upgrade looks off, the metered swap throws a 400, and the customer ends up with two invoices when they should have had one. The mode flag on the subscription says flexible — and that one string is why Cashier is misbehaving. Stripe made flexible billing mode the default for any account or integration on API version 2025-09-30.clover, and Cashier is still catching up.

Here's what actually changed, where Cashier breaks, and the migration path I'm using on the apps I've already pushed onto the Clover API.

Classic vs flexible — the actual differences#

Flexible billing mode is not a feature flag for one new behaviour. It's a redesign of how Stripe calculates subscription invoices end to end. From the Stripe docs:

Flexible billing mode provides more accurate and predictable billing behavior for prorations, usage-based pricing, flexible invoicing, and trial settings. It also unlocks new capabilities like mixed interval subscriptions that aren't available in classic mode.

The differences that actually bite in production:

  • Mixed intervals on one subscription. A customer can be on a yearly base price plus a monthly add-on, billed correctly on each cadence. Classic mode required one subscription per interval.
  • Credit prorations are calculated against the original debited amount. Tax, discount, and billing-cycle history are applied accurately. Classic mode used a simpler formula that drifted on long-running subs.
  • Consolidated invoicing on renewal. Removed metered items don't generate a second invoice. Zero-amount line items disappear from added meter prices.
  • Configurable proration on cancellation. You decide whether mid-period cancels generate credits or not.
  • Stable billing cycle anchor. Adding or removing items doesn't implicitly reset the cycle.

The version map is worth pinning to a wiki:

  • 2025-06-30.basilbilling_mode becomes a parameter, /migrate endpoint introduced (stripe-php v17.4.0)
  • 2025-07-30.basil — billing thresholds and mixed intervals land (stripe-php v17.5.0)
  • 2025-09-30.clover — flexible becomes the default, iterations removed from subscription schedules (stripe-php v18.0.0)

If you bumped the Stripe library through any Laravel upgrade since last summer, check what API version your account is pinned to in the dashboard before you assume nothing changed.

What's broken in Cashier today#

Cashier has shipped one targeted patch — the clear_usage fix in PR #1820 — but full flexible-mode support is still tracked in the open feature request, issue #1832. Two patterns bite hardest right now.

clear_usage on metered swaps. Cashier sends clear_usage: true when you call swapAndInvoice() on a metered subscription. Flexible mode rejects the parameter outright. The current workaround is to drop into the underlying Stripe SDK for the swap:

use Laravel\Cashier\Cashier;

$subscriptionItem = $subscription->items()->first();

Cashier::stripe()->subscriptionItems->update($subscriptionItem->stripe_id, [
    'price' => $newPriceId,
    // 'clear_usage' => true, // <- omit in flexible mode
    'proration_behavior' => 'create_prorations',
]);

billing_mode on checkout sessions. On a subscriptions.create call, billing_mode is a top-level parameter. On a checkout session — which is what Cashier::checkout() uses — the same parameter has to be nested under subscription_data:

// Bad: top-level on checkout sessions returns 400
$session = $user->newSubscription('default', $priceId)
    ->checkout(['billing_mode' => ['type' => 'flexible']]);

// Good: nested under subscription_data
$session = $user->newSubscription('default', $priceId)
    ->checkout([
        'subscription_data' => [
            'billing_mode' => ['type' => 'flexible'],
        ],
    ]);

If you're using Stripe Checkout sessions in Laravel without Cashier directly, the same nesting applies — subscription_data.billing_mode on the session payload, never top-level.

There's also a subtle behavioural change for anyone running usage-based billing with Stripe Meters and Cashier: meters are customer-level, not subscription-level. If a customer holds two flexible-mode subscriptions that share the same metered price, reported usage aggregates across both. Classic mode let you scope usage per subscription item. Re-architect your reporting before you migrate or you'll double-count.

The migration path Stripe gives you#

Stripe ships a dedicated migration endpoint. You cannot send billing_mode on a subscription update — it's read-only after create. The endpoint lives at POST /v1/subscriptions/:id/migrate:

curl https://api.stripe.com/v1/subscriptions/sub_1NX8Lk2eZvKYlo2C/migrate \
  -u sk_test_xxx: \
  -H "Stripe-Version: 2025-09-30.clover" \
  -d "billing_mode[type]=flexible"

The response includes billing_mode_details.updated_at so you can confirm the migration actually happened. It is one-way — once a subscription is on flexible mode, it stays there. There is no /migrate back to classic.

To track which subs you've moved without hammering the API on every page load, add a local column:

Schema::table('subscriptions', function (Blueprint $table) {
    $table->string('billing_mode')->default('classic')->after('stripe_status');
    $table->timestamp('billing_mode_migrated_at')->nullable();
});

Then a small accessor on your subscription model:

public function usesFlexibleBilling(): bool
{
    return $this->billing_mode === 'flexible';
}

Now your code can branch on the local flag — no per-request Cashier::stripe()->subscriptions->retrieve() call just to check the mode.

For the actual migration, batch it through a queue. The same advice from my scaling Laravel queues in production guide applies: chunk by 100, retry transient failures, and log the billing_mode_details.updated_at timestamp on success. Don't run /migrate synchronously from a controller.

What Cashier support will look like#

The proposed API in issue #1832 follows the pattern Cashier already uses for tax behaviour and currencies — global default plus per-call override:

// In a service provider
use Laravel\Cashier\Cashier;

Cashier::defaultBillingMode('flexible');

// Override for a specific subscription
$user->newSubscription('default', $priceId)
    ->withBillingMode('flexible')
    ->create($paymentMethod);

// Migrate an existing subscription in-line
$subscription->migrateToFlexibleBillingMode();

// Read the current mode
if ($subscription->usesFlexibleBilling()) {
    // ...
}

Two PRs are in play: a draft (#1772) covering most of the surface area, and a closed PR (#1830) that landed cleaner but never got a review. The maintainers' position is that the migration path is in scope but quotes and subscription schedules are subsequent work. If you need quotes today, you'll be reaching past Cashier into the underlying SDK for now.

What to do right now#

If you're on classic mode and your Stripe account hasn't been bumped to 2025-09-30.clover:

  • Pin your Stripe-Version header explicitly in config/cashier.php rather than trusting the account default. This stops a dashboard upgrade from silently changing your subs' behaviour.
  • Add the billing_mode column to your subscriptions table now, even though you're not using it yet. Backfill it to classic for all existing rows.
  • Add a feature flag — Laravel Pennant feature flags is the obvious fit — for flexible_billing_enabled so you can roll the migration out per cohort.

If you're already creating new subs in flexible mode and hitting the clear_usage or checkout-session bugs:

  • Drop clear_usage from any direct or swapAndInvoice() call paths.
  • Audit every checkout() call for top-level billing_mode and move it under subscription_data.
  • If you're running Stripe subscription lifecycle webhooks, update the handler to read billing_mode.type off the payload and persist it to your local column.

Gotchas and edge cases#

A few production traps the docs gloss over.

Prebilling, the legacy max_occurrences parameter, legacy non-meter usage billing, and pay_immediately=false for legacy 3P tax integrations are all rejected with a 400 in flexible mode. If you're integrated with Avalara on the older flow, plan a separate migration for tax before you flip billing mode.

iterations is removed from subscription schedules in 2025-09-30.clover. Use duration instead. If you have a scheduled command rolling subs forward through phases, find that string and replace it.

Cashier's Stripe Customer Portal helpers work in flexible mode, but the portal itself surfaces different proration UI. Take screenshots of your portal flows in test mode before and after migration so support knows what changed.

Trials behave differently. Free-trial behaviour got cleaner in flexible mode but the timing of the first proration on conversion shifts. Re-record your test fixtures from my Stripe trials for memberships walkthrough against a flexible-mode account.

billing_mode is set at create time and cannot be changed via subscriptions.update. Use /migrate. Period.

Wrapping Up#

Flexible billing mode is the right default — Stripe wouldn't have flipped the switch otherwise — and the proration accuracy alone justifies migrating. The pain is timing: do it before Cashier ships native support and you carry the workarounds yourself; wait, and you risk shipping new code paths that assume classic semantics. My advice is to add the local column and the feature flag now, run the /migrate endpoint behind a job for a small cohort, and keep an eye on issue #1832 for when the global default lands in Cashier itself. If you want the broader Laravel-13 context, the Laravel 12 to 13 upgrade guide covers what shifted at the framework level around the same window.

FAQ#

What is Stripe flexible billing mode?

Flexible billing mode is the new default subscription engine in Stripe API version 2025-09-30.clover. It rewrites how prorations, usage-based pricing, and invoice consolidation work, and it unlocks features like mixed-interval subscriptions on a single subscription record. Existing classic-mode subscriptions keep their behaviour until you explicitly migrate them.

How do I migrate Cashier subscriptions to flexible billing?

There's no first-party Cashier helper yet — issue #1832 is still open. Migrate by hitting the Stripe SDK directly with Cashier::stripe()->subscriptions->migrate($subscription->stripe_id, ['billing_mode' => ['type' => 'flexible']]). Persist the result to a local billing_mode column on your subscriptions table so you don't pay an API call every request. Always run migrations through a queued job, never synchronously from a controller.

Does Laravel Cashier support flexible billing mode?

Cashier supports flexible mode partially. The clear_usage fix landed in PR #1820, but the full feature set — global default, per-subscription override, and a migrateToFlexibleBillingMode() helper — is still tracked in open issue #1832. Until those merge, you'll set billing_mode manually on create calls and reach past Cashier for the migration endpoint and quotes.

What's the difference between classic and flexible billing?

Classic billing uses Stripe's original proration formula and the one-interval-per-subscription rule. Flexible billing calculates credits against the originally debited amount, supports mixed intervals on a single subscription, consolidates renewal invoicing, and lets you configure cancellation proration. Flexible mode also requires meter-based usage billing — the legacy non-meter usage path is rejected outright.

Will I lose data when switching to flexible billing mode?

No. The /migrate endpoint preserves the subscription's history, items, and customer association. Stripe records the migration timestamp on billing_mode_details.updated_at and continues invoicing without interruption. The migration is one-way, though — you can't move a subscription back to classic — so test thoroughly on a copy of your data before running it in production.

Can I run flexible and classic mode subscriptions side by side?

Yes. Each subscription carries its own billing_mode flag, so an account can hold a mix indefinitely. New subscriptions on the Clover API default to flexible; older classic subscriptions stay classic until you call /migrate. The catch is your code: branch on the local billing_mode column when sending parameters that differ between modes (like clear_usage), or you'll hit 400s on the classic path.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.