Skip to main content
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

MethodPathSummaryStream?
GET/api/settingsRead persisted settings (token redacted to hasToken)No
POST/api/settingsPersist gateway URL / token / first-run dismiss (partial update)No
GET/api/healthLatest boot report (or compute one on demand)No
POST/api/health/recheckRecompute the boot report freshNo

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:
{ "ok": true }
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)

idWhat it verifiesFatal?
clawbooHomeWritable~/.clawboo exists and is writableYes
vaultPermssecrets/ is 700, master.key/proxy-device-identity.json are 600 (POSIX-only; skipped on Windows and on a fresh install)No
masterKeyBootSentinelA fixed value encrypted on first boot still decrypts (a rotated/lost master key fails closed)No
databaseIntegrityPRAGMA integrity_check returns okYes
databaseSchemaThe core tables (teams, agents, settings, budgets, orchestration_events, tasks) are presentNo
apiPortFileMatchesThe on-disk api-port file matches the actual listening port (skipped when no port is supplied)No
mcpServersHealthyEach in-process MCP server builds and answers a tools/list round-tripNo
openclawGatewayReachableThe configured OpenClaw Gateway is reachable + synced (degrades to serving last-synced agents from SQLite; skipped when no gatewayUrl is set)No
otelExporterReachableThe OTel exporter initialized, only checked when an OTLP endpoint is configuredNo

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:
FieldDefault valueMeaning
logLevel"info"pino log level (override via LOG_LEVEL)
budgetPosture"track-and-warn"Budgets warn rather than hard-cap by default
budgetHardCapUsdCentsnullNo global hard USD cap (no auto-pause unless a user sets a cap-mode budget)
budgetWarnSoftPct80Soft-warn threshold (mirrors SOFT_CAP_PERCENT)
otelEnabledByDefaultfalseThe OTLP→Jaeger bridge is opt-in
otelActiveruntime-resolvedtrue 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