Subscriptions
A subscription is the core billing relationship between your business and a customer. It connects a customer to a plan, defines a billing interval, and drives automatic invoice generation.
Subscription model
| Field | Notes |
|---|---|
id, customer_id | Identity + owning customer |
plan_id, plan_version | Pins to a specific plan version, not the live mutable plan |
currency | Authoritative ISO 4217 - every item, invoice, credit note, and proration on this subscription is denominated here. A price resolution that yields a different currency is a hard error |
billing_interval_unit + billing_interval_count | month × 1, year × 1, month × 3 (= quarterly), week × 2, etc. The unit is one of week, month, year |
billing_anchor_day | Day-of-month (1–28) the cycle aligns to |
auto_renew | When false, the subscription does not start a new cycle at term end; the renewal runner stops generating invoices and an operator or scheduled change closes it out via cancel |
current_phase_id | Pointer to the active phase. The phase's phase_kind (setup, trial, standard, paused, dunning) carries the current billing mode while status stays ACTIVE |
dunning_stage (on the active phase) | Populated only while phase_kind = dunning; identifies the sub-stage the dunning runner has reached: WARNED, GRACED, SUSPENDED. Lives on the phase row, not the subscription envelope |
started_at, current_period_start, current_period_end | Period bookkeeping; current_period_end is when the next charge cuts |
cancelled_at | Set when the subscription reaches CANCELLED. Pause/resume and trial/dunning transitions are phase flips that emit events; they do not stamp dedicated status timestamps |
cancellation_reason | On a terminal row, discriminates how it ended: USER_REQUEST, AUTO_NON_RENEWAL, DUNNING_TERMINAL |
payment_terms_days | Per-subscription override of the org's default invoice due-days |
items, custom_fields, metadata | Populated on GET |
Every subscription has one of the following statuses (returned in the status field, uppercase):
| State | Meaning | Transitions to |
|---|---|---|
DRAFT | Created but not yet started; no invoices generated | ACTIVE, CANCELLED |
ACTIVE | Started and billing normally - invoices generated at each period boundary. Trial, paused, and dunning are billing-mode overlays that ride on phase_kind while the status stays ACTIVE | CANCELLED, EXPIRED |
CANCELLED | Ended by customer or system. cancellation_reason records USER_REQUEST, AUTO_NON_RENEWAL, or DUNNING_TERMINAL | Terminal |
EXPIRED | Terminal end-of-life status | Terminal |
These are the only four lifecycle values. There is no TRIALING, PAUSED, or DUNNING status — those are billing modes carried on the active phase's phase_kind (trial / paused / dunning), and the subscription's status remains ACTIVE throughout. See Phases and Dunning.
The state machine is enforced at the service layer (allowedSubscriptionTransitions in internal/subscription/model/subscription.go). Disallowed transitions return a 409 conflict.
Code
A dunning phase carries a dunning_stage that advances WARNED → GRACED → SUSPENDED as the dunning policy escalates. These sub-stages drive access changes and notifications, but the subscription's lifecycle status stays ACTIVE for all three — the progression rides on the phase row and never re-enters the lifecycle FSM.
- A subscription starts in
DRAFTand flips toACTIVEwhen its start trigger fires (immediate, on-checkout-complete, manual, or a start date). - Trials are not a status: a subscription created with a leading
trialphase keepsstatus = ACTIVEand generates no invoices while the trial phase is active. - A payment failure mints a
dunningphase; recovery mints astandardphase. Neither changes the lifecyclestatus. - Pause and resume are phase flips (
phase_kind = paused↔standard) that preserve the subscription configuration; resuming picks up where it left off. The status staysACTIVE. CANCELLEDandEXPIREDare terminal - to re-activate, create a new subscription. Whenauto_renew = false, the renewal runner stops generating invoices at term end and the subscription is closed out viacancelwithcancellation_reason = AUTO_NON_RENEWAL.
Billing intervals
The billing_interval_unit field determines how often invoices are generated. Combined with billing_interval_count, you control the cycle length precisely.
billing_interval_unit | Used for |
|---|---|
week | Short-cycle and trial-adjacent billing |
month | SaaS subscriptions, per-seat pricing; month × 3 for quarterly, enterprise contracts |
year | Annual plans with discounted rates |
There are only three units — week, month, year. Cadences like quarterly are expressed as a unit plus a count (month × 3), not as a distinct unit. The unit defaults to month when omitted.
billing_interval_count (default 1) multiplies the unit - e.g. month × 3 gives a quarterly cadence; year × 2 gives a two-year term. Allowed range: integer >= 1.
By default, billing cycles align to the subscription start date. Set billing_anchor_day (1-28) to align all cycles to a specific day of the month. Values outside 1-28 return 400 validation errors.
Example: A subscription created on January 15 with billing_anchor_day: 1:
- First period: Jan 15 to Feb 1 (prorated)
- Second period: Feb 1 to Mar 1 (full month)
- All subsequent periods start on the 1st
Trials
A trial is a leading phase with phase_kind = trial, not a separate status. Give the customer a free period by making the first entry in the subscription's phases a trial phase with an end policy (a duration or an end date); the next phase (standard) begins billing when the trial phase completes.
During a trial:
- No invoices are generated - a
trialphase does not generate invoices, regardless ofbilling_timing. - Usage events are tracked but not billed.
- The subscription
statusisACTIVEthe whole time; only the active phase'sphase_kindistrial. - When the trial phase ends, the subscription transitions to the next phase (
standard) and the first invoice is generated.
There is no trial_duration_days field. The trial length is the trial phase's own end policy (duration value/unit or end date).
Tip: Combine trials with test clocks in sandbox to simulate trial expiration without waiting.
Proration
Proration adjusts charges when a subscription changes mid-cycle. Kontorion prorates automatically for plan transitions, item-quantity changes, and immediate cancellations.
Plan transition (upgrade / downgrade)
Use POST /v1/subscriptions/{id}/transition-plan to move a subscription to a different plan. The service computes:
- Unused credit for the remaining days on the old plan.
- Charge for the remaining days on the new plan.
- The net delta appears as line items on the next invoice (or as an immediate-charge invoice, depending on plan configuration).
Quantity change
Use POST /v1/subscriptions/{id}/change-quantity with { subscription_item_id, new_quantity } to change the quantity of one item. The response includes both the updated subscription and the proration records produced.
Cancellation
POST /v1/subscriptions/{id}/cancel cancels immediately and produces a prorated credit for the unused portion of the current period (if applicable). The handler accepts no request body. To schedule a cancellation at period-end, use scheduled changes.
Subscription items
Each subscription contains one or more items - the specific products and quantities the customer has access to. Items reference products from the plan and can have their quantities adjusted via change-quantity.
Items are passed at creation:
Code
quantity must be at least 1; product_id is required.
Add-ons
Add-ons are additional products attached to a subscription outside of the base plan. They're billed alongside regular subscription charges and appear as separate invoice line items. Manage them via POST /v1/subscriptions/{id}/add-ons (attach) and DELETE /v1/subscriptions/{id}/add-ons/{addonId} (remove).
Common workflows
Create a subscription with a trial
A trial is the first phase. Make it a trial phase with an end policy (here a 14-day duration that auto-transitions into the standard billing phase):
Code
Returns 201 Created with the new subscription.
Change the plan (upgrade or downgrade)
Code
Change item quantity
Code
Cancel
Code
Returns 200 OK with the cancelled subscription. No request body.
Pause and resume
Pause and resume are phase flips: pause mints a phase_kind = paused phase and resume returns to standard. The lifecycle status stays ACTIVE throughout; no invoices are generated while paused.
Code
Topup (mid-cycle usage credits)
POST /v1/subscriptions/{id}/topup adds extra units of a metered product mid-cycle - the customer paid for an extra 5,000 calls, you credit them as allowance:
Code
Topups apply against the current period's allowance bucket; usage events draw against the topped-up balance before the customer hits standard usage rates.
Allowance state
GET /v1/subscriptions/{id}/allowance returns the current period's allowance state per product - included quantity, consumed, remaining, rolled-over, expiring. The same data the platform uses to bill. Cheap to call from a customer-facing dashboard.
Endpoints
All Subscriptions endpoints - covers the full surface above plus /simulate for dry-running any planned change.
Related
- Plans - what subscriptions pin to (and the migration flow for moving cohorts of subscribers between plan versions)
- Customers - owning entity
- Invoices - what gets generated at every period boundary
- Usage metering - feeds quantity into metered items (
quantity_source = METERED) - Dunning - drives
dunning_stageWARNED → GRACED → SUSPENDED on adunningphase while the subscription'sstatusstaysACTIVE - Promotions - phased promotions ride a subscription's phase machine
- Scheduled changes - defer a transition to a future date