Public API (/api/v1)¶
Clauster exposes a small, documented, versioned subset of its HTTP surface under
/api/v1 so you can drive it from your own scripts and apps instead of the
dashboard's internal /api/... routes. This is single-operator scoped: every
token authenticates as the same operator identity the dashboard session does —
there is no per-user/multi-account auth (see Security).
What's public vs internal¶
- Public (
/api/v1/...), documented and stable. The resource endpoints: project list, session reads, instance spawn/stop/resume, agent spawn/stop/resume. - Internal (bare
/api/...only), unversioned, may change without notice. Everything else — HTML-fragment/partial routes the Alpine dashboard renders (/api/projects/{name}/row,/api/widget, and similar), config-write, environments, project create/clone, and the per-instancemessage/permissions/{request_id}/forget/qrroutes.
/api/v1/... is an alias, not a fork: each /api/v1 route is served by the
exact same handler as its bare /api/... counterpart, so the two always return
identical payloads for the same request. The dashboard keeps calling the bare
routes directly — this is purely additive; nothing about the internal surface
changed.
| Method | Path (bare /api/... and /api/v1/...) |
What |
|---|---|---|
| GET | /projects |
List projects |
| GET | /sessions |
External (unmanaged) working sessions, by project |
| GET | /sessions/tracked |
Live sessions owned by a managed bridge, by instance |
| GET | /sessions/adoptable |
Project names whose live external session is adoptable |
| GET | /instances |
Every managed bridge/hosted instance |
| POST | /instances |
Spawn a bridge/session |
| GET | /instances/{instance_id} |
Read one instance |
| DELETE | /instances/{instance_id} |
Stop an instance |
| POST | /instances/{instance_id}/resume |
Resume a stopped/crashed instance |
| GET | /agents |
List claude --bg background sessions |
| POST | /agents |
Dispatch a background session |
| DELETE | /agents/{job_id} |
Stop a background session |
| POST | /agents/{job_id}/resume |
Resume an ended background session, in instance_defaults.permission_mode (403 when the bypass ceiling refuses that mode; 409 when the session is still live, has no resumable session id, or has no recorded directory that can be resolved) |
A request to a route not on this list (e.g. GET /api/v1/widget) 404s — the
internal surface was never aliased, by design.
{instance_id} accepts a unique prefix¶
Every {instance_id} path segment resolves a full instance id, a unique prefix of
one, or a project name (#1099).
Both exact forms outrank a prefix: a full instance id first, then a project name, then
a unique prefix. A segment naming one of your projects is never reinterpreted as a
prefix — if that project has no instance the reply is 404, never another project's
bridge. This matters only for a hex-ish project name (cafe, deadbeef) that happens to
prefix an unrelated instance's UUID; the id stays reachable by typing one more character.
A reference matching several bridges is refused with 409 rather than resolved to
an arbitrary one — acting on the wrong live session is unrecoverable — and the detail
names the candidates:
Two shapes reach the 409: an id prefix matching several bridges (#1099), and a bare
project name matching several instances (#1150) — the latter ends the hint with use
an instance id directly, since a fixed name can't be narrowed. An id matching nothing is
still 404. A prefix that used to 404 now 409s; a bare project name with two instances
used to return 200 (silently picking the last-registered row) and now 409s
deliberately.
Deprecated instance fields¶
Every instance response still has the key sandbox_mode, so that a /api/v1
client keeps working. The key is deprecated, and its value is always "default".
It never shows "on" or "off". This holds for an instance that an older
Clauster stored with "on" or "off".
The per-launch sandbox option is removed and was ignored. To sandbox sessions,
set sandbox.enabled in a Claude Code settings file. See
Sandboxing sessions. A sandbox value in
a POST /instances body is accepted and ignored, and a successful response
carries the same note in warnings.
Authenticating¶
Every /api/v1 request needs the same credential any other /api/* route
needs whenever auth.enabled is set: a session cookie, trusted-reverse-proxy
auth, or an Authorization: Bearer <token> header. A headless/API client uses a
Bearer token — see Named API tokens
below to mint one. With auth.enabled: false the whole API (bare and /api/v1
alike) is unauthenticated, matching the existing /api/* posture — safe only on
loopback, because a non-loopback bind refuses to start without enforced auth
unless auth.allow_unauthenticated_network explicitly opts out.
Named API tokens (clauster api-token)¶
Tokens are managed CLI-first — there is no dashboard surface for token
management yet (planned for a later release). Each token has a unique,
operator-chosen label; only its SHA-256 hash is ever stored, and a raw
token is shown to you exactly once, at issue/rotate time. On issue, rotate,
and revoke the label can be given either as a positional argument or with
--label — the two forms are equivalent.
clauster api-token issue ci-runner # mint + print a new token (once)
clauster api-token list # label, created, last-used — never the token
clauster api-token rotate ci-runner # mint a fresh secret for an existing label
clauster api-token revoke --label ci-runner # delete a token; it stops authenticating immediately
Tokens never expire by default and are revocable at any time. The legacy
single auth.api_token_hash config field (from clauster hash-token, #360)
keeps working unchanged alongside any named tokens — it's checked as one more
accepted credential, so upgrading doesn't invalidate an existing deployment's
token.
OpenAPI docs (/docs, /openapi.json)¶
Off by default: api.openapi_enabled: false means the routes don't exist at
all (a 404), so an undocumented deployment never advertises its own shape to a
prober. Set api.openapi_enabled: true in clauster.yml (restart required) to
serve the interactive docs UI at /docs and the raw schema at /openapi.json.
Both sit behind the same auth guard as every other /api/* route — an
unauthenticated request gets a 401, not a login-page redirect, and there is
no way to expose docs without also exposing the API they document. See the
api section in Configuration for the full toggle
reference.
API-only deployment¶
Set ui.enabled: false (default true, restart required) to run Clauster as a
pure JSON API with no browser dashboard at all (#806):
With the UI off, these all 404: the / dashboard page, /login + /logout,
/static/*, and the internal HTML-fragment / per-session interactive routes
(/api/projects/{name}/row, /api/widget, and the per-instance message /
permissions/{request_id} / forget / qr routes) — the exact "internal,
unversioned" set described above. Everything else is unaffected: the rest of
the bare /api/... routes, /api/v1/..., /healthz, /metrics (per its own
gate), and the WebSocket streams all keep working, still behind whatever auth
is configured.
ui.enabled is independent of api.openapi_enabled and every auth.*
setting — none of them implies another. Every combination is valid: dashboard
and API together (the default), API-only, a UI with /docs off, and so on.
No login page means no session-cookie auth
With the UI off there is no /login route, so session-cookie (and
password) auth is unreachable. Only a Bearer token — the legacy
auth.api_token_hash or a named clauster api-token — or a trusted
reverse proxy can still authenticate. If auth.enabled is on and neither
is configured, Clauster logs a loud startup warning rather than refusing to
start (refusing outright would brick a deployment that flips ui.enabled
off before minting a token — a stricter fail-closed refusal is a
reasonable alternative and open to revisiting). Mint a token first:
clauster api-token issue --label ci-runner.
Lifecycle webhooks¶
Beyond the inbound /api/v1 reads, Clauster can push an outbound machine-readable
JSON POST to your own HTTP endpoint on a lifecycle transition — for wiring it into an
automation, a queue, or your own dashboard. Enable and configure them under webhooks:
in clauster.yml (see the
webhooks field reference),
and see Operations → Lifecycle webhooks for the
operational behaviour (fail-open, no retry, which bridges emit). This section is the
payload reference.
Bridge events (spawn / ready / stop / crash)¶
Each bridge event is a single JSON POST body of the shape:
{
"event": "ready",
"project": "my-project",
"label": "my-project",
"status": "running",
"resume_mode": "standard",
"spawn_mode": "same-dir",
"session_ref": "a1b2c3d4e5f60718"
}
The event is one of spawn / ready / stop / crash. status is the bridge's
lifecycle status at emit time. session_ref is a stable, non-reversible correlation
token — a 16-hex-char (64-bit) HMAC-SHA256 prefix keyed by a per-deployment secret — so
a receiver can group the spawn / ready / stop / crash events of one session
without ever holding the raw session id. The raw session_<ULID> is deliberately
never egressed: it is bearer-equivalent (anyone holding it can open a New Session
composer for the bridge), so it is stripped from every egress surface. session_ref is
null until a session attaches.
Extended events (bg-settled / permission-needed / clone-done)¶
Beyond the four bridge events, Clauster emits three more lifecycle signals. Each
defaults to disabled — set its key to true in events to turn it on. They do not
reuse the bridge payload shape: each carries an event_type discriminator so a receiver
can branch on the body without parsing event.
bg-settled— aclaude --bgbackground (agent-view) job reached a terminal state via the supervisor stop path.
{
"event": "bg-settled",
"event_type": "bg-settled",
"id": "a1b2c3d4",
"settled": true,
"removed": true,
"detail": null
}
settled is true only for a confirmed cloud-deregistering stop; removed reports
whether the job row was dropped. detail (a human-readable note, or null) is redacted
before egress.
permission-needed— a hosted session parked a tool-permission prompt that needs an explicit approve/deny. This is the highest-value "come look" signal, which is why it defaults off (enabling it egresses an attention signal). It carries only the session's process id and the request subtype — never the prompt body, which can contain a tool path or argument.
{
"event": "permission-needed",
"event_type": "permission-needed",
"process_id": "0f1e2d3c4b5a6978",
"subtype": "can_use_tool"
}
clone-done— a project clone finished.
{
"event": "clone-done",
"event_type": "clone-done",
"project": "my-project",
"status": "done",
"error": null
}
status is done or error. The clone URL is never sent (it can carry
credentials); on a failure, error is the redacted failure detail.