---
title: Plans, usage and billing
description: 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
```

```ts
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.

:::note
In the Python SDK, `series` is not wrapped. Call `client.api.usage.series(...)` — it is the same
route with the same shape.
:::

## The current period

```
GET /v1/billing/usage-summary
```

```ts
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`.

```ts
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:

```ts
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.
