Skip to content

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-instance message / permissions/{request_id} / forget / qr routes.

/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:

{"detail": "ambiguous 'f2c456fd' — matches f2c456fd-aaaa-…, f2c456fd-bbbb-…; use more characters"}

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.

curl -H "Authorization: Bearer clauster_pat_…" https://clauster.example/api/v1/instances

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):

ui:
  enabled: false

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 — a claude --bg background (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.