Plans, usage and billing
Entitlements, meters, rates, and the current-period summary.
Pricing lives in a server-owned catalog. No price, allowance, limit, weight or provider
identifier is hardcoded in any SDK, the dashboard or the marketing site — every one of them reads
GET /v1/plans. That is what lets an older SDK display and enforce a newer catalog without being
republished, and it is why the numbers below are illustrative rather than authoritative.
The catalog is versioned and effective-dated, because a charge has to stay reconstructible after a price changes: an account records the catalog version its period was priced under, and a summary is rebuilt from that version rather than from whatever is current.
Plans
GET /v1/plans
GET /v1/plans/{plan_id}
developer, pro, startup, enterprise. There is no zero-price plan; see
Trials for how a new organization gets in.
| Entitlement | developer | pro | startup | enterprise |
|---|---|---|---|---|
| Running machines | 3 | 10 | 30 | negotiated |
| Machines | 5 | 20 | 100 | negotiated |
| Snapshots per machine | 10 | 25 | 50 | negotiated |
| Templates | 5 | 25 | 100 | negotiated |
| Members | 5 | 25 | 100 | negotiated |
| Concurrent sessions | 10 | 40 | 120 | negotiated |
| Largest machine size | standard | large | large | negotiated |
| Included compute (std-hours) | 120 | 400 | 2000 | negotiated |
| Included hot storage | 75 GB | 200 GB | 1 TB | negotiated |
| Overage allowed | yes | yes | yes | yes |
null means negotiated, never unlimited. An enterprise organization is refused until an
effective-dated override supplies real numbers — an unpriced machine is worse than a blocked one.
Trials
The developer product carries a two-week trial, configured on the billing provider. The
provider runs that clock and holds the card; this product records what it is told and never forms
a second opinion about when a trial converts.
That means a card is required before an organization can start anything. Signing up opens a
billing account in state unsubscribed, which grants nothing — a checkout is what starts the
trial.
A trial carries the developer plan’s limits with two differences:
| Entitlement | developer | on trial |
|---|---|---|
| Included compute (std-hours) | 120 | 56 |
| Included egress | none | 50 GB |
| Overage allowed | yes | no |
These are not about being unable to charge — a card is on file. They bound what a trial cancelled on day thirteen leaves behind: the provider bills nothing for a trial that never converts, so every hour run inside one is an hour this product pays for. 56 standard-hours is 120 prorated to fourteen days, so a trial runs at the same rate as the plan it previews. The egress allowance exists because the developer plan includes none — it bills every byte instead — and a trial that included none could not open a single session.
GET /v1/billing returns trial_ends_at: the provider’s conversion date, present only while the
account is trialing.
An unsubscribed or canceled account may create and start nothing, and running machines are
stopped with their disks captured. Nothing is deleted. Machines, snapshots, templates and
disks stay exactly where they are, and subscribing brings them back untouched. past_due keeps
the plan: a failed renewal is a card to fix, not a reason to stop someone’s machines the same
hour.
Meters
Six meters, all recorded as exact integers:
| Meter | Unit | What it counts |
|---|---|---|
compute_small_seconds |
seconds | runtime of a small machine |
compute_standard_seconds |
seconds | runtime of a standard machine |
compute_large_seconds |
seconds | runtime of a large machine |
hot_storage_byte_seconds |
byte-seconds | provisioned disk of a live machine |
cold_storage_byte_seconds |
byte-seconds | captured disk held in snapshots |
egress_bytes |
bytes | bytes served through the gateway |
Quantities travel as decimal strings, not numbers. A byte-second over a month exceeds what a double can hold exactly, and a billing quantity that rounds in transport is a billing quantity nobody can reconcile.
Compute is normalized: each size has a weight in thousandths of a standard-hour, and one compute price is multiplied by it. So the per-size rates cannot drift away from each other, and included allowances are expressed in standard-hours regardless of what sizes you actually ran.
A stopped machine still costs storage and no compute. That is the whole trade-off of keeping one around.
Usage
GET /v1/usage raw records for a period
GET /v1/usage/series the same records bucketed, for charting
for await (const r of client.usage.list({ periodStart, periodEnd })) {
r.meter;
r.size;
r.quantity;
r.machine_id;
}
const series = await client.usage.series({ periodStart, periodEnd, meter: "egress_bytes" });
The bucket width is the server’s choice — hourly up to a week, daily past it — and comes back on the response. Buckets with nothing in them are absent rather than zero.
Internal usage records are the source of truth. The billing provider holds a copy; this is the original.
The current period
GET /v1/billing/usage-summary
const summary = await client.billing.current();
summary.status; // "estimate" while the period is open, "final" after
summary.catalog_version; // which catalog priced it
summary.lines; // per meter: quantity, included, billable, amount
summary.compute_credits;
Each line separates raw quantity from included from billable from amount, so a bill is arithmetic a customer can check rather than a number they have to trust. A plan that cannot incur overage prices to zero rather than being hidden — you can still see what you used.
Account, checkout and portal
GET /v1/billing the organization's account
POST /v1/billing/checkout returns a url to send the customer to
POST /v1/billing/portal returns a customer-portal url
An account state is unsubscribed, trialing, active, past_due or canceled, and carries
the plan, the period, trial_ends_at, and the organization’s current limits — each with a
key, its current value and its limit.
Every state except unsubscribed comes from the provider. unsubscribed is where an
organization starts and has never bought anything; canceled is a subscription that stopped
being paid. They are enforced identically and are different things to say to a customer. Both
refuse new work with quota_exceeded (HTTP 402) and a quota.limit of subscription.
const url = await client.billing.checkout({ planId: "pro", successUrl });
const portal = await client.billing.portal();
billing:manage is an owner-only permission.
When a limit declines a request
A quota_exceeded error carries a quota object rather than only a sentence:
error.quota.limit; // "running_machines"
error.quota.current; // 3
error.quota.maximum; // 3
error.quota.plan_id; // "developer"
error.quota.upgrade_url; // where to go
Render that, not the message. A caller that shows the sentence instead makes the customer guess
which of their limits it was. Limit keys are running_machines, machines, templates,
members, concurrent_sessions and snapshots_per_machine.
Running without a provider
BILLING_PROVIDER=none is a first-class mode, not a broken one. Usage accounting, organization
limits and the whole trial work with no provider configured — which is what local
development and the integration suite run in.