> ## Documentation Index
> Fetch the complete documentation index at: https://docs.claw.boo/llms.txt
> Use this file to discover all available pages before exploring further.

# Settings & Health API

> REST reference for /api/settings (gateway URL + token) and /api/health (the boot probe / System Health surface).

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

<Note>
  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.
</Note>

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:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  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:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "Failed to load settings" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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**:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  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'
}
```

<Info>
  `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.
</Info>

### Responses

**`200 OK`**: settings saved:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "ok": true }
```

**`400 Bad Request`**: the body is missing or not an object:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "JSON body required" }
```

**`400 Bad Request`**: `gatewayUrl` is present, non-empty, and not a `ws:`/`wss:` URL:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "gatewayUrl must be a ws:// or wss:// URL" }
```

**`500 Internal Server Error`**: saving failed:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "Failed to save settings" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
# 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](#the-bootreport-shape) below). `Date` fields serialize to ISO strings via JSON.

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  ok: boolean // = fatal.length === 0; the one-field liveness summary
  // ...the full BootReport (startedAt, finishedAt, checks, degraded, fatal, config, resolved)
}
```

<Note>
  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.
</Note>

**`500 Internal Server Error`**: only on the pre-boot path, if computing the report throws:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "ok": false, "error": "<message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "ok": false, "error": "<message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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`).

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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. Three failures are fatal: `clawbooHomeWritable`, `databaseIntegrity` and `databaseSchema`, because nothing works without them. A fatal check's `detail` says what to do; resetting `~/.clawboo` and re-running the onboarding wizard is the last resort.

`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 are present, and no column the bootstrap DDL declares is missing from them                                                      | **Yes** |
| `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)](/using/system-health)
* [Production defaults & posture](/operating/production-defaults), the `config` block's source of truth
* [Configuration & file locations](/reference/configuration), `~/.clawboo/settings.json`, the vault, the api-port file
* [Environment variables](/reference/environment-variables), `STUDIO_ACCESS_TOKEN`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `LOG_LEVEL`
* [Security & access gate](/operating/security), why the token is never returned, redaction-on-display
* [System API](/reference/rest-api/system), OpenClaw gateway lifecycle (start/stop/configure)
* [REST API overview](/reference/rest-api/index)
