Base 3:16
Concepts

Billing

Invoices, terms, collection and what happens when a payment fails.

What an invoice is made of

subtotal        the plan fee, usage past the allowance, addons, one-off charges
− discountTotal a coupon, if one applies
+ taxTotal      applicable rates
= total

and separately:

amountDue = total − amountPaid − creditedTotal

amountDue is derived, never stored, so it cannot drift out of step with the payments and credit notes it is computed from.

How a usage line was worked out

Every usage line records what it was rated against: the plan's included allowance (scaled by quantity for a per-unit allowance) and the prepaid credits that covered usage past it. The invoice page in the dashboard writes the working out with the line's own numbers:

1,500 used − (1,000 included + 200 credits) = 300 billable

That is the rule to quote when a bill is disputed: only usage past the allowance and credits is billed, so nothing check reported as inside the allowance is charged. An unlimited entitlement records no allowance and bills nothing. Lines rated before lines began recording these stored only the usage and the billable quantity, so for those the page shows what the allowance and credits covered together (used − billable) rather than splitting it from a plan that may have changed since.

Addons with included units

A PER_UNIT addon can include units before it charges: includedQuantity: 10 bills max(0, quantity − 10) × amount, so "10 clients included, then $12 each" is one addon. The subscription carries the real quantity; the invoice line shows it with what was included and what was billed (11 − 10 included = 1 billed). FLAT addons ignore quantity and cannot include units. The managed tenants guide covers the per-client case.

Overage: soft limits and hard caps

A metered feature with a usage price on the plan version (PER_UNIT, MATRIX, a tiered model or PACKAGE carrying the feature's id) has a soft limit: check keeps allowing usage past limit + credits, says so with usage.overage: true, and the billing run charges that usage at the price. With such a price, limit: 0 bills every unit. A feature with no usage price — or only a FLAT one, which does not charge per unit — keeps a hard limit: check refuses at limit + credits with USAGE_LIMIT_EXCEEDED, exactly as before.

An entitlement can also carry a hard cap (hardCap), the ceiling on what a customer can run up. check refuses once usage would pass it, with HARD_CAP_REACHED. It is scaled by quantity like the limit when limitPerUnit is set, and credits never raise it. It needs a numeric limit and cannot be below it.

The cap governs check, not ingestion: an event your application records past the cap is still metered and billed, like any other usage. Base316 cannot know whether the work behind an event happened, and refusing to record work that did would under-bill silently.

Per-model pricing (MATRIX)

When the same feature costs a different amount depending on what the event carries — tokens from a large model cost more than tokens from a small one, a request in one region more than in another — price it with a MATRIX price: a unit price per value of one or more of the meter's groupBy dimensions. You keep one feature, one meter and one allowance, rather than a feature per model kept in step by hand.

{
	"model": "MATRIX",
	"featureId": "feat_llm_tokens",
	"amount": "0.000002",
	"rows": [
		{ "dimensions": { "model": "opus" }, "unitAmount": "0.000015" },
		{ "dimensions": { "model": "sonnet" }, "unitAmount": "0.000003" },
		{ "dimensions": { "model": "haiku" }, "unitAmount": "0.000001" }
	]
}

The rules:

  • Rows name the meter's dimensions. Every key in a row's dimensions must be in the meter's groupBy, and every row names the same keys — so an event matches at most one row, whatever order the rows are in. A row may name several keys ({ "model": "opus", "region": "eu" }). Values are compared as the text the event carries.
  • amount is the default row. Usage whose values no row names is charged at it, on one line called "other values". Leave it out and that usage is unpriced: it is listed on the invoice — quantity, billable units, the values it carried — with unpriced: true and an amount of zero, flagged on the invoice page, the PDF, the upcoming-invoice preview and the customer's entitlement panel. It is never silently free. When new models can appear without warning, set a default.
  • Per-unit rows. Each row is a unit price with at most eight decimal places (a price per million tokens divided by a million: $3 per million is 0.000003). Tiers or package sizes per row are not supported: a row's price would then depend on how much of it the allowance covered, and the allowance rule below could no longer promise the lowest bill.
  • SUM or COUNT meters only. The allowance is checked by check against the feature's total. Only for an additive aggregation do the rows add up to that total, so only then can the allowance be split across rows without billing anything check called included. A MAX, MIN, LAST or UNIQUE_COUNT meter is refused.
  • The allowance covers the dearest usage first. The plan's included allowance (limit), then prepaid credits, are applied to the rows in descending unit price — table order breaking a tie, unpriced usage last. Each unit is billed or covered exactly once, so the billable units across the lines equal used − (limit + credits) — exactly what check reports as billableOverage — and no other split would charge the customer less. limit: null is unlimited and bills nothing; limit: 0 bills every unit.
  • A matrix makes the limit soft, like any usage price.
  • One line per row with usage, each rounded once, to the currency (roundToCurrency), so ¥ and KWD come out in whole yen and fils. Each line carries its dimensions ({} for the default row) and its own share of the allowance and credits, so the working on the invoice page reads per row: quantity − (allowance + credits) = billable.

A table that cannot be rated is refused when the plan or version is written, with error.param naming the row: prices[0].rows[2].dimensions.tier.

Worked example: AI tokens

A meter llm_tokens sums tokens from llm.completion events and groups by model. The Pro plan includes 1,500,000 tokens a month and prices the matrix above (opus $0.000015, sonnet $0.000003, haiku $0.000001 a token, anything else $0.000002). In one period a customer sends:

modeltokens
opus1,000,000
sonnet1,000,000
haiku1,000,000
llama-405500,000

The 1,500,000 included tokens cover the dearest first: all of opus, then half of sonnet. The invoice has four usage lines:

lineusedincludedbillableunitamount
LLM tokens — model: opus1,000,0001,000,00000.000015$0.00
… model: sonnet1,000,000500,000500,0000.000003$1.50
… model: haiku1,000,00001,000,0000.000001$1.00
… other values500,0000500,0000.000002$1.00

2,000,000 billable tokens, which is 3,500,000 used − 1,500,000 included — the same number check reported — for $3.50. Without the default row the llama line would read unpriced, with 500,000 billable and $0.00, and the total would be $2.50.

Send the dimension on every event:

await base316.events.record({
	customerId,
	event: "llm.completion",
	idempotencyKey: completion.id,
	payload: { tokens: usage.output_tokens, model: "opus" },
});

The invoice page

Billing → Invoices & payments lists every invoice, filterable by status — including Overdue, payable invoices past their due date — and searchable by invoice number or the customer's name, email or your own id. The filter lives in the URL, so a filtered list can be shared.

Each number opens the invoice: header and amounts, the lines with their working, discounts, tax, wallet applied, payments (method, reference, and any reversal or declined attempt), credit notes against it, and a timeline of what happened — created, finalized, emails sent about it, pay links, payment attempts, credit notes, write-off, grace period end, paid or voided — built from the invoice's own records and the audit log. Actions that do not apply stay visible and say why: only a draft can be finalized, and a paid invoice cannot be voided (issue a credit note instead).

Fee in advance, usage in arrears

The recurring fee is billed in advance — it pays for access about to begin. Usage is billed in arrears, because it can only be rated once it has happened.

So a renewal invoice carries two things: the usage for the period that just closed, and the fee for the one opening.

The upcoming invoice

GET /v1/subscriptions/:id/upcoming-invoice answers "what will this subscription's next invoice be?" It runs the billing run's own arithmetic — the same draft, the same discount and tax, the same credit and wallet rules — and writes nothing: no invoice row, no invoice number, no charge marked billed, no credit or discount period spent, no wallet movement and no webhook. Call it as often as a screen refreshes; asking twice gives the same answer.

const next = await base316.subscriptions.upcomingInvoice("sub_...");
// next.issuesAt, next.lines, next.total, next.amountDue

Which invoice it previews follows what the billing run will actually do next:

reasonWhenWhat is on it
RENEWALAt the period endUsage for the current period, plus the fee for the next one
FINALAt the period end, when cancelling thereUsage only — no further period opens, so no fee is charged
TRIAL_ENDWhen a trial convertsThe first paid period's fee; usage during the trial is free

A reverse trial whose payer has no card raises nothing when the trial ends, so its preview is the downgrade plan's first renewal, with planVersionId naming that plan.

  • Usage so far is included, and generatedAt says when it was read. Usage between then and issuesAt still adds to the bill.
  • One-off addons and pending charges are included. They wait for the next invoice, so a "next bill" without them would under-report it. Previewing never consumes them; only raising the invoice does.
  • Discount, then tax, then wallet, exactly as at finalization: subtotal − discountTotal + taxTotal = total, and amountDue = total − walletApplied. creditsApplied lists the prepaid credit the usage lines will draw, per grant.
  • willBeIssued: false means the invoice would have no lines. The billing run raises nothing then — a free plan does not collect zero-value bills.
  • A canceled, paused or incomplete subscription answers NOT_FOUND: nothing will bill it again.

For the signed-in end user's own bill, GET /v1/me/subscription/upcoming-invoice takes the publishable key and their session, and <UpcomingInvoice /> in @base316/react renders it. A managed tenant's members are refused with billed_by_parent: the firm's invoice lists every client's usage.

Plan versions: draft, then published

A subscription points at the plan version it was sold against, so a published version never changes — repricing is a new version, and existing subscribers keep the terms they agreed to. A new version can start as a draft instead:

StatusTermsCan be subscribed toDeleted
DRAFTEditable in placeNoYes — discard it
PUBLISHEDImmutableYesNever
ARCHIVEDImmutableNoNever
  • Nothing sells a draft. Subscribing, checkout, quotes, plan changes, scheduled phases and ramps, the public catalog and <PricingTable />, the CSV import, a trial's downgrade target and promotion to Production all take published versions only.
  • One draft per plan. Starting a second is a 409 naming the open one: edit it, publish it or discard it first.
  • Publishing freezes it. From then on it is exactly as immutable as a version created published. Publishing is idempotent. If a published version was added above the draft while it was open, the draft takes the next number, so versions are numbered in the order they went on sale.
  • A hard cap rides along. Sending entitlements to a draft edit replaces the list, but an entry that leaves hardCap out keeps the draft's current cap for that feature; hardCap: null removes it. Caps are checked as at creation: a cap needs a numeric limit and cannot be below it (INVALID_REQUEST, param: entitlements[n].hardCap).
  • Leaving publish out publishes, as it always has: createPlan, createVersion and POST /v1/plans/:id/versions only make a draft when asked with publish: false.
const { planVersionId } = await base316.catalog.plans.createVersion(plan.id, {
	currency: "USD",
	interval: "MONTH",
	prices: [{ model: "FLAT", amount: "29.00" }],
	publish: false,
});

// Change it as often as you like — nobody is on it yet.
await base316.catalog.plans.updateDraft(plan.id, planVersionId, {
	prices: [{ model: "FLAT", amount: "25.00" }],
	trialDays: 14,
});

await base316.catalog.plans.publishVersion(plan.id, planVersionId);
// or: await base316.catalog.plans.deleteDraft(plan.id, planVersionId);

Every version in GET /v1/plans and GET /v1/plans/:id carries subscriberCount: its TRIALING, ACTIVE and PAST_DUE subscriptions. Canceled and paused ones are not counted, so the number is who would be affected if you stopped selling that version — not everyone who ever bought it. In the dashboard, Catalog → Plans lists each plan's versions with their status and subscriber count; Edit on a plan starts a draft (or reopens the open one), and the draft's row has Publish and Discard draft.

Duplicating a plan

To make a variant of a plan — a yearly price, a bigger limit, another trial — use Duplicate in the plan's menu under Catalog → Plans. It asks only for a new key and name, then creates a new plan whose version 1 is a draft holding everything the source's current version says: prices (including tiers, packages and per-model tables), entitlements with their caps and reset rules, the trial and what it ends in, the interval, and the API rate limits a subscriber gets. The description, product, tax code and propagation setting come across too.

The copy opens in the plan editor straight away. Nothing is for sale until you publish it, so the two plans never sell identical terms by accident. Subscribers, the source's older versions and its migration history are not copied: the new plan starts at version 1 with nobody on it. The source is the latest published version, or the draft for a plan that has never been published.

Changing plan: now, or at the period end

A plan change happens now or at the period end, and the preview says what each would do before anything is changed:

const preview = await base316.subscriptions.previewPlanChange("sub_...", {
	planVersionId: "pv_basic",
});
// preview.direction        "DOWNGRADE"
// preview.defaultTiming    "PERIOD_END"
// preview.entitlements     what is GAINED, LOST, INCREASED or DECREASED, limits before and after
// preview.now              credit for the unused fee, usage so far, the new plan's first charge, net
// preview.atPeriodEnd      when it takes effect, and the renewal invoice on the new plan

await base316.subscriptions.changePlan("sub_...", { planVersionId: "pv_basic" });
TimingWhat happens
NOWThe subscription is closed and a replacement, with a new id, opens on the new plan. The unused share of the fee and addons already paid is credited, usage so far is invoiced on the old plan, and the new plan's first period is invoiced in advance.
PERIOD_ENDNothing changes until the renewal. The same subscription then renews onto the new plan: the closing period's usage is rated on the old plan's allowances, the new period's fee on the new plan.
  • Without a timing, a downgrade waits and anything else happens now. Direction is decided by the recurring fee per day at the subscription's quantity, so monthly and yearly plans compare fairly. During a trial a change always happens now — it costs nothing — and "at period end" is refused.
  • One schedule at a time. A new change, now or later, replaces whatever was scheduled. DELETE /v1/subscriptions/:id/phases withdraws it.
  • A cancellation and a scheduled change never stand together. Cancelling withdraws the schedule — leaving is always allowed — and scheduling is refused with 409 while a cancellation is pending: resume first (POST /v1/subscriptions/:id/resume).
  • Same currency. A scheduled change is billed on the same renewal invoice as the period before it, so the new plan must share the currency. Change now to move currency.
  • Only the fee and addons are prorated, never usage.

End users can do the same for themselves: POST /v1/me/subscription/change takes the same optional timing (a downgrade is scheduled by default and needs no card), GET /v1/me/subscription/plan-change-preview previews it, and DELETE /v1/me/subscription/scheduled-change undoes it. GET /v1/me/subscription returns the pending schedule. A managed tenant's members are refused with billed_by_parent.

Ramps

A ramp is a schedule of phases: $500 a month for six months, then $800. Each phase names a plan version and the date it starts from, and takes effect at the first renewal on or after that date — never mid-period.

await base316.subscriptions.setPhases("sub_...", [
	{ planVersionId: "pv_growth_800", startsAt: "2027-04-01" },
	{ planVersionId: "pv_scale_1200", startsAt: "2027-10-01" },
]);

A period-end plan change is a ramp of one phase; the two share the machinery and the rules above. Setting phases replaces the pending schedule, and an empty list clears it. Dates must be in the future and increasing, consecutive phases must change the plan, and two phases that would land on the same renewal are refused because only one could ever apply. A move to a yearly plan changes where every later renewal falls, and effectiveAt on each phase accounts for it. A phase set during a trial counts from the first renewal after the trial converts.

Changing the quantity

A subscription's quantity is what a per-unit price multiplies — seats, devices, agents. Changing it mid-period is a question of money, so preview it first:

const quote = await base316.subscriptions.previewQuantity("sub_...", { quantity: 12 });
// quote.proratedAmount, quote.taxTotal, quote.total, quote.newRecurringAmount, quote.effectiveAt

const changed = await base316.subscriptions.setQuantity(
	"sub_...",
	{ quantity: 12 },
	{ idempotencyKey: "seats-acme-12" },
);
// changed.quantityChange.invoiceId

The preview is the change's own planning step with the writes left out, so the figure it shows is the figure the invoice is raised for.

DirectionDefaultprorationeffective
IncreaseNow, invoiced for the rest of the periodinvoice_now (default), next_invoice, noneimmediately (default), period_end
DecreaseAt the period end, nothing creditedRefused — a decrease is not creditedperiod_end (default), immediately
  • Only the recurring per-unit fee is prorated. Usage never is — it is measured over the real window — and a FLAT fee does not depend on the count. The plan is rated at the new count and at the old one and the difference is scaled by the fraction of the period left, so volume tiers stay honest.
  • The fraction is the time left over the interval the period started on, clamped to [0, 1], and each amount is rounded once in the currency's own scale: 2 seats at $10 with 11 of 31 days left is $7.10; 3 at ¥1,000 is ¥1,065.
  • invoice_now raises an invoice at once, finalizes it with tax (but not the subscription's standing discount, which is spent on the period's own invoice), applies the wallet and attempts collection — an account on terms is invoiced, never charged. A decline leaves it open for dunning; the seats are not taken back. next_invoice adds the amount to the renewal as a pending charge. none charges nothing until the renewal bills the new count in full.
  • A decrease waits for the period end because the period was paid for at the current count, and shrinking it would also shrink a per-unit allowance already bought. It shows as scheduledQuantity; the renewal bills the next period at it, and the upcoming invoice already does. Asking for the current count again cancels it.
  • The plan's seat limit is enforced first. If the plan grants the environment's seat feature (seats unless you changed it in the authentication policy) with a limit, a larger count is refused before anything is charged. Credits extend the limit, as they do for check().
  • Retrying is safe. The request names the target count, so a repeat finds nothing to change; two changes racing from the same count cannot both charge; and Idempotency-Key replays the first response.

A signed-in end user changes their own count with GET /v1/me/subscription/quantity/preview and POST /v1/me/subscription/quantity, or <SeatManager /> from @base316/react. They choose only the number — the defaults always apply, because letting a browser choose none would make seats free. A managed tenant's members are refused with billed_by_parent.

Custom prices for one subscription

Negotiated pricing — "400 a month instead of 500", "0.002 a call instead of 0.003" — goes on the subscription, not into a new plan. The plan version is left exactly as every other subscriber has it; plan versions never change once published.

POST /v1/subscriptions/{id}/price-overrides
{ "priceId": "price_…", "model": "FLAT", "amount": "400.00", "note": "MSA 2026-14" }

A custom price has the same shape as a plan price — model, amount, packageSize, tiers, all decimal strings — in the subscription's currency, and targets either:

  • priceId: one price on the version the subscription is on — the recurring fee or a usage price; or
  • featureId: that feature's usage price on whatever version the subscription is on. Use this when the custom rate should outlive a plan change.

When it applies. To an invoice line when the custom price is in effect at the start of the window that line bills: the period a recurring fee buys, the period usage was measured over, the rest of the period a seat increase is prorated for. It never splits a window, so startsAt in the middle of a period first applies to the next one. With no startsAt it applies from the start — including the current period's usage, which has not been invoiced yet. endsAt stops it the same way.

Every bill and every preview agrees. The renewal, a subscription's first invoice, the upcoming invoice, both halves of the plan-change preview and a seat proration all resolve the price through one function, so a preview cannot show a different rate from the bill. The upcoming invoice marks each line rated on a custom price with priceOverrideId, and so does the issued invoice line.

A plan change ends it, unless the new version still has the price it overrides. A price id belongs to one version, so a price-targeted custom price always ends; a feature-targeted one carries across when the new version prices the feature. It ends at the moment of the change — the closing period is still billed on it — and the plan-change preview lists what would end in customPricesEnding, which the dashboard shows as a warning before anyone confirms.

  • A MATRIX price cannot be customised (400, MATRIX_NOT_SUPPORTED), by priceId or by featureId, and a custom price cannot itself be a MATRIX. A matrix's per-dimension rates share one allowance, and a single custom rate cannot stand in for that table; publish a plan version with the negotiated rows instead. A fee override on a plan with a matrix usage price works as usual — the matrix lines are rated on their own rows. A feature-targeted custom price ends at a plan change onto a version that prices the feature by MATRIX, and the plan-change preview lists it.
  • Two custom prices for the same price cannot overlap in time (409).
  • Terms that cannot rate are refused: a tier table needs exactly one open-ended band, a PACKAGE needs packageSize.
  • A coupon on the subscription still applies on top: the custom price changes what a line costs; the coupon takes a share off the invoice.
  • Removing one (DELETE /v1/subscriptions/{id}/price-overrides/{overrideId}) returns every line not yet invoiced to the plan price. Issued invoices are untouched.

On the dashboard, the subscription page's Custom pricing section lists each plan price with any custom price beside it, and adds, edits and removes them; the Next invoice panel beside it updates as you save.

Contract terms: PO number, memo and billing day

PATCH /v1/subscriptions/{id}/contract
{ "purchaseOrderNumber": "PO-1001", "invoiceMemo": "Pay by wire to …", "billingAnchorDay": 1 }

Each field is optional; null (or an empty string) clears it. All three can also be set when the subscription is created (POST /v1/subscriptions).

PO number and memo are printed on every invoice the subscription raises — renewals, the first invoice, seat prorations — and on its PDF: the PO among the invoice details, the memo after the totals. They are copied onto the invoice when it is generated (purchaseOrderNumber, memo), so changing them later never rewrites a document already sent.

Billing day (billingAnchorDay, 1–31) fixes the day of the month every period ends on. A month shorter than the day ends on its last day — an anchor of 31 bills on 28 February and on 31 March, never drifting to the 28th. Only monthly and yearly plans can be anchored; a daily or weekly one is refused.

The stub rule. Changing the billing day never touches the period already paid for. That period runs to its end as agreed; the period that opens there runs only to the next billing day — the stub — and the renewal bills its recurring fee prorated. The fraction is the stub's share of the full anchored period it ends: a subscription renewing on 10 February that moves to the 1st has a stub to 1 March, 19 of February's 28 days, so a 100.00 fee bills 67.86, ¥10,000 bills ¥6,786 and KWD 30.000 bills KWD 20.357, each rounded once in the currency's own scale. Only the recurring fee is prorated; usage is billed for the window it was measured over. Every period after the stub is a full one.

A subscription created with a billing day starts with the same stub: from its start to the next billing day, prorated. An immediate plan change keeps the billing day, so the first period on the new plan runs to it as a stub too.

Preview first — GET /v1/subscriptions/{id}/contract/billing-anchor-preview?billingAnchorDay=1 returns the stub (nextPeriod, stub, prorationFactor), its fee and a full period's (nextPeriodFee, fullPeriodFee) and the renewal invoice it lands on, from the same function the renewal bills with. The dashboard's Contract section shows that preview as soon as a different day is picked, before saving.

Trials, and the reverse trial

A plan version's trialDays opens every new subscription to it as TRIALING. Nothing is invoiced during a trial. When it runs out, the version's trial-end action decides what happens:

trialEndActionNo payment method on fileA card on file
CONVERT (default)ACTIVE on this plan, first period billed (and chased by dunning if unpaid)Same, and charged
DOWNGRADEMoved to trialDowngradePlanVersionId, ACTIVE, no invoiceConverts and bills, as CONVERT

A trial cancelled before it ends simply ends. Whatever the trial-end action, a TRIALING subscription with a cancellation scheduled becomes CANCELED when the trial runs out: no conversion, no downgrade and no invoice. subscription.canceled arrives with reason: "trial_end", and the upcoming invoice for it is NOT_FOUND.

DOWNGRADE is the reverse trial: every signup gets the paid plan for two weeks without a card, and whoever has not added one by the end lands on Free rather than on an invoice that can only go past due. The subscription keeps its id; subscription.updated arrives with change: "trial_downgraded".

const free = await base316.catalog.plans.currentVersion("free");

await base316.catalog.plans.create({
	key: "team",
	name: "Team",
	currency: "USD",
	interval: "MONTH",
	trialDays: 14,
	trialEndAction: "DOWNGRADE",
	trialDowngradePlanVersionId: free!.id,
	prices: [{ model: "FLAT", amount: "49.00" }],
});

The downgrade target must be a published version of another plan in the same environment, in the same product (when the plan belongs to one) and the same currency. Plan versions are immutable, so the setting is part of each version: publish a new version to change it.

"A card on file" means the payer has a default payment method that has not expired — for a managed tenant, that is its parent's account. An account invoiced on terms (auto-collection off) never needs a card, so it converts.

The downgrade plan bills from its first renewal like any other plan; pick a free version unless you mean the first period after a trial to be free.

What the pricing pages say

<PricingTable /> from @base316/react and the hosted checkout page both read the published version's trialEndAction and say what the trial turns into:

WhereCONVERTDOWNGRADE (downgrading to Free)
PricingTable14-day free trial14-day free trial, then Free unless you add a card
Hosted checkout14-day free trial. You will not be charged today.14-day free trial. You will not be charged today. When it ends, Team is billed to the card on file; with no card on file you move to Free instead.

PricingTable names the target when it is among the published plans it shows; when it is not, it says "no card needed. Add one to keep Team" instead. A pricing page of your own can read the same fields from GET /v1/public/plans (trialEndAction, trialDowngradePlanVersionId). The CLI's base316 plans shows them as the TRIAL, TRIAL END and DOWNGRADE TO columns.

Starting a trial without checkout

Checkout is not the only way in. POST /v1/subscriptions opens a subscription to a version with a trial as TRIALING, with no card, and takes trialDays to override the version's own length — 0 for none, for a returning customer:

await base316.subscriptions.create({
	externalId: newOrg.id,
	planVersionId: team.id,
	trialDays: 14,
});

The dashboard's New subscription dialog takes the same two overrides: Trial days (blank uses the plan's, 0 means none, at most 365) and Starts, the start of the first period (blank is now).

Charging the first period now

A paid subscription's first invoice is raised at once and charged by the hourly collection run. To charge it before the call returns — ending a trial and starting the paid plan while the customer watches — pass collect: true:

const subscription = await base316.subscriptions.create(
	{ customerId, planVersionId: pro.id, trialDays: 0, collect: true },
	{ idempotencyKey: `upgrade-${customerId}` },
);

if (subscription.latestInvoice?.status !== "PAID") {
	// No card on file, or it was declined: ask for one.
}

It is the collection run's own charge against the customer's default card, so a missing card leaves the invoice OPEN, and a decline makes the subscription PAST_DUE, sends payment.failed and starts dunning — the call itself still succeeds. collect is ignored when the subscription starts TRIALING. With an idempotency key a retried call returns the first response and charges nothing.

Draft, then open

generateInvoice stops at DRAFT so a mistaken invoice can be discarded without burning a number. Finalizing makes it OPEN — owed, with a due date, and its line items stop changing.

Invoice numbers are gapless within an environment. A voided invoice keeps its number rather than releasing it: a missing number is indistinguishable from a hidden one during an audit.

Collection, and accounts on terms

By default Base316 charges a stored card automatically.

Set auto-collection off for an account that pays on terms — wire, cheque, purchase order. Nothing will attempt a card it was never given, and dunning will not chase it. You record the payment when it arrives.

Payment terms are per customer. An account on net-30 gets a due date thirty days out; an account with none gets the default of fourteen. This matters because the accounts-receivable ageing buckets by due date, and treating a net-30 account as net-14 puts it in a collections queue two weeks early.

Recording a payment that arrived outside Stripe

POST /v1/invoices/:id/payments
{ "amount": "500.00", "method": "BANK_TRANSFER", "reference": "WIRE-9912" }
  • Part payments work. $500 against a $2,000 invoice leaves it open for the rest.
  • receivedAt is when the money cleared, not when you typed it in. The ageing reflects the cleared date.
  • Overpayment is refused, with the amount actually owed. Base316 cannot know whether the surplus is a duplicate wire or the next invoice paid early, and swallowing it would lose money that belongs to your customer.
  • A bounced cheque is reversed, not deleted. The invoice reopens and both the receipt and its reversal stay in the ledger.

The dashboard's Payments ledger, under Billing → Invoices & payments, lists every payment with its status: Received (the only one that counts towards what an invoice is paid), Pending (not yet sent to Stripe), Processing (accepted by Stripe and still settling), Failed (a card charge that did not go through, with Stripe's code) and Reversed (an offline payment backed out).

Take card on an open invoice mints a link and QR code for the customer to pay on their phone. The dialog also lists every link minted for that invoice — Active, Expired, Voided, or Invoice settled once nothing is owed — and Void withdraws one. Only the link just minted can be copied: a link's address is stored as a digest, so it cannot be shown again. Over the API, the same list and withdrawal are GET /v1/invoices/:id/payment-links and DELETE /v1/invoices/:id/payment-links/:linkId.

Wallet reasons

Every wallet movement carries a reason. A top-up made by hand is a Purchase (the default), Promotional, a Refund back to balance, or an Adjustment; a manual debit is an Adjustment (the default) or an Expiry. Invoice payment is written only when finalization draws the wallet down, and cannot be entered by hand. The customer screen shows recent movements with their reasons; over the API, pass reason to POST /v1/wallets/credit or /debit.

When a card fails

Dunning retries on a schedule and gives up rather than retrying for ever. It skips accounts with auto-collection off — chasing a card a B2B customer never gave is noise, and pausing a subscription that is being paid perfectly well by wire is worse.

What the customer experiences, with the defaults:

WhenSubscriptionCustomer is emailed
A card is declinedPAST_DUE — still entitled"Your payment didn't go through", with the next retry date and a pay link
Retries on days 1, 3 and 7PAST_DUEthe same, after each decline
Retries are exhaustedPAST_DUE for a 7-day grace period"Action needed", with the date access ends
Grace ends unpaidPAUSED (or your end action), invoice UNCOLLECTIBLE"Your subscription is paused", with a pay link

A hard decline — a stolen card, a closed account — skips the remaining retries and goes straight to the grace period. Paying at any point ends it all: a payment during retries or grace returns the subscription to ACTIVE, and so does paying the written-off invoice after a pause, which also starts a fresh period rather than billing the months without access. A cancellation is final.

Every part is set per environment under Settings → Payments & failed payments → Failed payments, or with base316.dunning.update(): the retry days, the grace period (0 acts at once), the end action (pause, cancel, or write off and keep the subscription), and whether Base316 sends the emails.

The emails need a verified sending domain — Base316 does not send billing mail about your customer's bill from base316.com. A template of your own for payment.failed or invoice.uncollectible replaces the matching default, and those templates now reach the customer: billing events name a customer rather than an address, and the address is looked up. Advancing a sandbox's test clock past a grace period applies its end action, so the whole sequence can be rehearsed.

Coupons and tax rates

A coupon is PERCENTAGE (percentOff, at most 100) or FIXED (amountOff and currency). A fixed amount only ever comes off an invoice in its own currency: "10 off" is a different sum of money in dollars, yen and dinar, so a fixed coupon without a currency is refused when it is created. Send amounts as decimal strings ("12.50").

duration says how many of a subscription's invoices a standing discount comes off: ONCE, REPEATING with durationInPeriods (counted in invoices, so a paused month does not use one up), or FOREVER. redeemBy is the last moment it can be taken up, and maxRedemptions counts customers, not invoices.

Neither a coupon nor a tax rate is ever deleted, because invoices refer to them. DELETE /v1/coupons/:id and DELETE /v1/tax-rates/:id archive:

  • An archived coupon cannot be redeemed on an invoice, a quote or a subscription. A subscription that already carries it keeps the discount it was promised — those terms were copied onto the subscription when it was applied. Remove it from a subscription with DELETE /v1/subscriptions/:id/discounts to stop it there.
  • An archived tax rate is not charged on anything finalized afterwards. Issued invoices copied the rate onto themselves and are unchanged.

Test clocks

A sandbox can hold one test clock: a time the whole environment believes it is. Advancing it runs the billing sweeps at the new time — trial ends, renewals, collection, dunning grace periods and quote expiry — and reports how many of each happened, so "does this renew?" takes one call instead of a month.

  • Sandbox only. Production refuses a clock: advancing it would renew every subscription and charge real cards.
  • Forward only, and at most a year per advance.
  • Deleting a clock keeps what it produced. Invoices dated in the future stay; the environment goes back to real time.

In the dashboard it is Billing → Test clock, shown on Sandbox only: create a clock, advance it by a day, a week, a month or to a date you choose — or straight to the next renewal or trial end listed under Coming up — and read what the advance did. Over the API it is /v1/test-clocks (GET, POST, POST /advance, DELETE), secret key only.

Credit notes

A credit note reduces what is owed without deleting anything. Total credits can never exceed the invoice total. Credit is not a payment: amountPaid is cash received, and an invoice half written off records only the cash that actually arrived.

Refunds

A refund is always issued through a credit note — there is no separate refund action, so every refund has the document that says why the money went back. A credit note settles in up to three parts:

total = what it takes off the amount still owed     min(total, amountDue)
      + what it refunds to the original payments    the settlement's refund
      + what it credits to the customer's wallet     the rest

Only money already received can be refunded or credited, so on an unpaid invoice a credit note only reduces what is owed. On a paid one, settlement decides where the money goes:

settlementMoney already received goes
CREDIT (default)to the customer's wallet, applied to their next invoice
REFUNDback to the payments that paid the invoice
SPLITrefundAmount back to the payments, the rest to the wallet
const note = await base316.creditNotes.create(
	{
		invoiceId: invoice.id,
		reason: "PRODUCT_UNSATISFACTORY",
		lines: [{ description: "Cancelled seats", amount: "100.00" }],
		settlement: "SPLIT",
		refundAmount: "40.00",
	},
	{ idempotencyKey: `refund-${ticketId}` },
);
// note.refundAmount "40", note.walletAmount "60", note.refunds[0].status "PENDING"
  • A card is refunded through Stripe, on your connected account — you are the merchant of record, so the money comes out of your Stripe balance. The refund is PENDING until Stripe settles it and then SUCCEEDED or FAILED; refund.succeeded and refund.failed announce which. Base316's application fee on the charge is refunded pro rata with it.
  • Cash, cheques and transfers are refunded by recording it. Base316 never moved that money and cannot send it back; issuing the note records that you did, with refundReference and refundMethod. It is SUCCEEDED at once.
  • What can be refunded. Succeeded payments on that invoice, in its currency, never more than each one paid less what is already refunded or pending. Cards go first, newest first, unless paymentId names one. A part paid from the wallet goes back to the wallet, not to a card.
  • A retry never refunds twice. Stripe is sent an idempotency key derived from the credit note. Send an Idempotency-Key on the request too, so a retried call replays the first credit note rather than issuing a second.
  • A failed refund is never lost. The credit note stays issued and the money is still owed. Credit to wallet instead on the invoice page (POST /v1/refunds/:id/credit-to-wallet) puts it in the customer's wallet; nothing does so on its own.
  • A refunded invoice stays PAID. amountPaid is what was received and is never reduced; amountRefunded beside it is what went back.
  • A note that moved money cannot be voided — a refund pending or made, or wallet balance the customer may already have spent. A note that only reduced an amount owed can be voided, and the invoice owes it again.

On the dashboard, Issue credit note on the invoice page offers Refund to card, Credit to wallet or Split, with what each payment still has to refund. Each refund's status shows on its payment, its credit note and the timeline, and GET /v1/refunds?invoiceId=… lists them. The credit note PDF says "Refunded to card ending 4242", "(pending)" until Stripe settles it, or "Credited to account".

PDF invoices, credit notes and quotes

Every invoice, credit note and quote can be downloaded as a PDF. Base316 renders it on its own servers each time you ask, from the stored figures. A credit note or a payment recorded since the last download shows up on the next one.

WhereHow
API (secret key)GET /v1/invoices/:id/pdf, GET /v1/credit-notes/:id/pdf, GET /v1/quotes/:id/pdf
SDKbase316.invoices.pdf(id), base316.creditNotes.pdf(id), base316.quotes.pdf(id)
Your app, for the signed-in userGET /v1/me/invoices/:id/pdf (browser SDK: billing.invoicePdf(session, id))
Hosted pay pageA Download PDF link under the same pay-link token as the page
EmailsAttached to every template on an invoice.* event, and to the dunning notices
DashboardPDF on each row of the invoice, credit note and quote lists, and on a customer's invoices
CLIbase316 invoice:pdf inv_… [file]
import { writeFile } from "node:fs/promises";

const pdf = await base316.invoices.pdf(invoice.id); // Uint8Array
await writeFile(`${invoice.number}.pdf`, pdf);

The response is application/pdf, with a Content-Disposition filename built from the document number (invoice-INV-2026-000042.pdf). Errors come back as the usual JSON error. An id from another environment returns 404, the same as an id that does not exist.

What is on an invoice. It lists your business name, logo, brand colour, support address and website, all taken from your hosted sign-in branding. If you set no support address, it uses the reply-to or from address of your verified sending domain. It then shows the customer's name, email, country and your id for them; the issue and due dates and the service period; and every line, with its quantity, unit price and amount. For a usage line it shows how much was used and how much of that was billable after the allowance. Below the lines come each discount, each tax rate, the total, any credit notes, wallet balance and payments applied, and the amount due. A draft is marked as a draft, so you can review it before you finalize.

Amounts are formatted in the invoice's currency, to that currency's own number of decimal places: none for JPY and KRW, three for KWD and BHD. A unit price keeps the precision it was rated at, so a per-token price of 0.00000125 is printed as it is, not rounded to 0.00.

The pay link. Base316 stores only a digest of a pay link, never the link itself, so an API download cannot print one. A PDF downloaded from the hosted pay page, or attached to a dunning notice, includes that page's own link as Pay online.

Customer details. Base316 does not store a postal address for a customer. The bill-to block shows what is stored: their name, email, country and region, and your id for them.

The text uses an embedded Noto Sans font, so names like "Łódź" and "Dvořák" print correctly. Logos must be PNG or JPEG.

Reporting

MRR normalises every recurring fee to a month — an annual plan at 12,000 is 1,000, not 12,000 in whichever month somebody renewed. Trials are excluded and reported separately, PAST_DUE counts, usage is excluded entirely because it is consumption rather than recurring revenue, and currencies are never summed together.

Ageing buckets open invoices by how overdue they are, net of part payments and credit notes.

On this page