REST surface for two operator-facing concerns: the persisted Clawboo settings (/api/settings, the upstream OpenClaw Gateway URL + token, plus a first-run dismiss timestamp) and the boot probe (/api/health + /api/health/recheck, a fresh-install health check that backs the System Health view and answers a one-field liveness query).
The raw gateway token is never returned by any route here. GET /api/settings exposes only hasToken: boolean; the same-origin proxy injects the upstream token server-side, so the browser never needs the credential.
All POST routes read a JSON body parsed by express.json({ limit: '2mb' }). Every error response is the standard { error: string } envelope (the health routes’ 500 path uses { ok: false, error }, see below).
Routes
| Method | Path | Summary | Stream? |
|---|
| GET | /api/settings | Read persisted settings (token redacted to hasToken) | No |
| POST | /api/settings | Persist gateway URL / token / first-run dismiss (partial update) | No |
| GET | /api/health | Latest boot report (or compute one on demand) | No |
| POST | /api/health/recheck | Recompute the boot report fresh | No |
GET /api/settings
Returns the current persisted settings from ~/.clawboo/settings.json (via loadSettings()). The gateway token is redacted to a presence boolean.
- Path/query params: none.
- Request body: none.
Responses
200 OK: the persisted settings, token-redacted:
{
gatewayUrl: string // e.g. "ws://localhost:18789" (may be empty)
hasToken: boolean // true if a non-empty gatewayToken is stored
firstRunDismissedAt: number | null // epoch ms when the first-run UI was dismissed, else null
}
500 Internal Server Error: settings could not be loaded:
{ "error": "Failed to load settings" }
Example
curl http://localhost:18790/api/settings
POST /api/settings
Persists settings with a partial update: only fields present in the body are written; an omitted field is left unchanged (so a dismiss-only POST does not clear the gateway URL). When gatewayUrl or gatewayToken is present, the server-side AgentSource registry is reconnected best-effort (non-blocking, errors swallowed).
- Path/query params: none.
- Request body:
{
gatewayUrl?: string // must be ws:// or wss:// if non-empty (host unrestricted); '' clears it
gatewayToken?: string // '' clears the stored token
firstRunDismissedAt?: number // epoch ms; only applied when typeof === 'number'
}
gatewayUrl is validated to a ws:/wss: scheme before it is stored, because the same-origin proxy later dials it (new WebSocket(upstreamUrl)). A non-websocket target (http/file/javascript) is rejected. The host is intentionally not restricted; a remote gateway is a supported choice.
Responses
200 OK: settings saved:
400 Bad Request: the body is missing or not an object:
{ "error": "JSON body required" }
400 Bad Request: gatewayUrl is present, non-empty, and not a ws:/wss: URL:
{ "error": "gatewayUrl must be a ws:// or wss:// URL" }
500 Internal Server Error: saving failed:
{ "error": "Failed to save settings" }
Example
# Set the upstream gateway URL and token
curl -X POST http://localhost:18790/api/settings \
-H 'Content-Type: application/json' \
-d '{"gatewayUrl":"ws://localhost:18789","gatewayToken":"<token>"}'
# Dismiss-only update — leaves the gateway URL/token untouched
curl -X POST http://localhost:18790/api/settings \
-H 'Content-Type: application/json' \
-d '{"firstRunDismissedAt":1750000000000}'
GET /api/health
Returns the latest BootReport. The boot probe runs once at server start and the result is cached; if no report exists yet (a pre-boot request), the handler computes one on demand so the endpoint is always answerable; this is the one liveness surface that works with the Gateway down. The ok field (= no fatal checks) is the one-field summary a simple liveness probe can read.
- Path/query params: none.
- Request body: none.
Responses
200 OK: the boot report (see The BootReport shape below). Date fields serialize to ISO strings via JSON.
{
ok: boolean // = fatal.length === 0; the one-field liveness summary
// ...the full BootReport (startedAt, finishedAt, checks, degraded, fatal, config, resolved)
}
The response is passed through redactObject before send (the same redact-on-display applied to obs/audit/tools): a credential-shaped substring landing in any check message/detail is masked, while readable paths and config stay intact.
500 Internal Server Error: only on the pre-boot path, if computing the report throws:
{ "ok": false, "error": "<message>" }
Example
curl http://localhost:18790/api/health
POST /api/health/recheck
Recomputes the boot report fresh (the “Re-run probe” action after the user fixes a problem). Same response shape as GET /api/health.
- Path/query params: none.
- Request body: none (ignored).
Responses
200 OK: the freshly-computed boot report, redacted (identical shape to GET /api/health).
500 Internal Server Error: if recomputing throws:
{ "ok": false, "error": "<message>" }
Example
curl -X POST http://localhost:18790/api/health/recheck
The BootReport shape
GET /api/health and POST /api/health/recheck both return this structure (with ok prepended). It is the output of the boot probe (runBootProbe in apps/web/server/lib/bootProbe.ts).
interface BootReport {
startedAt: Date // ISO string in JSON
finishedAt: Date // ISO string in JSON
checks: BootCheck[]
degraded: string[] // check ids that failed but the server still runs
fatal: string[] // check ids that prevent a working server
config: BootConfig
resolved: {
clawbooHome: string
dbPath: string
apiPort: number | null
stateDir: string
vaultPresent: boolean
masterKeyOk: boolean
}
}
interface BootCheck {
id: string
ok: boolean
message: string // short, user-friendly status
detail?: string // optional multi-line detail
durationMs: number
}
interface BootConfig {
logLevel: string
budgetPosture: string
budgetHardCapUsdCents: number | null
budgetWarnSoftPct: number
otelEnabledByDefault: boolean
otelActive: boolean // whether an OTLP endpoint is configured THIS boot
}
Fatal vs degraded
Almost every check degrades (the server keeps running and the UI shows a banner) rather than being fatal. Only two failures are fatal: clawbooHomeWritable and databaseIntegrity, because nothing works without them. There are no migration/upgrade paths: a fatal boot means the install is broken, and the remedy is to reset ~/.clawboo and re-run the onboarding wizard.
ok = report.fatal.length === 0. A degraded-but-not-fatal install returns ok: true with a non-empty degraded[].
The checks (in run order)
id | What it verifies | Fatal? |
|---|
clawbooHomeWritable | ~/.clawboo exists and is writable | Yes |
vaultPerms | secrets/ is 700, master.key/proxy-device-identity.json are 600 (POSIX-only; skipped on Windows and on a fresh install) | No |
masterKeyBootSentinel | A fixed value encrypted on first boot still decrypts (a rotated/lost master key fails closed) | No |
databaseIntegrity | PRAGMA integrity_check returns ok | Yes |
databaseSchema | The core tables (teams, agents, settings, budgets, orchestration_events, tasks) are present | No |
apiPortFileMatches | The on-disk api-port file matches the actual listening port (skipped when no port is supplied) | No |
mcpServersHealthy | Each in-process MCP server builds and answers a tools/list round-trip | No |
openclawGatewayReachable | The configured OpenClaw Gateway is reachable + synced (degrades to serving last-synced agents from SQLite; skipped when no gatewayUrl is set) | No |
otelExporterReachable | The OTel exporter initialized, only checked when an OTLP endpoint is configured | No |
config (production-defaults posture)
config surfaces the shipped defaults so a user or bug report can see what the install runs with. The values come from apps/web/server/lib/defaults.ts:
| Field | Default value | Meaning |
|---|
logLevel | "info" | pino log level (override via LOG_LEVEL) |
budgetPosture | "track-and-warn" | Budgets warn rather than hard-cap by default |
budgetHardCapUsdCents | null | No global hard USD cap (no auto-pause unless a user sets a cap-mode budget) |
budgetWarnSoftPct | 80 | Soft-warn threshold (mirrors SOFT_CAP_PERCENT) |
otelEnabledByDefault | false | The OTLP→Jaeger bridge is opt-in |
otelActive | runtime-resolved | true only when OTEL_EXPORTER_OTLP_ENDPOINT (or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) is set this boot |
Error envelope
Every error response on these routes is the standard envelope { error: string } (/api/settings), except the health routes, which return { ok: false, error: string } on their 500 path (their success body always carries ok).
See also
- System Health (UI panel over these routes)
- Production defaults & posture, the
config block’s source of truth
- Configuration & file locations,
~/.clawboo/settings.json, the vault, the api-port file
- Environment variables,
STUDIO_ACCESS_TOKEN, OTEL_EXPORTER_OTLP_ENDPOINT, LOG_LEVEL
- Security & access gate, why the token is never returned, redaction-on-display
- System API, OpenClaw gateway lifecycle (start/stop/configure)
- REST API overview