Skip to content
Paykit
Esc
↑↓navigate↵open⌘Jpreview
On this page

Subscriptions

The plans an account holds, the period they run for, and the states between a trial and a cancellation.

An account holds one subscription. It records the plans the account bought, the period they are paid for, and a status that says whether the account may still use the product.

The subscription is a fold of the account’s events. Nothing edits it in place: a change is a command that appends an event, and the next read shows the result.

The status

Status What it means May use the product
trialing The account is inside a trial period Yes
active Paid and current Yes
grace A payment failed, and service continues while the provider retries Yes
canceled The subscription ended at its boundary No
expired Grace ran out, or an operator ended the subscription No
none The account has never subscribed No

grace is the deliberate exception. A failed card should not switch off a product the customer is already using, so the account stays in grace while the provider retries, and a check still answers yes. When the grace deadline passes, the subscription moves to expired.

The status answers one question and the balance answers another. A check returns both: the coarse standing (good, grace or blocked) and the allowance as balance.

The plan set

A subscription holds a set of plans, not one plan. Each plan belongs to a group, and a move replaces the plan in its group. A plan with addOn: true stacks beside every group instead of replacing anything.

This is how an account holds Pro and an add-on at the same time, or a base plan and an enterprise plan in another group, without a second subscription.

A plan with isDefault: true is what the account falls back to when a move or a cancellation leaves its group empty.

The period

A subscription runs for a period with a start, an end and an anchorTz. The anchor decides where the month boundary falls, so an account that signed up at 09:00 in Berlin renews at 09:00 in Berlin.

Monthly and annual allowances reset at the period boundary. An allowance with a shorter resets interval, such as a daily one, runs its own window and does not wait for the period.

Move, upgrade, and downgrade

Subscribe is the first purchase. ChangeTier moves a plan that already exists. Both compute what the move costs for the rest of the period, and Preview shows that number before anyone confirms.

const preview = yield* Paykit.Preview(account);
const quote = yield* preview({ command: { kind: "changeTier", tier: "scale" } });

if (quote._tag === "accepted") {
  quote.proration?.due; // minor units. Negative means the account is owed.
}

ChangeTier records the move and the proration, and the provider collects the money in a separate call. A downgrade’s credit is recorded on the subscription and not paid out automatically.

const changeTier = yield* Paykit.ChangeTier(account);
const moved = yield* changeTier({ tier: "scale", dedupeKey: requestId });

if ((moved.proration?.due ?? 0) > 0) {
  yield* (yield* Paykit.ChargeProration(account))();
}

The plan’s version is the generation the account bought. A deploy that changes the plan’s terms bumps the version, and the existing subscription keeps the old one, so a catalog change never reprices history. Moving the account to the new terms is an ordinary plan change.

Schedule a change or a cancellation

A change does not have to happen now. ScheduleTierChange records a phase that applies at a future instant, and ScheduleCancellation ends a group at its boundary. Resume withdraws a pending cancellation before it fires.

const schedule = yield* Paykit.ScheduleCancellation(account);
yield* schedule({ group: "default", dedupeKey: requestId });

The phases live on the subscription in order. Scheduling a phase also schedules the command that applies it, so the move does not wait for the next request.

Trials

A trial is a status, not a plan. StartTrial opens a trial period for a plan, and the account keeps the plan’s allowances while status is trialing. An account gets one trial ever, and a second attempt is refused with TrialAlreadyUsed.

A plan can offer a trial with trial: { days, cardRequired, onEnd }. When the trial ends, bill starts the paid period and revert returns the account to the plan it held before.

What reads it

  • Check returns the status as standing.
  • Balances reset at the period boundary.
  • Paykit.List() pages every account in a status, which is how a console finds the accounts in grace.

See Plans and prices for what a plan grants, and Build a billing page for the customer’s side of a plan change.

Was this page helpful?