---
title: "Read usage"
method: GET
path: "/usage"
tags: ["Usage"]
---

# Read usage

`GET /usage`

## Request

- Base URL: `https://app.mandala.computer/api/v1`
- URL: `https://app.mandala.computer/api/v1/usage`
- Auth: HTTP bearer

What this account has used — running hours weighted by cores and memory, and the storage it holds — with the per-computer breakdown behind the totals. The same figures the dashboard shows, and the read to build a spend check around: a loop that launches computers is the caller that can run up a bill without noticing.

The default window is the period your plan bills on, which is what makes this comparable with an invoice — up to the 62 days this endpoint reads at once. A period longer than that (an annual plan, a long trial) is reported in `period` in full and MEASURED over the most recent 62 days of it; `from` and `to` in the response always say which window the figures cover, so compare those against `period` before reconciling. `from` and `to` override the default, and you will want them for a window that has closed — the billing period is the CURRENT one, and by the time an invoice arrives the period it covers is not.

READ `degraded` AND `unmetered` BEFORE USING THE NUMBERS. Every figure here is a sum across the hypervisors your computers are on, so a host that did not contribute does not leave a hole you could notice — it leaves a total that is quietly too small. Those two flags are how that says so, and it is why this endpoint answers 200 with a caveat rather than the 503 the listings use: one of the two shortfalls never clears by retrying.

The account, not the key: a workspace-scoped key reads the whole account’s totals, because usage is metered per account and billed per account. What it does not get is `usage.computers`, which would name machines outside its scope.

Only `from` and `to` are read. Any other query parameter is ignored rather than refused, and a bound sent twice is read from its first value.

Requires the `viewer` role or higher.

## Query parameters

- `from` string
- `to` string

## Response `200`

The totals for the window, and whether anything is missing from them.

- Usage
  - `period` UsagePeriod, required
    - `start` string, required: RFC 3339.
    - `end` string, required: RFC 3339.
    - `source` string, required: `subscription` when the boundary came from your plan’s own billing period, which is what an invoice is anchored to — usually the renewal date, and the end of the trial while you are in one. `subscription-projected` in the short gap between a renewal happening and our hearing about it: `start` is the previous period’s end and is exact, and `end` is that plus the previous period’s length, which is a duration rather than a billing rule — so it can be a few days out across a short month, and a whole term out if a plan change took effect at that same renewal. Treat a projected `end` as provisional and never quote it as a renewal date; `start` is exact and safe to use. `calendar-month` when there is no billing period to take it from, in which case the period is the current UTC month. Do NOT read `calendar-month` as “no subscription”: it also covers a subscription whose period start we do not hold, which is possible for a plan billed over anything longer than a month. This API does not distinguish the two, so do not branch on it to decide whether an account has a plan — read `from` and `to` for what was measured. New values may be added, so treat this as an open set.
  - `from` string, required: The start of the window these figures cover, RFC 3339.
  - `to` string, required: The end of it, RFC 3339, and worth reading rather than assuming. A `to` in the future is answered as now, because the future holds no usage; everything else is the instant you asked for.
  - `usage` UsageTotals, required
    - `run_hours` number, required: Wall-clock hours computers on this account spent running.
    - `vcpu_hours` number, required: Those hours weighted by cores.
    - `ram_gb_hours` number, required: And by memory.
    - `snapshot_gb_hours` number, required: The integral of snapshot bytes held, in GB-hours.
    - `snapshot_gb_months` number, required: The same integral in the unit snapshots are priced in.
    - `disk_gb_hours` number, required: The integral of provisioned computer disk, running or not — a stopped computer still holds its disk. Kept apart from the snapshot figures on purpose: disks are provisioned at create and released at delete, snapshots come and go under your plan’s retention and your own deletions, and the two have different remedies.
    - `disk_gb_months` number, required: The same integral in the unit disk is priced in.
    - `computers` UsageComputer[]: The per-computer breakdown, which is what makes a total checkable. ABSENT when the API key is scoped to a workspace: usage is metered per account, so these lines cover the whole account and would name computers outside the key’s scope. The totals are still account-wide either way.
      - `id` string, required: The computer this line is for. The time one of your own templates spends booting its seed for fast launches is billed under the seed’s own id and name, which `GET /computers` never lists; that line reads `gone` once the seed is deleted.
      - `name` string: What it was called. The name from a host that still holds it wins.
      - `run_hours` number, required
      - `vcpu_hours` number, required
      - `ram_gb_hours` number, required
      - `gone` boolean: Present and `true` when this computer is no longer on the fleet. It ran during the window and was deleted, which is why it is billed for and not in `GET /computers`.
  - `degraded` boolean, required: A hypervisor could not be reached, so every figure above may be too small. This is NOT a 503, and it is not a gap: each figure is a sum across the fleet, so a host that did not answer leaves a total that is quietly short rather than an obviously missing row. Do not reconcile against an invoice while this is true — retry, and it clears when the host comes back.
  - `unmetered` boolean, required: The same shortfall from the other cause, and it is separate because it does NOT clear on its own: a hypervisor is up and running a daemon older than the meter, so it has no hours to report. Waiting does not fix this one.
  - `reported_through` string, nullable, required: The last UTC day (`YYYY-MM-DD`) whose usage has settled for billing — a contiguous prefix, so a day still being held back stops the count where it is. Null when none of the window has settled yet. This is not a caveat on the totals: those are live from the ledger and true through `to`. It answers the other question, which is how much of the same window has reached the billing system, and it is the one to check before comparing these numbers with an invoice. NULL, not absent, until something has settled — which is every account today.

## Other responses

- `400`: The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.
- `401`: Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).
- `403`: The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: "revoked"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help. A suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.
- `405`: This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.
- `429`: You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: "contention"` (see `POST /snapshots/{id}/clone`).
- `500`: The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.

---

[API](https://skmtc.dev/mandala/apis/mandala-computer-api.md) · [All operations](https://skmtc.dev/mandala/apis/mandala-computer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mandala/apis/mandala-computer-api/revisions/2afbb87fa703?raw)
