Skip to main content

Workspaces API

All endpoints require an Authorization: Bearer nt_… header and share the error contract. Status codes not listed per-endpoint (e.g. 401, 429, 500) still apply.

Create Workspace

curl -X POST https://api.nanoterm.dev/api/workspaces \
-H 'Authorization: Bearer nt_…' \
-H 'Content-Type: application/json' \
-d '{
"name": "my-agent",
"image": "ubuntu:22.04",
"size": "small"
}'

Body

FieldTypeRequiredDefaultDescription
namestringnoauto-generatedHuman-readable name
imagestringnoubuntu:22.04Container base image. Must be one of the curated images returned by GET /api/info (or match a server-configured ALLOWED_IMAGE_PATTERNS entry). Unrecognised values return 400 INVALID_IMAGE.
sizeenumnosmallxs, small, medium, large
policyIdstringnoAttach a policy by id
ports{host?, container}[]noautoPort mapping. host optional — omit to auto-pick in the container + 10000 band; provide to pin. A pinned port that collides returns 409 PORT_IN_USE.
repo.urlstringnoGit repo to clone into /workspace. Auth via the org's GITHUB_TOKEN secret if set.
repo.branchstringnodefault branchBranch to check out

Responses

201 Created

{
"id": "ws_a1b2c3d4",
"name": "my-agent",
"image": "ubuntu:22.04",
"size": "small",
"status": "running",
"ports": [{ "host": 13000, "container": 3000 }],
"createdAt": "2026-04-18T08:00:00.000Z"
}

400 VALIDATION_FAILED

{ "error": { "code": "VALIDATION_FAILED", "message": "size: invalid enum value" } }

400 INVALID_IMAGE

The image you sent isn't on the server's allowlist. The default allowlist is the curated set surfaced by GET /api/info; server operators can extend it with the ALLOWED_IMAGE_PATTERNS env var (comma-separated regex sources matched against the full image string). This gate is what stops a caller from pulling a workspace from attacker.example/x:latest — both an SSRF and a supply-chain hole.

{
"error": {
"code": "INVALID_IMAGE",
"message": "Image 'attacker.example/x' is not allowed.",
"action": "Use one of the curated images from GET /api/info...",
"retryable": false
}
}

409 PORT_IN_USE

Returned when you pin an explicit host port that's already held by another active workspace. Retry with a different host or omit host to let the server pick.

{ "error": { "code": "PORT_IN_USE", "message": "Host port 13000 is already in use by another workspace." } }

429 QUOTA_EXCEEDED

Workspace-count quota hit. Different from RATE_LIMITED (RPM).

{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Workspace limit reached (10/10).",
"action": "Terminate unused workspaces or upgrade your plan.",
"retryable": false
}
}

429 COMPUTE_QUOTA_EXCEEDED

Monthly compute-hours budget for the plan is exhausted. Consumption is tallied from the audit log of workspace.start / workspace.stop events for the current calendar month.

{
"error": {
"code": "COMPUTE_QUOTA_EXCEEDED",
"message": "Monthly compute hours exhausted (50/50 h).",
"action": "Wait for the cycle to reset on the 1st, or upgrade your plan.",
"retryable": false
}
}

503 RUNTIME_UNAVAILABLE

{
"error": {
"code": "RUNTIME_UNAVAILABLE",
"message": "Container runtime not available.",
"action": "Start Docker Desktop or `podman machine start`.",
"retryable": true
}
}

List Workspaces

curl https://api.nanoterm.dev/api/workspaces \
-H 'Authorization: Bearer nt_…'

200 OK

[
{
"id": "ws_a1b2c3d4",
"name": "my-agent",
"image": "ubuntu:22.04",
"size": "small",
"status": "running",
"createdAt": "2026-04-18T08:00:00.000Z"
}
]

Get Workspace

curl https://api.nanoterm.dev/api/workspaces/ws_a1b2c3d4 \
-H 'Authorization: Bearer nt_…'

200 OK

Single WorkspaceInfo (same shape as the list entries).

404 NOT_FOUND

Returned both when the workspace doesn't exist and when it belongs to another org — we don't leak existence across tenants.

{ "error": { "code": "NOT_FOUND", "message": "Workspace 'ws_a1b2c3d4' not found" } }

Stop / Start Workspace

curl -X POST https://api.nanoterm.dev/api/workspaces/ws_a1b2c3d4/stop \
-H 'Authorization: Bearer nt_…'

curl -X POST https://api.nanoterm.dev/api/workspaces/ws_a1b2c3d4/start \
-H 'Authorization: Bearer nt_…'

Stop preserves the filesystem; start resumes from it. Compute billing pauses while stopped.

200 OK

Updated WorkspaceInfo with status transitioned to stopped or running.

400 WORKSPACE_STOP_FAILED / WORKSPACE_START_FAILED

Illegal transition (e.g. starting a running workspace).


Terminate Workspace

curl -X DELETE https://api.nanoterm.dev/api/workspaces/ws_a1b2c3d4 \
-H 'Authorization: Bearer nt_…'

Irrevocable — removes the container and marks the row removed.

200 OK

{ "ok": true }

Snapshots

Snapshots commit the running container to a local image. Restoring creates a new workspace from that image; the source is untouched. Podman-only — the Kubernetes runtime returns 501 NOT_SUPPORTED.

Create Snapshot

curl -X POST https://api.nanoterm.dev/api/workspaces/ws_a1b2c3d4/snapshots \
-H 'Authorization: Bearer nt_…' \
-H 'Content-Type: application/json' \
-d '{"name":"before-migration"}'

Bodyname (string, optional). Auto-generated timestamp if omitted.

201 Created

{
"id": "sn_x1y2z3",
"workspaceId": "ws_a1b2c3d4",
"name": "before-migration",
"imageRef": "nanoterm-snapshot:sn_x1y2z3",
"sizeBytes": 44040192,
"sourceImage": "ubuntu:22.04",
"createdAt": "2026-04-18T08:01:23.000Z"
}

400 SNAPSHOT_CREATE_FAILED

Source workspace must be running.

429 STORAGE_QUOTA_EXCEEDED

Your plan's snapshot storage budget is full. The estimate uses the source image size as a conservative upper bound.

{
"error": {
"code": "STORAGE_QUOTA_EXCEEDED",
"message": "Storage quota exceeded (4.0 GB used + ~2.5 GB for this snapshot > 5 GB).",
"action": "Delete old snapshots or upgrade your plan."
}
}

501 NOT_SUPPORTED

Running on the Kubernetes runtime.

List Snapshots

curl https://api.nanoterm.dev/api/workspaces/ws_a1b2c3d4/snapshots \
-H 'Authorization: Bearer nt_…'

200 OK

Array of Snapshot, newest first.

Restore Snapshot

curl -X POST https://api.nanoterm.dev/api/workspaces/ws_a1b2c3d4/snapshots/sn_x1y2z3/restore \
-H 'Authorization: Bearer nt_…' \
-H 'Content-Type: application/json' \
-d '{"name":"restored-agent"}'

Bodyname (string, optional). Defaults to <snapshotName>-restore.

201 Created

{ "workspaceId": "ws_newid01" }

429 QUOTA_EXCEEDED

Restore counts against the workspace quota.

Delete Snapshot

curl -X DELETE https://api.nanoterm.dev/api/workspaces/ws_a1b2c3d4/snapshots/sn_x1y2z3 \
-H 'Authorization: Bearer nt_…'

Removes the row and the local image.

200 OK

{ "ok": true }