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}
free, developer, pro, startup, enterprise.
| Entitlement | free | developer | pro | startup | enterprise |
|---|---|---|---|---|---|
| Running machines | 1 | 3 | 10 | 30 | negotiated |
| Machines | 1 | 5 | 20 | 100 | negotiated |
| Snapshots per machine | 2 | 10 | 25 | 50 | negotiated |
| Templates | 1 | 5 | 25 | 100 | negotiated |
| Members | 2 | 5 | 25 | 100 | negotiated |
| Concurrent sessions | 2 | 10 | 40 | 120 | negotiated |
| Largest machine size | small | standard | large | large | negotiated |
| Included compute (std-hours) | 5 | 120 | 400 | 2000 | negotiated |
| Included hot storage | 10 GB | 75 GB | 200 GB | 1 TB | negotiated |
| Overage allowed | no | 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.
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 trialing, active, past_due or canceled, and carries the plan, the
period, and the organization’s current limits — each with a key, its current value and its
limit.
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 free plan work with no provider configured — which is what local
development and the integration suite run in.