Every node serves one HTTPS API and the web UI on web.address:web.port (default port 8443). The web UI uses exactly this API, so anything you can click you can script. All paths are under /api, /auth, /oauth, /.well-known or /mcp; any other GET returns the web UI. Examples use cloud.example.com and a key in $KEY.
note
This page was written from reading the code. Only unauthenticated requests were tried against a running node: API paths answer 401 with JSON, and any other GET returns the web UI.
API key. Send Authorization: Bearer clk_<id>_<secret>. An API key is always an admin. Only a hash is stored; the plaintext is returned once by POST /api/keys. The first key of a new cluster is called bootstrap and is printed to the log once.
Session cookie.POST /auth/login with {"key": "clk_..."} returns the identity and sets the cl_session cookie (HttpOnly; Secure; SameSite=Lax). An OIDC login (access and login) sets the same cookie. The cookie is how browser EventSource streams authenticate, because they cannot send headers.
In the tables below, Auth means:
Value
Meaning
none
Public.
user
Any signed-in caller: an API key or a session.
admin
An API key, or an OIDC session whose admin claim matches. A viewer gets 403admin role required.
token
A secret token in the URL path.
internal
Node-to-node, signed with the cluster key. Not usable from scripts.
A malformed Authorization header (for example Basic ...) is a 401 even if a valid cookie is also sent. The Bearer prefix is optional on the dashboard API but required on /mcp.
Send requests to any node. You do not need to know the leader.
Request on a follower
Result
GET under /api
Answered from that node's replicated state. It may lag; compare state_version from /api/status. Live container, usage and daemon-log data comes from the node that owns it.
POST, PUT, PATCH, DELETE under /api or /auth
Relayed to the leader (method, headers and a body up to 32 MiB). The leader's status, headers and body come back to you.
/auth/oidc/*
Served locally, so login works from any node.
/api/git-backup/webhook, /api/git-backup/webhook/internal-trigger, any path containing /internal/
Served locally.
GET /oauth/authorize
307 redirect to the leader.
POST /oauth/token, POST /oauth/register
Relayed to the leader.
/mcp
Relayed to the leader (body up to 5 MiB).
No leader known
503.
The not-leader 503 with Location
Older documentation says a follower answers a write with 503 and a Location header pointing at the leader. The current code relays the write instead, and never sends a Location header, so clients do not need to follow redirects. The error type not the leader still exists (status 503) but is only built without a leader address.
An error is {"error": "<message>"}. Read only error; a detail field exists in the type but is never filled in.
Status
Meaning
400
Bad request (validation failed, bad body).
401
Not authenticated. On /mcp it adds WWW-Authenticate.
403
Forbidden (for example admin role required).
404
Not found (for example no instance <id>).
409
Conflict (for example an update already in progress, a stale settings save, this node holds no cluster key).
429
Too many requests.
503
Unavailable or not leader.
500
Internal error.
Two 503s are not JSON: the plain text no cluster leader elected yet (a follower with no known leader), and the JSON could not reach the leader: ... when the relay fails. Scripts that parse JSON errors must handle the plain-text one.
Each node probes its peers itself: raft is an authenticated raft ping, api a TCP connect to the web port. disk_ms is a write plus fsync on that node's data dir, null if unreported, failed, or disk_probe: false.
GET
/api/tls-fingerprint
user
none
Fingerprint of the certificate this node serves
GET
/api/config
user
none
Cluster config JSON
PUT
/api/config
admin
{"config": {...}, "rollback_after": "60s"}
200
Applies the config and arms a rollback timer; call confirm before it expires.
POST
/api/config/validate
user
{"config": {...}}
{valid, errors: [{path, message}]}
POST
/api/config/confirm
admin
none
200
Keeps the pending config.
POST
/api/config/rollback
admin
none
200
Restores the previous config now.
GET
/api/config/pending
user
none
{armed, seconds_left}
POST
/api/config/networks/{name}/rename
admin
{"new_name": "..."}
200
Also rewrites stacks, redirects, static routes and WireGuard.
InstanceSummary has 23 fields: id, node, desired, state, running, health, image, image_digest, running_digest, started_at, migration, migration_problem, reason, step, placement_error, proxy, exit_code, oom_killed, restart_count, restart_limit, finished_at, memory_limit_bytes, crash_moves, plus the optional ones omitted when empty. A pending placement has state: pending_placement and a placement_error. Instances on another node are forwarded to their owner, not rejected; if that node is unreachable the state falls back to unknown.
Paths containing /internal/, plus /api/links/..., are for nodes and the clouduler CLI. They are signed with the cluster key (HMAC over method, path, query, timestamp, nonce and body hash), accepted once within a time window, and cannot be called from a script. Examples are /api/git-backup/pull, /api/git-backup/reconcile, /api/updates/internal/scan and /apply, /api/self-update/internal/restart, /api/instances/internal/*, /api/nodes/internal/* and /api/dns/internal-reconcile. Do not expose them.