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.

When the first real charge lands

The local engine bills in arrears: an order closes the period it is for, so the charge happens at the end of a cycle rather than at its start. The payment taken at checkout is not a plan payment — it exists to establish the mandate, and billing.mandate_verification_minor sets it, defaulting to one minor unit (0.01 in the account currency).

Put together, that is how long a subscriber uses the service before real money moves:

PlanFirst real charge
Monthly, no trialafter 30 days
Monthly, 14-day trialafter 44 days
Yearly, no trialafter 365 days

The last row is the one to look at before shipping a yearly tier. A year of service is delivered before anything beyond that one minor unit has been collected, and the trial adds its own length on top rather than overlapping.

This is stated here rather than discovered in production. If the wait does not suit your business, the lever that needs no code is the period length: a yearly price billed monthly carries the same annual sum with thirty days of exposure instead of three hundred and sixty-five. Charging in advance instead is a different model and not one this engine offers — metered usage cannot be billed before it happens, so a mixed scheme would put two labeling rules in one ledger.

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 mapping, or a billing_coupons row with a provider_coupon_idThe 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.

A live billing_coupons row resolves its code on its own, with no config entry. Where a live row and a billing.coupons entry share a code, the row answers, and its provider_coupon_id, when set, is the coupon the checkout session gets.

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