---
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}
```

`developer`, `pro`, `startup`, `enterprise`. There is no zero-price plan; see
[Trials](#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
```

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

```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 trial work with no provider configured — which is what local
development and the integration suite run in.
