Skip to content
sistemo.io beta

Errors

The API uses standard HTTP status codes plus a JSON body:

{ "detail": "Machine quota exceeded (limit 1).", "code": "quota_exceeded" }
Field Meaning
detail Human-readable message (safe to show or log)
code Stable machine-readable code (may be absent on generic errors)

Status codes

Status Meaning What to do
400 Bad request (invalid/missing fields) Fix the request body
401 Unauthenticated Missing/invalid/revoked API key — check Authorization
402 Payment required Your balance is at €0, or too small for what you asked — top up. Freeing resources does not help
403 Forbidden Read key on a mutating route, or quota exceeded — use a full key / free capacity
404 Not found Resource doesn't exist or isn't yours
409 Conflict Machine isn't running, or exec_id already used
410 Gone Job existed and its output was reaped (gone) — do not re-run blindly. Or a retired route (endpoint_retired) — the detail says what replaced it
429 Rate limited Back off and retry (see below)
501 Not implemented Guest image too old for jobs — rebuild/update the agent
503 Unavailable If the body has exec_id, the command may be running — poll it
5xx Server error Transient — retry with backoff; if persistent, contact support

A 403 with no JSON body did not come from us

Cloud is behind a WAF, and it refuses some default client user-agents before the request reaches the API. Tell the two apart by the body, not the status:

Our 403 The WAF's 403
content-type application/json text/plain
Body {"detail": "...", "code": "forbidden"} error code: 1010
Headers — server: cloudflare, cf-ray

If you see the right-hand column, no API key or scope will fix it — set a real User-Agent (see Authentication). Python's stdlib default, Python-urllib/x.y, is the one people hit; both SDKs send their own and are unaffected.

code values

Verified against the live API, 2026-09-22. These are the complete set a public API caller can receive.

code Status Meaning
validation_error 400 A field, query parameter or cursor was invalid. The message names it
forbidden 403 The action is not allowed for this caller
quota_exceeded 403 An account limit was reached. The message names which one and how much is in use. Retry succeeds once that resource is freed — unlike forbidden, so branch on code, not the status. See Quotas
insufficient_balance 402 The prepaid balance cannot pay for this — create, start, a volume or a snapshot at €0. Top up; unlike quota_exceeded, freeing something will not help
not_found 404 Unknown id, or not owned by you
conflict 409 The resource is in the wrong state — e.g. exec on a machine that is not running, or deleting a machine that still has snapshots
invalid_state_transition 409 The requested lifecycle change is not legal from the current state
gone 410 The job existed, but its record and output have been reaped. Do not re-run it blindly — it already ran
endpoint_retired 410 The route was removed on purpose; the detail names its replacement. POST /v1/machines/{id}/exec answers this since 2026-10-11
rate_limited 429 Per-account limit hit
internal_error 500 Server error. Transient — retry with backoff
host_agent_error 502 / 503 The control plane could not reach the host. Retryable
no_host_available 503 No host could take the machine right now. Retryable

Authentication failures carry no code at all

This is the one case where branching on code does not work, and it is worth knowing before you write the handler. Auth and scope refusals are produced by middleware that runs before the handler layer, and it emits detail only:

What happened Status Exact body
Key missing, malformed or revoked 401 {"detail":"invalid API key"}
A read key on a mutating route 403 {"detail":"this API key is read-only; a full-scope key is required for this action"}

So err.code is undefined/None for both. Branch on the status for these two, and on code for everything else. The SDKs already do this: a 401, or a 403 whose code is not quota_exceeded, becomes an AuthError.

⚠ Earlier versions of this page listed invalid_api_key and forbidden_scope as codes, and machine_not_running for a 409. None of those three are ever sent. If your error handling branches on them, it has never fired — use the table above.

Handling rate limits

On 429, wait and retry with exponential backoff. Mutating routes and exec have separate limits, so a heavy exec loop won't block creates and vice-versa.

How the SDKs surface these

Both SDKs map responses to typed exceptions, each carrying status, detail and code:

Exception Raised for
AuthError 401, and a 403 that is not a quota refusal
QuotaExceededError 403 with code: quota_exceeded
NotFoundError 404
RateLimitError 429
APIError everything else — and the base class of all four

Catch QuotaExceededError before AuthError

Both arrive as 403 and they need opposite handling, so the SDKs split them deliberately: QuotaExceededError is not a subclass of AuthError. An auth failure never succeeds on retry; a quota refusal succeeds as soon as you free the resource it names.

Folding them together is what sends people off to rotate a perfectly good API key. Order your except / catch blocks accordingly — and if you only want one branch, catch APIError, which is the parent of all of them.

from sistemo import Sandbox, QuotaExceededError, AuthError

try:
    sb = Sandbox()
except QuotaExceededError as e:      # first — it is the narrower case
    print("free something and retry:", e.detail)
except AuthError as e:               # second
    print("credential problem:", e.detail)
else:
    with sb:                         # closes the machine on the way out
        print(sb.run("uname -a").stdout)

See Python errors / JavaScript errors and Quotas.