Authentication & Errors
Overview
NanoTerm has two authentication layers:
| Layer | Purpose | Method |
|---|---|---|
| Managed identity | User signup and signin | Browser-based OAuth via the dashboard |
| API keys | Programmatic access from CLI, SDK, and the API | Authorization: Bearer nt_xxx |
API Keys
Every API request carries your key in the Authorization header:
curl https://api.nanoterm.dev/api/workspaces \
-H 'Authorization: Bearer nt_live_a1b2c3d4...'
Create and revoke keys in the dashboard's API Keys page or via the
CLI (nanoterm auth login will generate one for you).
The full key is shown once at creation time. Store it somewhere you can find it — there's no way to retrieve it later.
Permission levels
| Permission | Workspaces | Exec | Files | Admin (keys, org) |
|---|---|---|---|---|
full | read/write | yes | read/write | yes |
execute | read/write | yes | read/write | no |
read | read | no | read | no |
read keys are enforced at every route — including the terminal and
proxy WebSockets and the diff endpoint, all of which shell out and
therefore count as exec. A read key that tries to open
/api/workspaces/:id/terminal or PUT to /files/* gets 403 FORBIDDEN, not a silent escalation. (This contract is regression-
tested; if a future route forgets the gate, CI fails.)
Passing the key on WebSocket connections
Browsers can't set custom headers on a WebSocket handshake, so the
terminal (/api/workspaces/:id/terminal) and proxy
(/api/workspaces/:id/proxy/:port) endpoints accept a ?token= query
parameter as a fallback only on WebSocket upgrade handshakes (the
request must carry Upgrade: websocket). Regular HTTP requests must
use the Authorization header — passing ?token= on a plain GET
is silently ignored to keep your API key out of access logs, CDN logs,
Referer headers, and browser history.
About the key format
Keys look like nt_<random>. The CLI and SDK don't need to know any
structure beyond that — just paste the key. Upgrading your plan takes
effect immediately for existing keys; no rotation required.
Rate Limits
API-key traffic (CLI, SDK, agent code) is rate-limited per plan:
| Plan | Requests / minute |
|---|---|
| Free | 60 |
| Developer | 600 |
| Team | 3,000 |
| Enterprise | Custom |
Exceeding the limit returns 429 Too Many Requests with a Retry-After
header telling you how many seconds to wait. The official CLI and SDK
auto-retry once when the wait is ≤5 seconds; longer waits throw a
clear error your code can catch. Upgrading your plan raises the ceiling
immediately — no key rotation needed.
Dashboard traffic (human sessions) is not subject to the per-plan cap. Every request still passes through a generic per-IP anti-abuse limit (1,000 rpm) that protects the service from floods.
Errors
Every API error looks the same:
{
"error": {
"code": "WORKSPACE_NOT_FOUND",
"message": "Workspace 'ws_abc' not found.",
"action": "Check the ID or list workspaces with GET /api/workspaces.",
"retryable": false,
"docs": "https://docs.nanoterm.dev/api-workspaces#get-workspace"
}
}
The fields are:
| Field | What it's for |
|---|---|
code | Stable programmatic identifier — match on this in your code |
message | Human-readable description |
action | One sentence telling you what to do next |
retryable | true if the same request might succeed later (network, rate limit); false if not (bad input, missing resource) |
docs | Deep-link to the relevant docs page |
Common status codes
These can come from any endpoint. Endpoint pages list additional endpoint-specific codes.
| Status | Code | Meaning |
|---|---|---|
400 | VALIDATION_FAILED | Request body failed validation. |
401 | UNAUTHORIZED | Missing or invalid Authorization header. |
403 | FORBIDDEN | Authenticated, but your key/role lacks the required permission. Body's action lists which role you'd need. |
404 | NOT_FOUND | Resource doesn't exist or belongs to another org. |
409 | CONFLICT | Unique constraint hit (e.g., duplicate slug). |
426 | CLI_VERSION_TOO_OLD | The official CLI must be upgraded — server has raised the minimum version. |
429 | RATE_LIMITED | Plan rate limit exceeded. |
429 | QUOTA_EXCEEDED | Workspace-count quota hit. |
429 | COMPUTE_QUOTA_EXCEEDED | Monthly compute-hours budget exhausted. |
429 | MEMBER_QUOTA_EXCEEDED | Plan's member cap reached (Free 1 / Dev 5 / Team 10 / Ent ∞). |
429 | STORAGE_QUOTA_EXCEEDED | Snapshot/storage budget exhausted. |
500 | INTERNAL_ERROR | Unhandled server error. Retry after a short backoff. |
501 | NOT_SUPPORTED | Feature isn't available on the current runtime. |
503 | RUNTIME_UNAVAILABLE | Container runtime temporarily unreachable. |
426 in practice
The server reads the X-Client-Name and X-Client-Version headers
that the official CLI/SDK send. If X-Client-Name === "nanoterm-cli"
and the version is below the documented minimum, every request returns
426. Other clients (curl, dashboards, your own SDK code) are not
gated by this check.
HTTP/1.1 426 Upgrade Required
Content-Type: application/json
{
"error": {
"code": "CLI_VERSION_TOO_OLD",
"message": "CLI 0.1.2 is below the minimum required version (0.1.3).",
"action": "Run: npm install -g @nanoterm/cli@latest",
"retryable": false
}
}
429 in practice
HTTP/1.1 429 Too Many Requests
Retry-After: 17
Content-Type: application/json
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Slow down requests or upgrade your plan.",
"action": "Retry after 17s, or upgrade at https://nanoterm.dev/pricing",
"retryable": true
}
}