REST API reference¶
The control plane exposes a versioned REST API under /v1. This page covers the
cross-cutting conventions; the full interactive specification is embedded at the
bottom and is generated from
api/openapi/control-plane.yaml,
the source of truth.
Base URL and versioning¶
All endpoints live under /v1 (for example http://localhost:8080/v1/vms).
Breaking changes go to a new major path (/v2); /v1 is never modified in a
breaking way. Health probes (/healthz, /readyz) live outside /v1 so
Kubernetes probes are not coupled to the API version.
Authentication¶
Both schemes use the Authorization: Bearer <token> header; the server picks the
verifier by token shape:
| Credential | Shape | How to get one |
|---|---|---|
| JWT access token | a signed JWT (no prefix), 15 min default TTL | POST /v1/auth/login |
| API token | otx_<base64url> |
otherix config add cluster, or POST /v1/users/me/api-tokens |
# Login -> access token
TOKEN=$(curl -s -X POST http://localhost:8080/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"..."}' | jq -r .access_token)
curl -s http://localhost:8080/v1/vms -H "Authorization: Bearer $TOKEN"
Anonymous endpoints (POST /v1/auth/login, POST /v1/auth/refresh, GET /v1/ca,
POST /v1/nodes/join) declare security: []. See
Users and RBAC for roles and scopes.
Error envelope¶
Every error response has the same shape:
code is a stable snake_case string; message is human-readable; details is
optional. The full catalog is in Error codes.
404, not 403, for invisibility
A resource that exists but is not visible to the caller returns 404 not_found,
never 403. Existence is never leaked.
Pagination¶
List endpoints use opaque cursor pagination:
- Query:
limit(1..200, default 50) andcursor(opaque base64). - Response:
{ "data": [...], "meta": { "next_cursor": "<string|null>" } }.
There is no total count. A null next_cursor means the last page. Treat the
cursor as opaque.
Idempotency¶
Every mutating request (POST/PATCH/DELETE) accepts an optional Idempotency-Key
header (up to 255 chars, 24 h TTL):
- Same key + same body within the TTL replays the original response.
- Same key + different body returns
409 idempotency_key_mismatch.
curl -s -X POST http://localhost:8080/v1/vms \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{ "name": "demo", "image_url": "...", "architecture": "amd64" }'
Carve-out
POST /v1/auth/login and POST /v1/auth/refresh do not accept an
Idempotency-Key - replaying a cached token pair would break refresh-token
rotation.
Async operations¶
Operations that touch a node or take more than a moment are asynchronous:
- The request returns
202 Acceptedwith{ "task_id": "...", "status": "pending", "links": { "self": "/v1/tasks/{id}" } }. - Poll
GET /v1/tasks/{task_id}untilstatusreachessuccess,failed, orcancelled.POST /v1/tasks/{id}/cancelis best-effort.
The otherix CLI hides this behind --wait. VM create/delete, the VM lifecycle
verbs (start/stop/poweroff/reboot), and storage-pool scans are async; pause,
resume, reset, and console-token issuance are synchronous 200s. See
Desired vs observed state.
Conventions¶
- Paths are plural nouns; VM sub-operations are sub-resources
(
/v1/vms/{id}/start).{id}for VMs and nodes is the name (UUID literals are rejected with400). - Datetimes are RFC 3339; UUIDs are
format: uuid. operationIdis<resource>.<action>(users.list,apiTokens.createMe).
Resources¶
| Tag | Guide |
|---|---|
auth, users, api-tokens |
Users and RBAC |
nodes, join-tokens |
Join a node |
vms |
Create and manage VMs |
storage-pools |
Storage pools |
networks |
Networks |
tasks |
(async operations, above) |
Browse the full API¶
The complete specification is embedded below. You can also browse it locally with Swagger UI and Redoc: