---
title: "Read Box"
method: GET
path: "/api/sandboxes/admin/box/{provider}/{sandbox_id}"
tags: ["sandboxes"]
---

# Read Box

`GET /api/sandboxes/admin/box/{provider}/{sandbox_id}`

One box's state row and its full timeline, oldest event first.

Both path parameters are required because the ledger's primary key is
composite: a provider-native id is unique only inside its own provider's
namespace.

``sandbox_id`` is a ``:path`` parameter because a provider-native id is
allowed to contain slashes: an ECS task ARN is
``arn:aws:ecs:<region>:<account>:task/<cluster>/<id>``, which the default
converter splits into extra path segments so that no route matches at all.
It is the last segment, so swallowing the remainder is unambiguous.

The timeline is capped like every other list here. A box that flapped
between running and stopped for months has an unbounded event count, and
an admin route that can return an unbounded body is a way to take the API
down from a browser tab.

## Path parameters

- `provider` 'modal' | 'aws' | 'gcp' | 'sail', required — The compute backend that owns a sandbox.
- `sandbox_id` string, required

## Query parameters

- `events` integer

## Response `200`

Successful Response

- BoxDetail — One box and its full timeline, oldest event first.
  - `box` SandboxRow, required — One ``sandbox_ledger_state`` row, plus its derived cost block. Every nullable field here is nullable because the writer could not name a value, never because the value is uninteresting. A null ``warmth`` is a box a reconciler found rather than witnessed starting; a null ``org_id`` is a box whose owner the writer could not identify. Readers should render those as unknown rather than as a default.
    - `provider` 'modal' | 'aws' | 'gcp' | 'sail', required — The compute backend that owns a sandbox.
    - `sandboxId` string, required
    - `sandboxAlias` string, required
    - `lifecyclePhase` 'pending' | 'running' | 'stopped' | 'deleted', required — Current phase of a sandbox in the ledger's state table. ``STOPPED`` is not ``DELETED``. ``checkpoint_and_stop`` retains the disk and is the normal cleanup path on GCP and Sail, so a stopped sandbox is still billing for storage. ``RUNNING`` and ``STOPPED`` are therefore the two "open" phases: a sandbox in either one still exists and still costs money. The state table's partial unique index on ``sandbox_alias`` is scoped to exactly this pair, so that an alias resolves to at most one open sandbox. Adding or removing an open phase means changing that index in the same migration.
    - `warmth` 'cold' | 'warm_provider' | 'warm_gateway', required — How much of a sandbox acquisition was served from something existing. Three values, not two. Provider-level reuse and gateway session reuse are separate events raised from separate call sites, and they have different latency profiles; collapsing them into one "warm" value loses the distinction the cold-start work depends on.
    - `resourceClass` string, required
    - `gpuType` string, nullable, required
    - `gpuCount` integer, nullable, required
    - `isPacked` boolean, required
    - `isIsolated` boolean, required
    - `commitmentBasis` 'on_demand' | 'reservation' | 'capacity_block', required — Which capacity pool a sandbox's launch actually consumed. A property of the individual launch, not of the resource class. The same shape lands differently from one start to the next: GCP GPU hosts launch into ``GCP_GPU_RESERVATION`` "only while the reservation has an idle slot" (``gcp_client.py:32-35``) and take the on-demand path when it is unset (``:166``), and an AWS capacity-block launch "falls back to the on-demand path instead of failing the kernel start" when the slot is raced away (``ec2_client.py:1268``). So two identical ``p5.48xlarge`` rows can carry completely different real cost, and nothing at read time can tell them apart — the pool a launch consumed is unrecoverable once the box is gone. That is what makes this a stored column rather than something the rate card derives. ``RESERVATION`` and ``CAPACITY_BLOCK`` are separate because the mechanisms are: a GCE specific reservation holds ``RESERVATION_BOUND`` VMs, while AWS capacity blocks are bought on a rolling cadence and reclaimed at expiry (QUA-2813 and QUA-2770 respectively). Neither is marginal spend.
    - `deploymentEnvironment` string, required
    - `notebookId` string, nullable, required
    - `kernelId` string, nullable, required
    - `orgId` string, nullable, required
    - `orgName` string, nullable
    - `workspaceId` string, nullable, required
    - `userId` string, nullable, required
    - `userEmail` string, nullable
    - `runScope` string, nullable, required
    - `createdAt` string, required
    - `lastTransitionAt` string, required
    - `causeOfDeath` 'idle_reap' | 'preemption' | 'health_check_kill' | 'readiness_failed' | 'lease_released' | 'lease_expired' | 'age_limit' | 'explicit_teardown' | 'superseded' | 'unknown_reconciled', required — Why a sandbox left its last open phase. ``UNKNOWN_RECONCILED`` is first-class and load-bearing. A hard preemption leaves nobody running to write a truthful cause, so the reconciler has to be able to say it does not know. Without this value the reconciler is forced to pick a plausible cause instead, and every other cause in the ledger stops being trustworthy.
    - `closedAt` string, nullable, required
    - `runningSeconds` integer, nullable, required
    - `stoppedSeconds` integer, nullable, required
    - `currentRunSeconds` integer, nullable, required
    - `cost` union, required
      - CostAvailable — A derived cost floor for one box, split by the kind of money it is. Three figures in three different units, and **none may be added to another**. The rate module refuses to produce a total and this shape keeps that refusal, because summing prepaid capacity with marginal spend produces a figure no invoice will ever show. - ``on_demand_usd_floor`` is money that left the account, underestimated. - ``committed_capacity_list_value_usd`` is what the capacity this box drew would have cost at list price had it not been prepaid. Not spend: the slot was paid for whether or not anything ran in it. - ``consumed_capacity_hours`` is slot-hours drawn from a commitment. It needs no rate card, so it is the one figure here that cannot be wrong. ``basis`` names the pool the box's *compute* drew on, which is what a dashboard groups by. It does not promise every dollar below it is that kind; the fields do that.
        - `available` true
        - `basis` 'on_demand' | 'committed', required
        - `onDemandUsdFloor` string, nullable, required
        - `committedCapacityListValueUsd` string, nullable, required
        - `consumedCapacityHours` string, nullable, required
        - `billingShape` string, required
        - `rateProvenance` 'vendor_list' | 'vendor_reserved', required
        - `rateRegion` string, nullable, required
        - `rateRevisedOn` string, required
        - `overheadMultiplier` string, required
      - CostUnavailable — No derivable figure, and the reason there is none. Every reason is a property of one box rather than of the deployment, and every one of them yields no dollars at all. None is a zero: a zero renders as free, and nothing in the ledger is free. Three of the four originate in ``compute.sandbox_cost`` and are passed through rather than collapsed. A Sail box (``usage_billed_provider``) and a box whose machine type the card has no entry for (``unpriced_shape``) are different problems with different fixes, and a reader that has to act on one should not have to guess which it is looking at. The fourth, ``no_duration_evidence``, is this module's own: it is about the event log rather than the rate card. ``unknown_resource_class`` arrives by both routes. The rate module returns it for a shape it cannot resolve; the read layer returns it as well, for a dedicated box whose ``gpuCount`` is null -- unresolvable for the different reason that the hardware was never learned. Those have different fixes, a rate-card entry against a reconciler backfill, but they do not need separate reasons, because the row already carries the discriminator: this reason beside a null ``gpuCount`` is the second case, and every other occurrence is the first.
        - `available` false
        - `reason` 'usage_billed_provider' | 'unpriced_shape' | 'unknown_resource_class' | 'no_duration_evidence', required
  - `events` SandboxEvent[], required
    - `eventId` integer, required
    - `occurredAt` string, required
    - `recordedAt` string, required
    - `eventKind` 'placement_refused' | 'acquired' | 'ready' | 'readiness_timeout' | 'stopped' | 'resumed' | 'deleted' | 'superseded' | 'reconciled', required — Kind of transition recorded in the append-only sandbox event log.
    - `source` 'gateway' | 'reaper' | 'reconciler' | 'drain', required — Which component wrote a ledger event. ``RECONCILER`` matters on its own: it is what keeps a cause we inferred after the fact distinguishable from a cause we actually caused. ``DRAIN`` is separate from it for the same reason, one step removed. The drain synthesizes a ``superseded`` event when a colliding acquisition proves an earlier box is gone: inferred, so not ``GATEWAY``, but inferred from something a gateway *witnessed* rather than from sweeping a provider after the fact. Folding it into ``RECONCILER`` would inflate exactly the gateway-to-reconciler ratio that ``models/sandbox_ledger.py`` calls the ledger's own trustworthiness metric, and no later migration could separate the two again -- an enum value cannot be removed, and by then the rows are indistinguishable.
    - `causeOfDeath` 'idle_reap' | 'preemption' | 'health_check_kill' | 'readiness_failed' | 'lease_released' | 'lease_expired' | 'age_limit' | 'explicit_teardown' | 'superseded' | 'unknown_reconciled', required — Why a sandbox left its last open phase. ``UNKNOWN_RECONCILED`` is first-class and load-bearing. A hard preemption leaves nobody running to write a truthful cause, so the reconciler has to be able to say it does not know. Without this value the reconciler is forced to pick a plausible cause instead, and every other cause in the ledger stops being trustworthy.
    - `sandboxId` string, nullable, required
    - `sandboxAlias` string, required
    - `payload` object, required

## Other responses

- `422` — Validation Error

## Changes

- **2026-09-18** `abf76937a751` — 8 warning, 2 info
  - added the new `age_limit` enum value to the `box/causeOfDeath/anyOf[subschema #1: CauseOfDeath]/` response property for the response status `200`
  - added the new `age_limit` enum value to the `events/items/causeOfDeath/anyOf[subschema #1: CauseOfDeath]/` response property for the response status `200`
  - added the new `lease_expired` enum value to the `box/causeOfDeath/anyOf[subschema #1: CauseOfDeath]/` response property for the response status `200`
  - added the new `lease_expired` enum value to the `events/items/causeOfDeath/anyOf[subschema #1: CauseOfDeath]/` response property for the response status `200`
  - …6 more
- **2026-09-15** `21910d3ae5d8` — 2 info
  - added the optional property `box/orgName` to the response with the `200` status
  - added the optional property `box/userEmail` to the response with the `200` status
- **2026-09-10** `3a8d4a28ae93` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/quadrillion/apis/quadrillion-cloud-api/changes/api/sandboxes/admin/box/:provider/:sandbox_id/get.md)

---

[API](https://skmtc.dev/quadrillion/apis/quadrillion-cloud-api.md) · [All operations](https://skmtc.dev/quadrillion/apis/quadrillion-cloud-api/llms.txt) · [OpenAPI document](https://skmtc.dev/quadrillion/apis/quadrillion-cloud-api/revisions/397ad3a1d858?raw)
