Documentation

Complete guide to using flexible billing in your Laravel application.

Back to Demo

Laravel Cashier — Flexible Billing Guide

This guide covers everything you need to know to use flexible billing in your Laravel application with Cashier.

Table of Contents


Installation

Publish and run the Cashier migrations:

php artisan vendor:publish --tag=cashier-migrations
php artisan migrate

This creates the standard Cashier tables plus:

  • subscription_schedules — for multi-phase subscription management
  • cashier_quotes — for quote lifecycle tracking
  • cashier_usage_thresholds — for usage monitoring
  • cashier_rate_cards — for local pricing models

Add the Billable trait to your User model:

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Getting Started

Setting the Global Default

The simplest way to enable flexible billing for your entire application is to set the global default in your AppServiceProvider:

use Laravel\Cashier\Cashier;

public function boot(): void
{
    Cashier::defaultBillingMode('flexible');
}

After this, every new subscription created through Cashier will use flexible billing automatically — no other code changes needed.

Per-Subscription Override

If you prefer to opt in per subscription, use withBillingMode():

$user->newSubscription('default', $priceId)
    ->withBillingMode('flexible')
    ->create($paymentMethod);

You can also override the global default for a specific subscription:

// Global default is flexible, but this one uses classic
$user->newSubscription('legacy', $priceId)
    ->withBillingMode('classic')
    ->create($paymentMethod);

Creating Flexible Subscriptions

Basic Subscription

$subscription = $user->newSubscription('default', 'price_monthly')
    ->withBillingMode('flexible')
    ->create('pm_card_visa');

With a Trial Period

$subscription = $user->newSubscription('default', 'price_monthly')
    ->withBillingMode('flexible')
    ->trialDays(14)
    ->create('pm_card_visa');

Via Checkout

$checkout = $user->newSubscription('default', 'price_monthly')
    ->withBillingMode('flexible')
    ->checkout([
        'success_url' => route('billing.success'),
        'cancel_url' => route('billing.cancel'),
    ]);

return redirect($checkout->url);

Checking Billing Mode

if ($subscription->usesFlexibleBilling()) {
    // This subscription is on flexible billing
}

Hybrid Billing (Fixed + Metered)

Combine a fixed monthly fee with usage-based charges on a single subscription:

$subscription = $user->newSubscription('default')
    ->price('price_base_plan')           // $29/mo fixed
    ->meteredPrice('price_api_calls')    // $0.01 per API call
    ->withBillingMode('flexible')
    ->create('pm_card_visa');

This creates one subscription with two items. The base plan charges monthly, and the metered price charges based on reported usage.

Removing a Metered Price

In flexible billing mode, you can remove metered prices without the clear_usage errors that occur in classic mode:

$subscription->removePrice('price_api_calls');

Adding a Metered Price Later

$subscription->addMeteredPrice('price_storage_gb');

Proration Discounts

Flexible billing gives you control over how prorations appear on invoices:

Itemized Mode

Discount amounts are shown as separate line items on invoices, giving customers full transparency:

$subscription = $user->newSubscription('default', $priceId)
    ->withBillingMode('flexible')
    ->withProrationDiscounts('itemized')
    ->create('pm_card_visa');

Included Mode

Amounts are net of discounts — the traditional behavior:

$subscription = $user->newSubscription('default', $priceId)
    ->withBillingMode('flexible')
    ->withProrationDiscounts('included')
    ->create('pm_card_visa');

Swapping Plans

Plan swaps work the same as classic mode. The billing mode is preserved automatically:

// Upgrade
$subscription->swap('price_premium');

// Downgrade
$subscription->swap('price_starter');

// The subscription stays on flexible billing through all swaps
$subscription->usesFlexibleBilling(); // true

Cancel and Resume

Grace periods and resumption work identically to classic mode:

// Cancel at end of billing period (grace period)
$subscription->cancel();

$subscription->onGracePeriod();  // true
$subscription->valid();          // true — still active until period ends

// Resume before grace period expires
$subscription->resume();

$subscription->active();                // true
$subscription->usesFlexibleBilling();   // true — preserved

Cancel Immediately

$subscription->cancelNow();

// Or cancel and invoice for usage
$subscription->cancelNowAndInvoice();

Subscription Schedules

Pre-plan subscription transitions with multi-phase schedules. Useful for:

  • Trial-to-paid conversions
  • Annual discount periods
  • Staged enterprise onboarding

Creating a Schedule

$schedule = $user->newSubscriptionSchedule('default')
    ->withBillingMode('flexible')
    ->addPhase([
        ['price' => 'price_starter', 'quantity' => 1],
    ], ['iterations' => 3])  // 3 months on starter
    ->addPhase([
        ['price' => 'price_pro', 'quantity' => 1],
    ], ['iterations' => 12]) // Then 12 months on pro
    ->startDate('now')
    ->create();

From an Existing Subscription

Convert a running subscription into a managed schedule:

$schedule = $user->newSubscriptionSchedule('default')
    ->createFromSubscription($subscription);

// The schedule inherits the subscription's billing mode automatically

Note: Do not call withBillingMode() when creating from an existing subscription — Stripe inherits it from the source and will reject an explicit value.

Schedule Operations

// Check status
$schedule->active();
$schedule->notStarted();
$schedule->completed();

// Get phases
$phases = $schedule->phases();

// Release — subscription continues independently
$schedule->release();

// Cancel — subscription is canceled
$schedule->cancel();

// Update
$schedule->updateSchedule([
    'end_behavior' => 'cancel',
]);

Querying Schedules

// Get all schedules
$schedules = $user->subscriptionSchedules;

// Find by type
$schedule = $user->subscriptionSchedule('default');

// Find by Stripe ID
$schedule = $user->findSubscriptionSchedule('sub_sched_xxx');

End Behavior

Control what happens when the last phase completes:

$schedule = $user->newSubscriptionSchedule('default')
    ->endBehavior('release')   // Subscription continues (default)
    // ->endBehavior('cancel') // Subscription is canceled
    // ->endBehavior('none')   // No action
    ->addPhase([...])
    ->create();

Quotes

Generate formal quotes for B2B sales workflows:

Creating a Quote

$quote = $user->newQuote()
    ->addLineItem('price_enterprise', 1)
    ->description('Enterprise plan — annual commitment')
    ->header('Acme Corp Proposal')
    ->footer('Valid for 30 days')
    ->withMetadata(['sales_rep' => 'jane@company.com'])
    ->create();

With an Expiration Date

$quote = $user->newQuote()
    ->addLineItem('price_enterprise', 1)
    ->expiresAt(now()->addDays(30))
    ->create();

With Flexible Billing

When a quote is accepted, the resulting subscription uses flexible billing:

$quote = $user->newQuote()
    ->addLineItem('price_enterprise', 1)
    ->withBillingMode('flexible')
    ->create();

Quote Lifecycle

// Draft → Open
$quote->finalize();

// Open → Accepted (creates the subscription)
$quote->accept();

// Open → Canceled
$quote->cancel();

Check Status

$quote->draft();     // Not yet finalized
$quote->open();      // Finalized, awaiting customer
$quote->accepted();  // Customer accepted
$quote->canceled();  // Quote was canceled

Download PDF

return $quote->downloadPdf();

// Or with a custom filename
return $quote->downloadPdf('proposal-2024.pdf');

Querying Quotes

$quotes = $user->quotes;
$quote = $user->findQuote('qt_xxx');

Billing Credits

Manage customer credit balances for promotional credits, refunds, or prepaid usage.

Adding Credits

// Add $50 in credits
$user->addBillingCredits(5000, 'Welcome bonus');

// Or use the existing Cashier method
$user->creditBalance(5000, 'Welcome bonus');

Checking Balance

$credits = $user->availableCredits();       // 5000 (in cents)
$hasFunds = $user->hasSufficientCredits(3000); // true

Calculating Credit Application

See how credits would cover a usage charge without actually modifying the balance:

$result = $user->calculateCreditApplication(8000); // $80 usage

// Returns:
// [
//     'applied_credits' => 5000,  // $50 covered by credits
//     'remaining_usage' => 3000,  // $30 still owed
//     'credits_after' => 0,       // $0 credits remaining
// ]

Deducting Credits

$user->deductBillingCredits(2000, 'Monthly usage charge');

// Or use the existing Cashier method
$user->debitBalance(2000, 'Monthly usage charge');

Transaction History

$transactions = $user->balanceTransactions(25);

foreach ($transactions as $transaction) {
    echo $transaction->amount();         // Formatted: -$50.00
    echo $transaction->rawAmount();      // Raw: -5000
    echo $transaction->endingBalance();  // Formatted: -$50.00
}

Metered Usage Reporting

Report usage events to Stripe for metered billing:

// Report 1 event
$user->reportMeterEvent('api_calls');

// Report multiple
$user->reportMeterEvent('api_calls', 100);

// With custom options
$user->reportMeterEvent('api_calls', 50, [
    'timestamp' => now()->subHour()->getTimestamp(),
]);

Getting Usage Summaries

$summaries = $user->meterEventSummaries(
    meterId: 'meter_xxx',
    startTime: now()->subMonth()->getTimestamp(),
    endTime: now()->getTimestamp(),
);

$totalUsage = $summaries->sum('aggregated_value');

Listing Meters

$meters = $user->meters();

Usage Thresholds

Monitor usage against configurable limits. Thresholds are stored in the database (not cache) so they persist reliably.

Setting a Threshold

$user->setUsageThreshold('meter_api_calls', 10000, 'billing_cycle', [
    'alert_email' => 'billing@company.com',
]);

Valid periods: billing_cycle, monthly, daily, weekly

Checking Against a Threshold

$threshold = $user->getUsageThreshold('meter_api_calls');

// Check if usage exceeds the threshold
$threshold->isExceeded(15000);  // true

// Get usage as a percentage
$threshold->usagePercentage(7500);  // 75.0

// Calculate overage
$threshold->overage(12000);  // 2000

Removing a Threshold

$user->removeUsageThreshold('meter_api_calls');

Rate Cards

Model pricing locally for display, comparison, or cost estimation without Stripe API calls.

Tiered Pricing (Graduated)

Each tier prices only the usage within its range:

use Laravel\Cashier\RateCard;

$card = RateCard::create([
    'name' => 'API Calls',
    'product_id' => 'prod_xxx',
    'pricing_type' => 'tiered',
    'rates' => [
        'mode' => 'graduated',
        'tiers' => [
            ['up_to' => 1000,  'unit_amount' => 10, 'flat_amount' => 0],   // $0.10
            ['up_to' => 10000, 'unit_amount' => 5,  'flat_amount' => 0],   // $0.05
            ['up_to' => null,  'unit_amount' => 2,  'flat_amount' => 0],   // $0.02
        ],
    ],
    'currency' => 'usd',
]);

$result = $card->calculatePricing(15000);
// Total: $65.00
// Tier 1: 1,000 x $0.10 = $10.00
// Tier 2: 9,000 x $0.05 = $45.00
// Tier 3: 5,000 x $0.02 = $10.00

Tiered Pricing (Volume)

All units priced at the tier the total falls into:

$card = RateCard::create([
    'name' => 'Storage',
    'product_id' => 'prod_xxx',
    'pricing_type' => 'tiered',
    'rates' => [
        'mode' => 'volume',
        'tiers' => [
            ['up_to' => 100,  'unit_amount' => 50, 'flat_amount' => 0],  // $0.50
            ['up_to' => 1000, 'unit_amount' => 30, 'flat_amount' => 0],  // $0.30
            ['up_to' => null, 'unit_amount' => 10, 'flat_amount' => 0],  // $0.10
        ],
    ],
    'currency' => 'usd',
]);

$result = $card->calculatePricing(500);
// 500 falls in tier 2, so all 500 priced at $0.30
// Total: $150.00

Package Pricing

Usage rounded up to the nearest package:

$card = RateCard::create([
    'name' => 'Messages',
    'product_id' => 'prod_xxx',
    'pricing_type' => 'package',
    'rates' => [
        'package_size' => 1000,
        'package_price' => 500,  // $5.00 per 1,000
    ],
    'currency' => 'usd',
]);

$result = $card->calculatePricing(2500);
// ceil(2500 / 1000) = 3 packages
// Total: $15.00

Flat Rate Pricing

$card = RateCard::create([
    'name' => 'Bandwidth',
    'product_id' => 'prod_xxx',
    'pricing_type' => 'flat',
    'rates' => ['unit_amount' => 1],  // $0.01 per MB
    'currency' => 'usd',
]);

$result = $card->calculatePricing(50000);
// 50,000 x $0.01 = $500.00

Querying Rate Cards

// Active rate cards for a product
$cards = RateCard::active()->forProduct('prod_xxx')->get();

// Deactivate a rate card
$card->deactivate();

Migrating from Classic to Flexible

This is a one-way operation. Once a subscription is migrated to flexible billing, it cannot go back to classic.

Individual Subscription

$subscription->migrateToFlexibleBillingMode();

This uses Stripe's dedicated /migrate endpoint. The subscription status is preserved — it continues running without interruption.

Safe Guards

The method includes safety checks:

// Already flexible — returns immediately (no API call)
$subscription->migrateToFlexibleBillingMode();

// Incomplete subscription — throws SubscriptionUpdateFailure
// Canceled subscription — throws LogicException

All New Subscriptions

Set the global default so new subscriptions automatically use flexible billing. Existing subscriptions are unaffected:

// In AppServiceProvider::boot()
Cashier::defaultBillingMode('flexible');

Migration Strategy

  1. Set Cashier::defaultBillingMode('flexible') — all new subs use flexible
  2. Migrate existing subs gradually: $subscription->migrateToFlexibleBillingMode()
  3. Use $subscription->usesFlexibleBilling() to check which mode a sub is on

Webhook Handling

The following webhook events are handled automatically:

Subscription Schedule Events

Event What Happens
subscription_schedule.created Creates a local schedule record
subscription_schedule.updated Updates status, phase timestamps, subscription ID
subscription_schedule.canceled Sets status to canceled with timestamp
subscription_schedule.completed Sets status to completed with timestamp
subscription_schedule.released Sets status to released with timestamp

Quote Events

Event What Happens
quote.finalized Updates status, number, amounts, finalized_at
quote.accepted Sets status to accepted with timestamp
quote.canceled Sets status to canceled with timestamp

To receive these events, add them to your Stripe webhook configuration. In your config/cashier.php:

'webhook' => [
    'events' => [
        // ... existing events ...
        'subscription_schedule.created',
        'subscription_schedule.updated',
        'subscription_schedule.canceled',
        'subscription_schedule.completed',
        'subscription_schedule.released',
        'quote.finalized',
        'quote.accepted',
        'quote.canceled',
    ],
],

Configuration Reference

Global Billing Mode

// In AppServiceProvider::boot()
Cashier::defaultBillingMode('flexible'); // or 'classic'

Per-Builder Billing Mode

Available on all builders:

// SubscriptionBuilder
$user->newSubscription('default', $price)->withBillingMode('flexible');

// CheckoutBuilder (inherited from SubscriptionBuilder)
$user->newSubscription('default', $price)->withBillingMode('flexible')->checkout([...]);

// SubscriptionScheduleBuilder
$user->newSubscriptionSchedule('default')->withBillingMode('flexible');

// QuoteBuilder
$user->newQuote()->withBillingMode('flexible');

Incompatibilities

Billing thresholds cannot be used with flexible billing mode:

// This will throw InvalidArgumentException
$user->newSubscription('default', $price)
    ->withBillingMode('flexible')
    ->withBillingThresholds(['amount_gte' => 1000])
    ->create($pm);

billing_mode cannot be changed after creation. Use the dedicated migration method:

// This is the only way to change billing mode
$subscription->migrateToFlexibleBillingMode();

// swap() does NOT change billing mode (it's preserved)
$subscription->swap($newPrice);

Stripe API Version

Flexible billing requires Stripe API version 2025-06-30.basil or later. Laravel Cashier's bundled stripe-php SDK (v17.4.0+) includes support for all flexible billing endpoints.

Back to Demo · View on GitHub