---
title: "Read Fleet"
method: GET
path: "/api/sandboxes/admin/fleet"
tags: ["sandboxes"]
---

# Read Fleet

`GET /api/sandboxes/admin/fleet`

A filtered, keyset-paginated page of the fleet.

``phase`` defaults to the open phases, which is what a fleet view means.
Every *single*-phase query, ``deleted`` included, is index-addressable --
``lifecycle_phase`` leads ``ix_sandbox_ledger_state_phase_transition``, so
an equality on it lets the index supply the ordering too. The **all-phases**
query is the one the index cannot help: it matches everything, and the
planner reads the table and sorts. Measured plans are in
``cloud_api/README.md``.

``gpuType``, ``gpuCount``, ``warmth`` and ``deploymentEnvironment`` have no
index of their own, so each is a filter applied to rows a phase predicate
already narrowed rather than a scan in its own right. See
``cloud_api/README.md`` for why no index was added.

``userId`` filters on the principal who launched the box, which under a
non-owner role is not the workspace owner ``orgId`` and ``workspaceId``
resolve through. It is the identity a runaway box has to be paged against.

``email`` filters on the same column, resolved through ``users`` on the way
in, and is what the dashboard offers -- an id is not something anyone
holds. Both are kept: ``userId`` still addresses a principal that is not an
account at all, such as the gateway's, and still honours a link someone
bookmarked. Passing both narrows by both, so an address and an id naming
different principals matches nothing, as the conjunction says it should.
An address nobody owns yields an empty page rather than an unfiltered one.

``notebookId`` answers "which box ran this notebook", the question an
error report starts from -- it carries a notebook id and no sandbox id.
It is an unindexed filter on the same terms as ``gpuType``. Two limits are
worth knowing before trusting a miss: the column holds the notebook
recorded when the box was *acquired*, kept first-witness-wins by the
drain, and a Modal ``ke-`` box is keyed on org/workspace/environment
rather than on a notebook, so several notebooks share one row. A hit
names the right box; a miss does not prove the notebook had none.

``cursor`` is opaque and must be passed back exactly as it was received.

## Query parameters

- `phase` LifecyclePhase[]
- `provider` SandboxProvider[]
- `gpuType` string, nullable
- `gpuCount` integer, nullable
- `isPacked` boolean, nullable
- `isIsolated` boolean, nullable
- `warmth` 'cold' | 'warm_provider' | 'warm_gateway' — 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.
- `deploymentEnvironment` string, nullable
- `orgId` string, nullable
- `workspaceId` string, nullable
- `userId` string, nullable
- `email` string, nullable
- `notebookId` string, nullable
- `cursor` string, nullable
- `limit` integer

## Response `200`

Successful Response

- FleetPage — One keyset page of the fleet, newest transition first.
  - `rows` SandboxRow[], required
    - `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
  - `nextCursor` string, nullable, required

## Other responses

- `422` — Validation Error

## Changes

- **2026-09-18** `abf76937a751` — 4 warning, 3 info
  - added the new `age_limit` enum value to the `rows/items/causeOfDeath/anyOf[subschema #1: CauseOfDeath]/` response property for the response status `200`
  - added the new `lease_expired` enum value to the `rows/items/causeOfDeath/anyOf[subschema #1: CauseOfDeath]/` response property for the response status `200`
  - added the new `lease_released` enum value to the `rows/items/causeOfDeath/anyOf[subschema #1: CauseOfDeath]/` response property for the response status `200`
  - added the new `readiness_failed` enum value to the `rows/items/causeOfDeath/anyOf[subschema #1: CauseOfDeath]/` response property for the response status `200`
  - …3 more
- **2026-09-15** `21910d3ae5d8` — 3 info
  - added the new optional `query` request parameter `email`
  - added the optional property `rows/items/orgName` to the response with the `200` status
  - added the optional property `rows/items/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/fleet/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/d7f2f3b8e82d?raw)
