Skip to content
Raster
Esc
navigateopen⌘Jpreview
On this page

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.

Was this page helpful?