Snapshots API
Lists the API routes that show and delete volumes, their lineage of snapshots, and their named versions.
Last updated
On this page
- What a snapshot freezes
- How long a snapshot lives
- GET /v1/volumes
- GET /v1/volumes/{key}/history
- GET /v1/volumes/{key}/refs
- DELETE /v1/volumes/{key}/snapshots/{snapshot}
- DELETE /v1/volumes/{key}
- Names and versions
- GET /v1/volumes/{key}/{name}
- GET /v1/volumes/{key}/{name}/versions
- GET /v1/volumes/{key}/{name}/versions/{n}
- DELETE /v1/volumes/{key}/{name}
- DELETE /v1/volumes/{key}/{name}/versions/{n}
- Readers
What a snapshot freezes
A snapshot records the definition of its source, and a restore defaults to it. A workspace snapshot freezes image, packages, locks, and resources; an environment snapshot freezes services. The environment a desk connects to is not frozen.
Sync points, which a clone cuts from its source, are not snapshots. They are never listed, never a restore target, and cannot be deleted by hand.
How long a snapshot lives
Every snapshot expires. A request to take a snapshot chooses when:
- Absent
ttl_secsandnever: the region's default, 30 days unless an admin changed it. ttl_secs: an integer from 3600 (one hour) to 31536000 (one year). Outside that is422.never: true: no expiry. Sendingneverwithttl_secsis400.
The response returns expires_at, null for never. Only the versions routes of a named volume return it later.
Using a snapshot extends it. Restoring or cloning from it, or starting a capsule from it, moves its expiry to a full default life from now, if that is later. It never shortens an expiry and never touches a snapshot that never expires.
Every ten minutes the platform deletes the snapshots past their time, by the rules of a delete by hand. A snapshot that a running workspace or environment stands on is kept and tried again. Deleting the last snapshot of a detached volume deletes the volume.
GET /v1/volumes
The volumes you can read that have at least one snapshot, one row each, sorted by id. A volume with only sync points is not listed. Without owner, the list holds your personal volumes and those of every team you belong to. A device workspace has no volume and is not listed.
| Query | Meaning |
|---|---|
kind | workspace or environment: keep one kind |
owner | one owner (you, or a team you belong to). Your handle keeps your personal volumes. A team keeps the team's volumes, your workspaces in that team included. It never widens your set |
Response: 200, an array of volume rows.
[ { "id": "env-1f2e3d4c5b6a7988", "name": "go-base", "latest_version": 2, "kind": "environment", "volume": "env-1f2e3d4c5b6a7988", "display_name": "acme-dev", "deleted": false, "latest_ms": 1760779800000, "snapshots": 3, "last_push_at": "2026-10-18T09:30:00Z", "usedBytes": 4194304000, "usedAt": "2026-10-18T09:35:00Z" }]| Field | Type | Meaning |
|---|---|---|
id | string | the volume id, the id of the workspace or environment it was cut from. Use it as {key} below |
name | string or null | the name its first push gave it, or null for a volume never named |
latest_version | integer or null | the highest version a push has numbered; null while the volume is unnamed |
kind | string | workspace or environment |
volume | string | the same as id |
display_name | string | what the source is called; once it is gone, the name its newest snapshot recorded, else the volume name |
deleted | boolean | the workspace or environment is gone; its snapshots are not. false when the cluster could not be asked |
latest_ms | integer or null | epoch milliseconds of the newest record on the volume, sync points included; approximate |
snapshots | integer | how many snapshots, in any phase but error |
last_push_at | string or null | RFC 3339; when the newest snapshot became ready; null while the only one is still being cut |
used_by | array | Each live workspace or environment on the volume as {kind, name, id}; the one named like the volume comes first |
usedBytes | integer or null | what the volume occupies on disk, as its node last measured it; null until a node has |
usedAt | string or null | RFC 3339; when that measurement was taken. The number is as of then, not live |
Errors:
403— a desk tool token asking for an owner outside its own team.404—?ownernames an owner that you cannot read.
GET /v1/volumes/{key}/history
The snapshots of one volume, newest first.
Response: 200, an array of snapshot rows.
[ { "id": "env-1f2e3d4c5b6a7988-8a7b6c91", "version": 2, "state": { "kind": "environment", "services": [ { "name": "db", "image": "postgres:16@sha256:5b1c…", "command": [], "env": { "POSTGRES_PASSWORD": "dev" }, "mounts": [{ "folder": "pgdata", "path": "/var/lib/postgresql/data" }], "ports": [5432] } ] }, "lineage": [], "region": "", "message": "before the schema change", "createdAt": "2026-10-18T09:30:00Z", "parent": "env-1f2e3d4c5b6a7988-3c2d1e4f", "phase": "ready" }]| Field | Type | Meaning |
|---|---|---|
id | string | the snapshot id; what restore, clone, and delete take |
version | integer or null | the number its push took under the volume name; null for an unnamed volume |
state | object or null | what was frozen, tagged by kind. {"kind": "workspace", "image", "packages", "locks", "resources"} or {"kind": "environment", "services"}. null for an old snapshot that froze nothing |
lineage | array | always []; kept for older clients |
region | string | always ""; kept for older clients |
message | string or null | the note from the snapshot |
createdAt | string or null | RFC 3339 |
parent | string or null | the snapshot this one followed |
phase | string | pending, working while it is being cut, ready, or error |
Sync points are not listed. Rows in error are listed and do not count in a volume's snapshots.
Query: include=working changes the response to {"history": [...], "working": [...]}. Without it, the response stays the bare array.
Each row of history then has working_copies, an array of the names of the workspaces and environments whose head is that snapshot. Each working entry shows one live workspace or environment on the volume.
| Field | Type | Meaning |
|---|---|---|
kind | string | workspace or environment |
name | string | the name of the working copy |
id | string | the id of the working copy |
state | string | running or stopped |
base | string or null | the snapshot the working copy was built from. For a clone, the newest push of the source at the time of the clone |
head | string or null | the latest push of the working copy |
Errors:
400—invalid name:{key}is not a plain name.404— the volume is unknown, has no snapshots, or is not yours.
GET /v1/volumes/{key}/refs
The newest push of the volume by time: the snapshot a clone or restore grafts onto by default. There is one ref, latest. It is not a branch tip, because clones fork the lineage.
Response: 200. A volume with no snapshot, and one you cannot read, both answer {"latest": null}, not 404.
{ "latest": "env-1f2e3d4c5b6a7988-8a7b6c91" }Errors:
400—invalid name.
DELETE /v1/volumes/{key}/snapshots/{snapshot}
Deletes one snapshot. If it was the last snapshot of a volume that no workspace or environment is on any more, the volume is deleted with it. Takes no body.
Response: 204 with an empty body.
Errors:
400—invalid name.404— the volume is not yours or has no snapshots, or the snapshot is not on it.409—a sync point cannot be deleted by hand;this snapshot is the base of {kind} {display}: a workspace or environment, of any owner, was created from it;this snapshot is where {kind} {display} stands now (its latest push): it is that parent's newest push;version {n} of {name} is used by {readers}, see Readers.503—the cluster could not be reached: the API cannot check whether anything is running on the volume, so nothing is deleted.pipelines cannot be read right now; retry: the API cannot read the readers.
DELETE /v1/volumes/{key}
Deletes a detached volume and every snapshot on it. Takes no body.
Response: 204 with an empty body.
Errors:
400—invalid name.404— the volume is not yours, or does not exist.409—the volume still has a workspace or environment;this volume also holds snapshots owned by someone else; delete your own snapshots instead;volume {name} is used by {readers}, see Readers.503—the cluster could not be reached;pipelines cannot be read right now; retry.
Names and versions
A volume gets a name on its first push. The name is unique among the volumes of one owner, a person or a team. Each push of a named volume is a version, numbered 1, 2, 3, and so on. A number is never reused, so a deleted version leaves a gap.
The three routes below read a volume by name. In them, {key} is the owner: your handle, or a team you belong to. The same position holds a volume id in the routes above.
A volume of another owner answers 404, the same as a volume that does not exist. This holds for a name and for an id.
A reference is name, name@N, or a snapshot id. name is the newest ready version. name@N is version N, written without a leading zero. Restore, clone, and from_snapshot take a reference.
GET /v1/volumes/{key}/{name}
One named volume.
Response: 200.
{ "id": "ws-0a1b2c3d4e5f6a7b", "name": "go-base", "owner": "acme", "kind": "workspace", "latest_version": 4, "version_count": 3, "deleted": false, "used_bytes": 4194304000, "used_at": "2026-10-18T09:35:00Z"}| Field | Type | Meaning |
|---|---|---|
id | string | the volume id |
name | string | the volume name |
owner | string | {key}, in lowercase |
kind | string | workspace or environment |
latest_version | integer or null | the highest number a push took; null before the first numbered push |
version_count | integer | how many versions exist now |
deleted | boolean | no workspace or environment uses the volume |
used_bytes, used_at | integer or null, string or null | as in GET /v1/volumes |
Errors:
404—volume {name} does not exist: the name is unknown, or the volume is not yours.
GET /v1/volumes/{key}/{name}/versions
The versions of the volume, newest first.
Response: 200, an array of version rows.
[ { "version": 4, "snapshot": "ws-0a1b2c3d4e5f6a7b-8a7b6c91", "phase": "ready", "parent_version": 3, "message": null, "created_at": "2026-10-18T09:30:00Z", "ready_at": "2026-10-18T09:30:12Z", "expires_at": "2026-11-17T09:30:00Z", "saved_by": { "pipeline": "build", "run": "1760779800000-a1b2c3", "job": "base" } }]| Field | Type | Meaning |
|---|---|---|
version | integer | the number of the push |
snapshot | string | the snapshot id; what restore and clone take |
phase | string | pending, working, ready, or error |
parent_version | integer or null | the version of the snapshot that this one followed. A version does not contain the version of another run that has the same parent |
message | string or null | the note from the push |
created_at, ready_at | string or null | RFC 3339 |
expires_at | string or null | RFC 3339; null when the version never expires |
saved_by | object or null | {pipeline, run, job} when a pipeline job saved it, else null |
Sync points and unnumbered pushes are not listed. Versions in error are listed.
Errors:
404—volume {name} does not exist.
GET /v1/volumes/{key}/{name}/versions/{n}
One version row, as above.
Response: 200.
Errors:
404—volume {name} does not exist;volume {name} has no version {n}.
DELETE /v1/volumes/{key}/{name}
Deletes a named volume and every snapshot on it. The rules are those of DELETE /v1/volumes/{key}. Takes no body. When the volume is gone, the name is free again.
Response: 204 with an empty body.
Errors:
404—volume {name} does not exist.409—the volume still has a workspace or environment;volume {name} is used by {readers}, see Readers.503—the cluster could not be reached;pipelines cannot be read right now; retry.
DELETE /v1/volumes/{key}/{name}/versions/{n}
Deletes one version. Takes no body. Deleting the last version deletes the volume.
Response: 204 with an empty body.
Errors:
404—volume {name} does not exist;volume {name} has no version {n}.409—a sync point cannot be deleted by hand;this snapshot is the base of {kind} {display};this snapshot is where {kind} {display} stands now (its latest push);version {n} of {name} is used by {readers}, see Readers.503—the cluster could not be reached;pipelines cannot be read right now; retry.
Readers
A delete is refused with 409 while a pipeline reads what it removes. The sentence lists the readers, for example volume go-base is used by pipeline build, run build #12.
- A pipeline reads a volume when the latest version of its definition names it in
fromSnapshot. A queued or running run reads it when its version does. - A pipeline that pins
name@Nblocks the delete of version N. - A pipeline that names the volume without a version blocks the delete of the last push of the volume.
- A job that only has
saveTofor the volume does not block a delete. The next run makes the volume again. - A version that a running or queued run started from is also kept.
Expiry follows the same rules. The platform skips a version that a reader needs and tries it again later.