Skip to main content

Subscriptions

A visitor becomes a subscriber from the /plan screen. The client submits the tier key only — the price is resolved server-side (anti-price-injection) — and the package opens a hosted Checkout Session in subscription mode. The trial, provider tax and VAT-id collection, promotion codes and the billing address all ride on that session, and the card is captured (with SCA / 3-D Secure) on the provider's own page. On return, /checkout/return reconciles the subscription onto the local row immediately, so a paying customer is never shown "Free" while the webhook is still in flight. An owner who already subscribes swaps in-app instead of opening a second subscription.

The return URLs default to the hub's own routes; set billing.checkout.success_url / cancel_url only to override them. Set billing.customer.model to your billable model — without it, no subscription webhook can find its owner.

Swaps, cancel, resume, trials

  • In-app upgrade/downgrade swap with a proration preview; an upgrade takes effect immediately, a downgrade is scheduled to the current period end so the customer is never charged twice or refunded for time they already paid.

  • Cancel-into-grace (paid through the period end), resume, and immediate cancel from the danger zone.

  • Trialsnone / subscription / generic, global or per-tier, card required or if_required; a trial-ending reminder goes out before the trial lapses, and it reaches every trial whatever drives it. Which path delivers it depends on whether the provider announces trial ends at all: a driver that does sends its own event, and billing:trials:warn covers the rest — a generic trial, which has no provider behind it, and a subscription trial under a driver whose cycle this package runs itself. Nothing at such a provider knows a trial is running, so nothing there can say it is ending. Both paths record the same notice, so a customer is told once.

    The mode without a card is the one where that matters most: there is no failed charge and no receipt to notice, so the reminder is the only signal before the first debit.

  • A subscription trial is granted once per customer. Not once per subscription — the eligibility is read from their own billing history, so somebody who cancels and comes back subscribes at the full price. The plan screen offers the trial only to a customer who would actually be granted one, so the page and the checkout never disagree. A generic trial follows the same rule through the customer's own trial date.

  • The days a trial covers carry no plan charge, and the document says so at zero. Under a driver whose cycle this package runs itself, an order names the period it closes — the cycle that ends a trial therefore names the trial's own days. That period is billed with the recurring plan line priced at zero, so the invoice states which days were free and what they were free of, rather than leaving no record of them. The first plan charge is the cycle after it.

    Metered usage and add-ons are not part of the waiver. A free trial is an offer about the recurring charge; consumption an application priced for those days is still billed on the same document. The distinction is the line type, so a CycleItemPricer that expresses a plan as several fixed lines is covered, and a metered line beside them is not.

    The waiver runs after the order-item preprocessor chain: a step may reprice the plan for an ordinary cycle, and may not reprice a period the plan screen promised for free. It runs before the coupon, so a customer does not spend one of their discounted cycles on a cycle nobody charged them for.

    One consequence to know if you listen for payments. The cycle still closes, so it still dispatches PaymentSucceeded — with an amount of zero and no provider reference, because nothing was collected. A listener that mails a receipt on that event will mail a zero receipt at every trial end unless it checks the amount. The event is not suppressed on purpose: a cycle that a customer's credit balance covers in full behaves the same way, and a listener that has to handle one already handles the other.

Coupons, and what "applied" means

The account hub has a coupon field, and the status under it answers one question: would the driver you configured actually apply this code? Not "does the code exist" — that is a different question and it is the one that used to be answered there, which meant a customer could be told their code took and then be charged in full.

Which catalog answers depends on who applies the discount:

Your driverWhere the code has to beWhat applies it
A hosted checkout (Stripe)billing.coupons, with a stripe_coupon mappingThe provider, on its own checkout session
A driver the package bills itselfThe billing_coupons tableThe package, as a discount line on each cycle it covers

A billing.coupons entry with no stripe_coupon reaches the checkout session as nothing at all, so the hub calls it invalid. That is not a new limitation — it is the existing one, said out loud.

Under a driver the package bills itself the coupon is carried, not spent, across the redirect. The customer goes to the provider to establish a mandate; the code travels with the rest of the request and is redeemed when the mandate settles and the subscription becomes real. A customer who closes the tab has spent nothing: a coupon allows one redemption per owner, and burning it on a checkout that never finished would take it away for good.

A coupon never blocks a subscription. If it has run out, expired, or was already used by this customer in the time between the screen and the settlement, they are subscribed at full price rather than not subscribed.

Localized hub and emails

The hub and its emails ship translated in English, German, Spanish, French, Italian, Dutch and Portuguese, with an informal register throughout. Publish the views or the translations to customize them:

php artisan vendor:publish --tag=billing-views
php artisan vendor:publish --tag=billing-lang

Every publishable asset also sits under a shared billing umbrella tag, so php artisan vendor:publish --tag=billing publishes the config, migrations, views and translations in one go.


← Back to the documentation index