> ## Documentation Index
> Fetch the complete documentation index at: https://boxrouter.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# How Sandbox Cost Works

> See who bills a sandbox, read its cost and the evidence behind it, and find your usage totals.

Every sandbox has one payer. With your own provider key (BYOK), the provider bills your account
and BoxRouter shows the cost information it can verify. With Credits, BoxRouter bills you the
provider's cost, with no markup, and shows that as the sandbox's charge.

| | BYOK | Credits |
| - | - | - |
| Who bills the compute | Your provider account | BoxRouter, from your credit balance |
| Where the charge appears | Your provider invoice | [Credits](https://boxrouter.ai/docs/credits.md) |
| What the dashboard shows | Cost, after the sandbox ends | Charge: reserved while it runs, then the amount charged |

Under BYOK you owe the provider's invoice and BoxRouter adds no fee. Compare the invoice with
`cost.total` once the sandbox has ended and `total` is no longer `null`.

## Funding from Your Balance

BoxRouter reserves credit before the sandbox starts. If your available credit can't cover the
reservation, the call fails with `402 insufficient_credit` and nothing is created. Extending a running
sandbox is refused the same way.

How much is reserved depends on how the sandbox is funded:

* **Rolling:** leave out `lifetime_seconds` and set `resources.extendable_lifetime`. BoxRouter reserves
  one hour at a time and renews before each hour ends. If your balance can't cover the next hour, the
  sandbox ends at `funded_until`. See [run while funded](https://boxrouter.ai/docs/run-while-funded.md).
* **Fixed:** with `lifetime_seconds`, or on a route that can't extend a lifetime, BoxRouter reserves
  the whole lifetime at the start. Buy credits or ask for a shorter `lifetime_seconds`.

A reservation lowers your available balance but is not a charge. When the sandbox ends, BoxRouter
sets the charge from the provider's records and returns the unused reservation. Until the records
arrive the charge is pending and the reservation stays held. [Credits](https://boxrouter.ai/docs/credits.md#see-what-a-sandbox-was-charged)
explains each state. Set a [spend limit](https://boxrouter.ai/docs/spend-limits.md) for the workspace.

## Read a Sandbox's Cost

A sandbox's cost is unknown until it ends. While it runs, `cost` is `{"label": "unknown", "total": null}`.
After it ends, `cost` stays `unknown` until the provider reports the usage it measured. Some providers take about 30 minutes, and the charge stays pending with the reservation held until then. The same delay applies to a [GitHub Actions job](https://boxrouter.ai/docs/actions/runs.md): its cost and charge read `unknown` until the provider's usage arrives.
Set `BOXROUTER_API_KEY` and install a client as in the [quickstart](https://boxrouter.ai/docs/quickstart.md). The example creates
a sandbox, destroys it, and reads its cost. It uses your workspace's routing and funding order. For BYOK,
[connect a provider key](https://boxrouter.ai/docs/provider-keys.md) and prefer an eligible key in [routing settings](https://boxrouter.ai/docs/routing-settings.md#provider-keys-and-credits-order).

```python Python theme={"theme":"css-variables"}
import json
from boxrouter import BoxRouter

with BoxRouter() as client:
    box = client.sandboxes.create(lifetime_seconds=300)
    try:
        print("sandbox:", box.id, flush=True)
    finally:
        box.destroy()

    sandbox = client.api.sandboxes.get(box.id)
    print(sandbox.cost.label)
    total = sandbox.cost.total
    print("total:", json.dumps(total.to_dict() if total is not None else None, indent=2))
    print(*sandbox.cost.held_back_by, sep="\n")
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

await using client = new BoxRouter();
let sandboxId: string;
{
  await using box = await client.sandboxes.create({ lifetime_seconds: 300 });
  sandboxId = box.id;
  console.log('sandbox:', sandboxId);
}

const sandbox = await client.api.sandboxes.get({ path: { sandbox_id: sandboxId } });
console.log(sandbox.cost.label);
console.log('total:', JSON.stringify(sandbox.cost.total, null, 2));
console.log(sandbox.cost.held_back_by.join('\n'));
```

```bash CLI theme={"theme":"css-variables"}
(
  set -euo pipefail
  id=$(boxrouter create --lifetime-seconds 300)
  echo "sandbox: $id"
  boxrouter destroy "$id"
  boxrouter get "$id" | jq -r '.cost.label, "total: \(.cost.total | tojson)", .cost.held_back_by[]'
)
```

One possible BYOK response is:

Output

```text theme={"theme":"css-variables"}
sandbox: <SANDBOX_ID>
unknown
total: null
The provider did not report the sandbox's size.
The provider did not report the billed time.
```

If destruction fails, use the printed sandbox ID to [check cleanup](https://boxrouter.ai/docs/errors.md#when-cleanup-fails).

Right after destroy, this provider had not yet reported the sandbox's size or billed time, so
`total` is `null` and `held_back_by` names each gap. `unknown` never means free. Read
the sandbox again later and check `held_back_by` if the total is still `null`; the missing evidence
or rate might not become available.

`cost.label` says how much to trust the total:

| `label` | Dashboard Word | Meaning |
| - | - | - |
| `reported` | **Reported By Provider** | The provider gave this sandbox's amount. `reported_figure` names the API call and time. |
| `calculated` | **Calculated** | BoxRouter priced the provider's usage records at list rates. `lines` shows each resource, quantity, and rate. |
| `unknown` | Not shown | Evidence or a rate is missing. `total` is `null` and `held_back_by` says what is missing. |

When `total` is known, read these fields:

| Field | Holds |
| - | - |
| `total.low`, `total.high` | Equal decimal strings in USD for the exact cost. Keep them as strings; hourly rate quotes can be ranges. |
| `exclusions` | What the figure leaves out, such as plan fees, credits, and discounts. |
| `price_version`, `as_of` | The rates used, and when BoxRouter calculated or recorded the cost. |

The [sandbox schema](https://boxrouter.ai/docs/reference/get.md) lists every field. [List sandboxes](https://boxrouter.ai/docs/reference/list-sandboxes.md)
returns only `label` and `total`.

## Check the Usage Behind It

`usage` records the sandbox's size, region, and time in each state. It also records active CPU and egress where they are billed.

Each fact includes `source`, `observed_at` and `how`. If a fact cannot be established, its source
is `unknown` and its value is `null`. BoxRouter does not substitute a default.

`source` is `provider`, `published`, `measured`, `recorded`, `calculated`, or `unknown`.
The [sandbox schema](https://boxrouter.ai/docs/reference/get.md) defines each value.

## Total Usage over Time

`usage` (CLI: `boxrouter usage`) groups managed and BYOK sandboxes by
provider, day, key, or sandbox. It needs a key with `credentials:manage` and a range of at most
92 UTC days. Select **Manage Provider Keys (BYOK)** when creating the key; see [key permissions](https://boxrouter.ai/docs/setup.md#key-permissions).

```python Python theme={"theme":"css-variables"}
import datetime
import json
from boxrouter import BoxRouter

with BoxRouter() as client:
    day = datetime.date(2026, 10, 29)
    report = client.usage.list(from_=day, to=day, group_by="provider")
    print(json.dumps(report.to_dict(), indent=2))
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

const client = new BoxRouter();
try {
  const query = { from: '2026-10-29', to: '2026-10-29', group_by: 'provider' } as const;
  console.log(JSON.stringify(await client.usage.list({ query }), null, 2));
} finally {
  await client.close();
}
```

```bash CLI theme={"theme":"css-variables"}
boxrouter usage --from 2026-10-29 --to 2026-10-29 --group-by provider
```

Output

```json theme={"theme":"css-variables"}
{
"as_of": "2026-10-29T10:34:35.533121+00:00",
"data": [
{
  "period": "2026-10-29/2026-10-29",
  "group_key": "<PROVIDER_ID>",
  "sandbox_count": 5,
  "compute_seconds": null,
  "compute_seconds_unknown_sandboxes": 5,
  "charge_usd": "0.000000000",
  "held_usd": "0.000000000",
  "byok_provider_cost_usd": null,
  "byok_provider_cost_unknown_sandboxes": 5
}
],
"next_cursor": null
}
```

* `charge_usd`: the Credits amount already charged.
* `held_usd`: the amount reserved right now, a snapshot that changes as sandboxes renew and end.
* `byok_provider_cost_usd`: the provider cost of your BYOK sandboxes. It is `null` whenever any
  sandbox in the row has an unknown cost, never a partial sum.

The same report downloads as CSV from `/usage.csv`. In the dashboard, **Usage** under Credits shows
it by day, key, or sandbox with date filters. `boxrouter api credits usage` lists the individual
charges, corrections and reservations, each with the `key_id` of the API key that made the request.

## Recheck the Rates

[List price versions](https://boxrouter.ai/docs/reference/list-price-versions.md) returns rates, sources, meter rules, and
exclusions without a key. Filter by `provider`, or pass `at` for the version in effect at a time.
A live version never changes. Meter rules specify quantities, minimum seconds, and billing increments.

## Stop the Meter

[Destroy sandboxes](https://boxrouter.ai/docs/lifecycle.md) when work ends. [Volumes](https://boxrouter.ai/docs/volumes.md) stay after compute stops. Their
storage is free during launch, up to the [workspace limit](https://boxrouter.ai/docs/volumes.md#what-it-holds); delete volumes you no longer need to stay inside it.

## Next Steps

> [card] [Buy and Manage Credits](https://boxrouter.ai/docs/credits.md)
>
> Buy Credits, read charges, and request a refund.

> [card] [Cap Your Spend](https://boxrouter.ai/docs/spend-limits.md)
>
> Set a limit for the workspace or a key.

> [card] [Check Your Limits](https://boxrouter.ai/docs/limits.md)
>
> Which limits apply and who can change them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.