Subscriptions
This page explains how to sell something that bills again. It is for the store administrator who wants a customer charged every month or every year — a membership, a hosted service, software with a maintenance fee — and needs the store to keep that calendar, produce a receipt for every period, and let both sides see where the agreement stands.
Subscriptions are an add-on, not part of KahunaCart itself. The base plugin knows nothing about plans, periods or renewals. Install the kahunacart-subscriptions plugin alongside it and your catalog can sell on a calendar; leave it out and every other part of your store behaves exactly as it does today.
Two facts sit underneath everything else here. Your store is the system of record: a subscription is a row in your own database naming a plan, a customer, a status and the period currently being paid for, and a payment provider moves money and tells you what happened exactly as it does for a one-off order. And every billing period is a real order, with an order number, a receipt, an invoice, line items, tax, a payment record and a place in your reports — so refunds, emails, licence issuance and reporting all work on a renewal with no special case.
Before you begin
- A working store you can already take an order on. See Getting started.
- The
kahunacart-subscriptionsplugin installed and enabled, andkahunacart.subscriptions.viewor.manageon whoever is going to run it. - The scheduler running. Invoice mode's whole calendar rides on
bin/plugin kahunacart work, and without it no reminder goes out and nothing renews. See CLI. system.custom_base_urlset to the store's public address, because emails queued by the scheduler have no browser request to read it from and every pay link would come out relative.- A product with the variant you want to sell on a calendar. See Products and catalog.
Plans are variants
A plan is a product variant plus a billing interval. There is no second catalog: the price, the title, the description, the images, the tax class and the translations all live on the variant, in the product editor you already use. The plan adds the calendar.
A variant carries exactly one plan. Sell the same thing monthly and yearly by giving the product two variants — "Pro, monthly" and "Pro, yearly" — with a plan on each, which also gives the customer something to choose between on the product page.
Create a plan
- In the admin sidebar, click KahunaCart, then Subscriptions → Plans.
- Click New plan.
- Pick the variant this plan sells. Variants that already carry a plan are listed but cannot be picked.
- Set the interval: a count and a unit.
1and Months is monthly;3and Months is quarterly;1and Years is annual. - Set a free trial in days, or leave it at
0. - Set the billing mode. Automatic where possible is the right answer unless you have a reason.
- Leave On sale ticked and click Create the plan.
The plan is live immediately, and the product's buy button now sells a subscription.
The plan fields
| Field | What it does |
|---|---|
| Variant | What is sold. Not editable afterwards — every running subscription froze its amount from this variant. |
| Interval | How long one period lasts: a count and a unit of days, weeks, months or years. The count is capped at 52. |
| Free trial | Days before the first invoice is due. 0 means the customer pays immediately. |
| Billing mode | Automatic where possible, Automatic, or Invoice. See The two billing modes. |
| Renewal variant | The variant a renewal order carries when it should not be the plan's own. Blank means the same variant. |
| Grace period | Invoice mode only. Days an unpaid invoice keeps the subscription alive. Blank uses the store default. |
| On sale | Untick to stop selling the plan. Running subscriptions keep renewing; nobody new can subscribe. |
Months and years are calendar arithmetic with end-of-month clamping: a monthly period starting on 31 January ends on 28 February, or 29 in a leap year, and then on 31 March. The clamp is lossy in one direction only, so a subscription cannot drift forward through the calendar. Days and weeks are exact multiples of 86,400 seconds, and every date is UTC.
The renewal variant is how an introductory period renews at the full price. Sell "Pro — first year $99" as the plan's variant, name "Pro — annual $149" as the renewal variant, and the first order charges 99 while every renewal charges 149. A plan with live subscriptions cannot be deleted, because a subscription reads its interval from the plan every time the calendar advances; untick On sale to stop selling it instead.
A subscription checks out on its own
A cart holding a plan holds nothing else and holds one of it. Adding a second subscription — the same plan or a different one — is refused at add-to-cart with a message beside the button, and so is adding an ordinary product alongside it. Changing the quantity of the plan line already in the cart still works, and the checkout refuses a second time if an order somehow reaches it holding two plan lines.
The recurring price is frozen at checkout
A subscription's amount is fixed at the moment it is created, so a plan whose price changes later never reprices anybody already paying. Edit the variant's price to change what new customers pay; existing subscriptions keep the amount they were sold at. Shipping and tax do not carry, and are recomputed for each period against wherever the customer is by then.
Important
The amount frozen is what the customer actually paid for the plan line, not the variant's list price. A sale running when they subscribed, or a coupon they entered at checkout, is carried into the subscription and renews at that price for the life of the agreement, and in provider mode the agreement is opened at the platform at that price too. Use the renewal variant field when you want a cheaper first period and the full price after it.
The two billing modes
Provider mode. The payment platform runs the calendar. It holds the customer's card, charges it on schedule, and tells your store what happened through the webhook the provider driver already receives. KahunaCart mirrors the state and mints the renewal order. Choose it when your provider supports recurring billing and your customers expect a card to be charged without being asked.
Invoice mode. Your store runs the calendar. It builds a real order for each period, emails the customer a link to pay it, chases with reminders, allows a grace period past the due date, and lapses the subscription if nothing arrives. Choose it when your provider has no recurring billing, when you would rather not store cards, or when your customers pay by bank transfer.
A plan set to Automatic where possible resolves to provider mode when the provider the customer picked can run a calendar, and to invoice mode when it cannot. A plan set to Automatic removes providers that cannot from the checkout entirely, so a customer never picks a payment method that cannot fulfil the purchase. Whichever mode it resolves to is frozen onto the subscription when it is created, so changing a plan's billing mode affects new subscribers only.
Which providers run which mode
| Provider | Provider mode | Invoice mode |
|---|---|---|
| Stripe, including Managed Payments | yes | yes |
| Polar | yes | yes |
Test provider (kahunacart-dummy) |
yes | yes |
| PayPal | — | yes |
| Offline (bank transfer, cheque) | — | yes |
| Any other provider | only if its driver declares the Subscriptions capability |
yes |
Invoice mode needs nothing from a driver. It uses the ordinary payment flow every provider already implements, once per period, on a link the store emailed. Stripe Managed Payments sells subscriptions as well as one-off orders, and Stripe stays merchant of record on every period rather than only the first — see Payment providers and Tax → Merchant-of-record providers.
Invoice mode
The reminder ladder
Reminders are configured as whole days before the period ends. The shipped schedule is 14, 3, 0: two weeks out, three days out, and the day the period itself ends. The renewal order is built once, when the first rung comes due, and every later rung links to that same order — a customer who pays on the first email and one who pays on the last both pay one invoice, and your reports never show a period with two orders against it.
Edit Reminder Schedule in the plugin's settings to change it. The list is sorted, de-duplicated and capped at Maximum Reminders. A schedule of [0] sends one email on the day the period ends, and an empty schedule is treated as [0].
Grace, past due and expired
Past due means the money has not arrived and the clock is running. The subscription is marked past_due the moment the period ends unpaid, and past_due counts as live: access continues, licences keep validating, and downloads keep working. Paying the outstanding invoice — through the link, or by you marking the order paid when a transfer lands — puts it straight back to active with the period advanced. No period is lost, because the new period is measured from the end of the old one rather than from the day the money arrived.
Expired means grace ran out. The subscription is over, the unpaid renewal order is cancelled so it cannot be paid afterwards, and the customer is told. An expired subscription is not resumable; the customer subscribes again, which starts a fresh period from the day they do it. The store-wide grace default is seven days, a plan can set its own, and the pay link expires when grace does — so a link forwarded three months later is refused with a message rather than taking money for an agreement that has already ended.
The four emails
| When | Setting | |
|---|---|---|
| Renewal reminder | Each rung before the period ends | emails.renewal_reminder |
| Invoice due | The period has ended and the invoice is unpaid | emails.renewal_due |
| Payment failed | A charge failed, or the invoice went past its due date | emails.past_due |
| Subscription ended | Grace ran out and access ended | emails.expired |
All four go through KahunaCart's own translatable email pipeline and carry the pay link. Switching one off stops the message, not the calendar — the subscription still goes past due and still expires on schedule. See Emails.
Note
With the default ladder the past-due email never sends. [14, 3, 0] puts a rung on the day the period ends, and the invoice-due message that rung sends falls at the same instant the subscription is marked past due and carries the same amount, the same pay link and the same date access stops. Remove the 0 rung to get a separate past-due message instead.
Invoice mode settings
| Setting | Default | What it does |
|---|---|---|
invoice.reminder_days |
[14, 3, 0] |
Days before the period ends that a pay link goes out. |
invoice.grace_days |
7 |
How long an unpaid invoice keeps a subscription alive past the period end. |
invoice.max_reminders |
4 |
A ceiling on the schedule above. |
Everything here is whole days in UTC, so a subscription crossing a daylight-saving boundary neither gains nor loses an hour.
Important
The billing sweep queues itself from bin/plugin kahunacart work, at most once every ten minutes. If your scheduler is not running, nothing about invoice mode works. See Troubleshooting → Emails never arrive, jobs pile up.
Renew early, or mark an invoice paid by hand
A customer can pay for the next period before it is due, from their own subscription page: Renew now builds the invoice immediately and takes them to the payment page. The new period is measured from the end of the current one, so paying two weeks early buys a whole period, and pressing the button twice does not raise two invoices.
When money arrives by bank transfer, find the renewal order in Orders and click Mark paid. That is an ordinary order operation: the customer gets their receipt, the ledger records the payment, any licence is extended, and the subscription advances to the next period off the back of it. Resend the pay link queues the same message the calendar sends for an invoice that is already outstanding, and never builds one, so a support call cannot bring a billing period forward by two weeks. See Orders and the admin.
Trials
A plan with Free trial set starts its subscription in trialing, and the first paid period begins where the trial ends. A 14-day trial of a monthly plan buys 14 free days and then a whole month.
In provider mode the trial costs nothing and says so. The origin order is completed and paid with a total of 0, carrying a discount adjustment labelled "Free trial (14 days)". The label is a language key, so it translates like a sale's, and the receipt, the invoice PDF and your reports all agree that no money moved. The first real money arrives as the renewal at the end of the trial.
In invoice mode the origin order is charged at the plan's price, because there is no platform holding a card and nothing to take money later on its own. The trial days then run on top of the period that order paid for, and the first pay link climbs the same reminder ladder against the end of the trial that every later period climbs against its own.
The cart and the checkout page say what is about to happen either way, with a sentence beside the plan line: "Free for the first 14 days, then $49.00 every month." A theme with its own cart or checkout template gets that sentence back by calling kahunacart_cart_line_notes(order, items). A trial never reprices the subscription — the plan line keeps its price, and the frozen amount is what renews.
How a period becomes an order
- The calendar says a period is due — the provider's webhook in provider mode, the billing tick in invoice mode.
- The store builds a renewal order for the plan's renewal variant, running the same calculator as any checkout, so a sale, a coupon, tax and shipping apply exactly as they would to a normal purchase, in the usual minor units.
- Any add-on that cares is asked what to attach to the line before it is built.
- The order is marked paid (provider mode) or left unpaid with a pay link (invoice mode).
- The subscription's period advances, an event is written to the timeline, and the customer gets the receipt your store already sends for every order.
Because step 2 is the ordinary order builder, everything downstream of an order works on a renewal without knowing it is one.
The lifecycle
| Status | What it means |
|---|---|
incomplete |
The first payment has not arrived. Nothing is billed and nothing is granted. |
trialing |
A free trial is running. The first invoice is due when it ends. |
active |
Paid and running. |
past_due |
A payment did not go through. Access continues through the grace period. |
paused |
Billing is stopped. Nothing renews until it is resumed. |
cancelled |
Cancelled. It will not renew again. |
expired |
Ended without being renewed. |
A cancellation scheduled by the customer leaves the subscription running until the end of the period already paid for, and only then moves it to cancelled. Any move not on the list is refused with an error you can read, from the admin and from the API alike — a cancelled subscription cannot be paused, and an expired one cannot be renewed.
Nothing is read from the status alone. Every change writes a timestamped event naming who made it — the customer, the store, the payment provider or the store's own calendar — and that timeline is what answers "why was I charged twice in March" without anybody opening a database.
What the customer sees
The account tab. {base}/account/subscriptions lists everything a signed-in customer is paying for, with the plan, a status chip, the current period, the next date and the amount. It sits beside Orders and Downloads in the account navigation. Subscriptions are matched both by customer record and by email address, so somebody who bought as a guest and created an account afterwards still sees what they bought. See Customer accounts.
The subscription's own page. {base}/subscription/{hash} carries the whole record: the current period, the next date, every order the subscription has produced with links to each receipt and invoice, and the full history. It is addressed by 32 characters of randomness rather than by a login, exactly as a receipt is, so a guest subscriber reaches it from the link in their email. The page is served private, no-store, and its buttons are drawn only when they would work:
| Button | When it appears |
|---|---|
| Cancel this subscription | While it is live. Two-step: a confirmation box has to be ticked. The period already paid for is kept. |
| Keep my subscription | Only while a cancellation is scheduled. Undoes it, with nothing lapsed in between. |
| Update payment method | Only when the provider driver answers a billing portal URL for this subscription. |
| Renew now | Invoice mode only, on a live subscription. |
| Pay now | On an unpaid renewal order that can still be paid. |
In provider mode a cancellation is mirrored to the payment platform as well. Where the platform confirmed it the customer gets a plain confirmation; where it refused or could not be reached, the page says the store has recorded the change and could not confirm it, and asks the customer to get in touch if they are charged again.
Every order that started or renewed a subscription also carries a panel on its receipt and in its order email: which subscription it belongs to, which dates it covers, and a link to the subscription's own page. All of it is Twig partials you can replace by copying them into your theme, and the stylesheet reads the --kc-* custom properties the base plugin already publishes — so a theme that has already been styled for KahunaCart styles the subscription cards for nothing. See Theming.
Refunds
A renewal is an ordinary order, so it refunds like one. What changes is the calendar.
- Refunding a renewal rolls the period back. The subscription returns to the period before the refunded one, and a
period_rolled_backentry is written to the timeline. - Refunding the order that started a subscription cancels it. There is no earlier period to return to.
- Refunding an older period changes no dates. A goodwill refund of the third period of six leaves a customer who has paid through December in December, and the timeline records that the money went back and the period did not move.
Orders and the admin → What a full refund reverses covers what a refund does to the rest of the order.
The admin
Subscriptions appear inside KahunaCart's own admin under Add-ons in the side navigation, beside Orders, Products and Licenses, with two tabs: Subscriptions and Plans.
Permissions
| Permission | What it allows |
|---|---|
kahunacart.subscriptions.view |
Reading. The list, the filters, the detail, the history, the figures, the plans. |
kahunacart.subscriptions.manage |
Everything that changes something: cancel, pause, resume, extend, note, resend a pay link, and every write on the Plans tab. |
A reader without .manage gets the whole screen with the buttons absent and a line saying why. Mark paid additionally needs the base plugin's kahunacart.orders.manage, because marking a renewal paid is an order operation.
The dashboard row
Four figures across the top of the Subscriptions tab: Monthly recurring, with the yearly equivalent underneath; Active, with the trial count underneath; Past due; and Paused.
Monthly recurring revenue is each live subscription's amount divided by the number of months in one billing period. A $120 yearly plan contributes $10, a $30 quarterly plan contributes $10, and a $10 weekly plan contributes $43.33 — 52 weeks and 12 months to the year, and 365 days for a daily plan. active and past_due both count, because both are money the store is contracted to receive; trials are excluded and reported separately, because nothing has been contracted yet. Currencies are never added together, so a store selling in USD and EUR gets two figures with the store's own currency as the headline.
The actions
Every action goes through the same state machine the webhooks and the billing calendar use, and every one writes an entry the customer can see on their own page.
| Action | What it does |
|---|---|
| Cancel at period end | Stops it renewing and leaves the period already paid for intact. Almost always the right button. |
| Cancel now | Ends it immediately. Access stops and the paid period is lost. For a chargeback or a fraudulent signup. |
| Undo the cancellation | Clears a scheduled cancellation. |
| Pause billing / Resume billing | Stops and restarts the calendar without ending the subscription. |
| Extend the period | Pushes the current period out by a number of days, with a required note. Negative pulls it in. Capped at ten years. |
| Resend the pay link | Invoice mode only. Queues the outstanding invoice's email again. |
| Mark paid | On any unpaid order in the subscription's order list. |
Cancelling, un-cancelling, pausing and resuming a provider-mode subscription change the store's record and tell the payment platform. A green toast means both happened, or that there was no platform to tell. An amber toast means the store's record changed and the platform did not confirm it — an agreement the platform still holds will charge the customer again next month, so that is a job to finish at the provider before telling the customer anything. Pausing is the one action that does not go ahead locally when the platform refuses it, because a pause the platform did not accept would stop access while the card went on being charged.
A customer with subscriptions also gets a Subscriptions panel on their admin record, linking straight into the detail here.
The admin API
Everything the screen does, it does through documented endpoints, mounted under the Grav API plugin's base — /api/v1 in a stock install — and authenticated the way every other KahunaCart admin endpoint is.
| Method and path | Permission |
|---|---|
GET /kahunacart/subscriptions, …/metrics, …/{id} |
view |
POST /kahunacart/subscriptions/{id}/ + cancel, uncancel, pause, resume, extend, note, resend-pay-link |
manage |
GET /kahunacart/subscription-plans, …/variants, …/{id} |
view |
POST /kahunacart/subscription-plans, PATCH and DELETE …/{id} |
manage |
PUT /kahunacart/subscription-plans/{id}/provider-refs/{provider} |
manage |
Times are UTC epoch seconds as integers, and an ISO-8601 string is accepted on the way in. A refusal from the state machine is a 422 carrying the reason as a sentence and a machine code in the X-KahunaCart-Error header, so a client can branch on it without parsing English.
Subscriptions and license keys
If you also run the Licenses add-on, a subscription is what turns its manual renewals into automatic ones. Write the licensed product, the renewal product and the renewal rule exactly as Renewals describes, then create a plan on the licensed product's variant and set the plan's renewal variant to the renewal product's variant. That second step is the whole integration.
- The first order mints the key, the way any licensed purchase does, and the key is stamped with the subscription when it activates.
- Every period after that extends the key rather than minting another, because the renewal variant carries a
renewalrule. A subscription holding several keys gets one line per key. - Refunds roll both back. Refunding a period restores the key's dates to what they were before it, and the subscription's period rolls back with it.
- The account tab says how it renews. A key paid for by a subscription reads Renews automatically in provider mode, or names the date the renewal is due in invoice mode, with a link to the subscription's page. The manual Renew link is hidden while the subscription is live and comes back the moment it ends.
Each licensing rule also carries a When a subscription ends setting: nothing (the default) leaves the key alone, so the customer keeps the term they paid for; suspend stops it working while keeping every seat; revoke kills it and frees the seats. It applies when the subscription actually ends, not when the customer clicks cancel.
What is not in this release
- Plan changes and proration. A customer who wants a different plan cancels this one and subscribes to the other, with nothing lapsing in between because the cancellation takes effect at the end of the period already paid for. The account page says so rather than leaving somebody hunting for a button that is not there.
- Charging a stored card on the store's own calendar. The interface is declared in the base plugin and nothing implements it yet, so the choice today is the provider's calendar or an emailed invoice.
- Usage-based and metered billing. Every period bills the plan's amount.
- Creating a price at the provider from the admin. A driver that can create its own price does so on the first checkout and writes the id back to the plan's Provider references. Fill those in by hand only to attach a plan to a price you created at the platform yourself.