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

Plans and prices

A purchasable bundle, the price it sells at, the allowance it grants, and the terms a move replaces.

A plan is a purchasable bundle: one price, a set of grants, and the terms around them. An account holds one plan per group, so a move replaces the plan it leaves behind. See Subscriptions.

import { Paykit } from "@potion.run/paykit";
import { messages } from "./features.ts";

export const pro = Paykit.plan({
  id: "pro",
  name: "Pro",
  price: { amount: 1900, currency: "usd", interval: "month" },
  grants: [Paykit.planItem(messages, { included: 2_000, overage: "bill" })],
});

The price

Money is integer minor units plus a currency. 1900 is $19.00, and no exchange rate is applied: a catalog that sells in EUR states each amount.

interval is month or annual. It sets the billing period, which is also the period every allowance without its own resets follows.

A plan with no price is free. It grants its allowance and takes no payment.

What a plan grants

One planItem states what a plan gives of one feature, and what happens past it. Only included is required:

Paykit.planItem(messages, { included: 100 }); // blocks past 100
Paykit.planItem(messages, { included: 2_000, overage: "bill" }); // bills past 2,000
Paykit.planItem(messages, { included: "unlimited" }); // never runs out
Field What it states
included The count in the feature’s own unit, or "unlimited"
overage block, bill or allow past included. Defaults to block, or allow when unlimited
resets When the allowance returns to included. Defaults to month
resetEvery How many reset intervals between boundaries, for example four hours
cap The most a period may bill past included, so included + cap is the ceiling
carryOver Whether unspent units move into the next window
carryCap The most one window may carry. Defaults to included
unitPrice What one billed unit costs, when the overage is priced
moneyCap The most money this feature may bill in one window
prorate How a quantity change is billed: immediate or period_end

Charge bands

Past the included amount, a plan item can price each unit with one rate, with graduated bands, or with volume bands. Graduated charges each band’s rate for the units inside it. Volume charges every unit at the rate of the band the total lands in.

import { Paykit, pricing } from "@potion.run/paykit";

Paykit.planItem(messages, {
  included: 1_000,
  overage: "bill",
  pricing: pricing({
    kind: "volume",
    currency: "usd",
    bands: [
      { upTo: 10_000, amount: 2, currency: "usd" },
      { upTo: "inf", amount: 1, currency: "usd" },
    ],
  }),
});

The last band must be upTo: "inf". Every billed event records the difference the new total makes, so the invoice is the sum of the events.

Roll over what the month did not spend

A rollover keeps the unspent part of an allowance, at most one window’s worth by default. check() reports the carried units as part of remaining.

Paykit.planItem(messages, {
  included: 2_000,
  overage: "bill",
  carryOver: true,
  carryCap: 2_000,
});

A rollover is its own balance source, so a balance’s breakdown shows what came from the plan and what carried in.

Offer a trial

A plan can offer a trial when an account subscribes to it. The plan’s interval sets the length unless days states one.

export const pro = Paykit.plan({
  id: "pro",
  price: { amount: 1_900, currency: "usd", interval: "month" },
  trial: { days: 14, cardRequired: false, onEnd: "bill" },
  grants: [Paykit.planItem(messages, { included: 2_000 })],
});

onEnd decides what happens when the trial ends. bill starts the paid period, and revert returns the account to the plan it held before. A trial is offered once per account.

Variants, add-ons and groups

A plan can be a named alternative of another. The catalog resolves the variant into a full plan, so the runtime never sees a partial one:

export const proAnnual = Paykit.plan({
  id: "pro_annual",
  variantOf: "pro",
  customize: {
    price: { amount: 19_000, currency: "usd", interval: "annual" },
  },
});

A plan’s group is the set of plans a move replaces within. A plan with addOn: true stacks beside the group’s plan instead of replacing it. A plan with isDefault: true is what an account gets when it has none, and a cancellation re-grants its terms.

A plan that sets custom: true lets a customer hold bespoke allowances under it. A plan that sets version: 2 states a new generation of its terms. A deploy never reprices history: moving the account onto the new terms is an ordinary move, and there is no automatic migration.

One-off packs

A plan with kind: "one_off" sells credits once and never starts a period. It grants no allowance.

export const credits100k = Paykit.plan({
  id: "credits_100k",
  kind: "one_off",
  price: { amount: 4_900, currency: "usd", interval: "month" },
  credits: 100_000,
  grants: [],
});

A one-off pack cannot state a top-up line or a grant. A subscription’s credits expire at the period close by default. Set creditsExpire: "never" for a lifetime grant.

See Credits.

What the deploy checks

The catalog rejects a contradiction before it provisions anything:

  • a cap, unitPrice or moneyCap without overage: "bill"
  • carryCap without carryOver, or a rollover on an allowance that never resets
  • resetEvery on resets: "never"
  • a variant of an unknown plan, or a cycle of variants
  • a grant on a one-off pack, or a one-off pack with no price
  • isDefault on an add-on
  • a plan whose grants are empty

Each is a typed error that names the plan and the field, so the failure arrives at deploy with a line to fix.

Was this page helpful?