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
= totaland separately:
amountDue = total − amountPaid − creditedTotalamountDue 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 billableThat 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
dimensionsmust be in the meter'sgroupBy, 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. amountis 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 — withunpriced: trueand 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
checkagainst 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 anythingcheckcalled included. AMAX,MIN,LASTorUNIQUE_COUNTmeter 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 equalused − (limit + credits)— exactly whatcheckreports asbillableOverage— and no other split would charge the customer less.limit: nullis unlimited and bills nothing;limit: 0bills 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 itsdimensions({}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:
| model | tokens |
|---|---|
| opus | 1,000,000 |
| sonnet | 1,000,000 |
| haiku | 1,000,000 |
| llama-405 | 500,000 |
The 1,500,000 included tokens cover the dearest first: all of opus, then half of sonnet. The invoice has four usage lines:
| line | used | included | billable | unit | amount |
|---|---|---|---|---|---|
| LLM tokens — model: opus | 1,000,000 | 1,000,000 | 0 | 0.000015 | $0.00 |
| … model: sonnet | 1,000,000 | 500,000 | 500,000 | 0.000003 | $1.50 |
| … model: haiku | 1,000,000 | 0 | 1,000,000 | 0.000001 | $1.00 |
| … other values | 500,000 | 0 | 500,000 | 0.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.amountDueWhich invoice it previews follows what the billing run will actually do next:
reason | When | What is on it |
|---|---|---|
RENEWAL | At the period end | Usage for the current period, plus the fee for the next one |
FINAL | At the period end, when cancelling there | Usage only — no further period opens, so no fee is charged |
TRIAL_END | When a trial converts | The 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
generatedAtsays when it was read. Usage between then andissuesAtstill 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, andamountDue = total − walletApplied.creditsAppliedlists the prepaid credit the usage lines will draw, per grant. willBeIssued: falsemeans 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:
| Status | Terms | Can be subscribed to | Deleted |
|---|---|---|---|
DRAFT | Editable in place | No | Yes — discard it |
PUBLISHED | Immutable | Yes | Never |
ARCHIVED | Immutable | No | Never |
- 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
409naming 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
entitlementsto a draft edit replaces the list, but an entry that leaveshardCapout keeps the draft's current cap for that feature;hardCap: nullremoves it. Caps are checked as at creation: a cap needs a numericlimitand cannot be below it (INVALID_REQUEST,param: entitlements[n].hardCap). - Leaving
publishout publishes, as it always has:createPlan,createVersionandPOST /v1/plans/:id/versionsonly make a draft when asked withpublish: 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" });| Timing | What happens |
|---|---|
NOW | The 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_END | Nothing 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/phaseswithdraws it. - A cancellation and a scheduled change never stand together. Cancelling
withdraws the schedule — leaving is always allowed — and scheduling is
refused with
409while 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.invoiceIdThe 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.
| Direction | Default | proration | effective |
|---|---|---|---|
| Increase | Now, invoiced for the rest of the period | invoice_now (default), next_invoice, none | immediately (default), period_end |
| Decrease | At the period end, nothing credited | Refused — a decrease is not credited | period_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_nowraises 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_invoiceadds the amount to the renewal as a pending charge.nonecharges 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 (
seatsunless 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 forcheck(). - 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-Keyreplays 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; orfeatureId: 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), bypriceIdor byfeatureId, and a custom price cannot itself be aMATRIX. 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
PACKAGEneedspackageSize. - 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:
trialEndAction | No payment method on file | A card on file |
|---|---|---|
CONVERT (default) | ACTIVE on this plan, first period billed (and chased by dunning if unpaid) | Same, and charged |
DOWNGRADE | Moved to trialDowngradePlanVersionId, ACTIVE, no invoice | Converts 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:
| Where | CONVERT | DOWNGRADE (downgrading to Free) |
|---|---|---|
PricingTable | 14-day free trial | 14-day free trial, then Free unless you add a card |
| Hosted checkout | 14-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.
receivedAtis 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).
Payment links
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:
| When | Subscription | Customer is emailed |
|---|---|---|
| A card is declined | PAST_DUE — still entitled | "Your payment didn't go through", with the next retry date and a pay link |
| Retries on days 1, 3 and 7 | PAST_DUE | the same, after each decline |
| Retries are exhausted | PAST_DUE for a 7-day grace period | "Action needed", with the date access ends |
| Grace ends unpaid | PAUSED (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/discountsto 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 restOnly 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:
settlement | Money already received goes |
|---|---|
CREDIT (default) | to the customer's wallet, applied to their next invoice |
REFUND | back to the payments that paid the invoice |
SPLIT | refundAmount 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
PENDINGuntil Stripe settles it and thenSUCCEEDEDorFAILED;refund.succeededandrefund.failedannounce 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
refundReferenceandrefundMethod. It isSUCCEEDEDat 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
paymentIdnames 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-Keyon 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.
amountPaidis what was received and is never reduced;amountRefundedbeside 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.
| Where | How |
|---|---|
| API (secret key) | GET /v1/invoices/:id/pdf, GET /v1/credit-notes/:id/pdf, GET /v1/quotes/:id/pdf |
| SDK | base316.invoices.pdf(id), base316.creditNotes.pdf(id), base316.quotes.pdf(id) |
| Your app, for the signed-in user | GET /v1/me/invoices/:id/pdf (browser SDK: billing.invoicePdf(session, id)) |
| Hosted pay page | A Download PDF link under the same pay-link token as the page |
| Emails | Attached to every template on an invoice.* event, and to the dunning notices |
| Dashboard | PDF on each row of the invoice, credit note and quote lists, and on a customer's invoices |
| CLI | base316 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.

