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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | no | auto-generated | Human-readable name |
image | string | no | ubuntu:22.04 | Container 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. |
size | enum | no | small | xs, small, medium, large |
policyId | string | no | — | Attach a policy by id |
ports | {host?, container}[] | no | auto | Port 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.url | string | no | — | Git repo to clone into /workspace. Auth via the org's GITHUB_TOKEN secret if set. |
repo.branch | string | no | default branch | Branch 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"}'
Body — name (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"}'
Body — name (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 }