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 + billing_interval_count | monthly × 1, yearly × 1, monthly × 3 (= quarterly), one_time, etc. |
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; an operator or scheduled change must close it out via cancel |
trial_duration_days | When set, the subscription starts in TRIALING until the trial elapses |
started_at, current_period_start, current_period_end | Period bookkeeping; current_period_end is when the next charge cuts |
cancelled_at, paused_at, resumed_at | Lifecycle transition timestamps maintained by the state machine |
dunning_stage | While status = DUNNING, identifies the sub-stage the dunning runner has reached: WARNED, GRACED, SUSPENDED |
current_phase_id | Pointer to the active phase - null only on terminal subscriptions |
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 |
|---|---|---|
TRIALING | Free trial period - no charges generated | ACTIVE, CANCELLED |
ACTIVE | Billing normally - invoices generated at each period boundary | PAUSED, DUNNING, CANCELLED |
PAUSED | Customer paused - no invoices generated | ACTIVE, CANCELLED |
DUNNING | Payment failed; the dunning runner is escalating through warn → grace → suspend stages | ACTIVE (recovered), CANCELLED |
CANCELLED | Ended by customer or system | Terminal |
The state machine is enforced at the service layer (allowedSubscriptionTransitions in internal/domain/subscription.go). Disallowed transitions return 409 INVALID_TRANSITION.
Code
While in DUNNING, the subscription's dunning_stage field cycles through WARNED → GRACED → SUSPENDED as the dunning policy escalates. The top-level status stays DUNNING for all three; the substages drive access changes and notifications without a status flip on every step.
- A subscription only enters
TRIALINGwhen created withtrial_duration_days > 0. DUNNINGis entered automatically when an invoice payment fails.PAUSEDpreserves the subscription configuration; resuming picks up where it left off.CANCELLEDis terminal - to re-activate, create a new subscription.auto_renew = falsedoes not produce a separate terminal state; the runner stops generating invoices and an operator (or a scheduled change) closes the subscription out viacancel.
Billing intervals
The billing_interval field determines how often invoices are generated. Combined with billing_interval_count, you control the cycle length precisely.
billing_interval | Used for |
|---|---|
monthly | SaaS subscriptions, per-seat pricing |
quarterly | Seasonal billing, enterprise contracts |
yearly | Annual plans with discounted rates |
one_time | Single-charge plans (no auto-renew) |
billing_interval_count (default 1) multiplies the interval - e.g. monthly with count 3 gives quarterly billing. Allowed range: integer >= 1.
billing_interval_count multiplies the base interval - monthly × 3 gives a quarterly cadence; yearly × 2 gives a two-year term.
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
Set trial_duration_days when creating a subscription to give the customer a free period before billing begins.
During a trial:
- No invoices are generated.
- Usage events are tracked but not billed.
- The
statusisTRIALING. - When the trial ends, the subscription transitions to
ACTIVEand the first invoice is generated.
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
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
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
USAGE-typed items - Dunning - drives
dunning_stageWARNED → GRACED → SUSPENDED while the subscription stays inDUNNING - Promotions - phased promotions ride a subscription's phase machine
- Scheduled changes - defer a transition to a future date