# REST API Overview

> Applies to: ChengOS v0.1.0+ ｜ Last verified: 2026-08-12 ｜ Source: `crates/cheng-api/src/rest/routes.rs`, `crates/cheng-api/src/rest/dto/common.rs`, `crates/cheng-api/src/middleware/workspace_context.rs`

Everything in ChengOS talks to one backend service, `cheng-api`. The editor, the chat
gateway, and the `cheng` CLI are all ordinary clients of the API described here — there is
no private channel they use that you cannot.

## Base URL and conventions

Versioned endpoints live under `/api/v1`. Against a default installation that is
`http://127.0.0.1:19225/api/v1`.

A few routes sit deliberately **outside** the version prefix, because they are
infrastructure rather than product API:

| Path | Purpose |
|---|---|
| `GET /health` | Liveness check. Use this for load balancers and `chengos.sh status`. |
| `GET /ready` | Readiness check, including datastore connectivity. |
| `GET /metrics` | Prometheus metrics. Unauthenticated, per standard Prometheus practice. |
| `/ws/executions` | WebSocket stream of execution events. |
| `/mcp` | App-level MCP server, mounted only when `MCP_ENABLED=true`. |

Request and response bodies are JSON with **camelCase** field names, even though the
backend is Rust. A login response is `{"token": …, "refreshToken": …, "expiresIn": 3600}`.

## Authenticating

Most endpoints require a JWT. Get one from `POST /api/v1/auth/login`:

```bash
curl -X POST http://127.0.0.1:19225/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email": "you@example.com", "password": "…"}'
```

```json
{
  "token": "eyJhbGciOi…",
  "refreshToken": "eyJhbGciOi…",
  "expiresIn": 3600,
  "user": { "id": "…", "email": "…", "username": "…" }
}
```

Send the token on every subsequent request:

```
Authorization: Bearer <token>
```

Access tokens expire after one hour. Exchange the refresh token at
`POST /api/v1/auth/refresh` rather than logging in again. `POST /api/v1/auth/logout`
returns `204` and is client-side only — it tells you to discard the token; it does not
revoke it server-side.

The rest of the auth surface:

| Endpoint | Purpose |
|---|---|
| `GET /auth/bootstrap-status` | Whether the installation has any users yet (`needsSetup`). Drives first-run setup. |
| `POST /auth/register` | Create an account, subject to the deployment's registration policy. |
| `GET /auth/validate` | Check a token without doing anything else. |
| `GET /auth/me` | The current user. |
| `POST /auth/password/forgot` · `/reset` · `/change` | Password lifecycle. Reset mail needs `EMAIL_ENABLED`. |

### Three other credential types

JWTs are not the only way in, and mixing them up is a common source of `401`s:

- **Channel keys** (`chk_` prefix) authenticate external channel adapters — WhatsApp,
  Telegram, Slack, and so on — against `/api/v1/channel/*` only. They are stored as
  credentials and never carry a user session.
- **WebSocket tickets** (`chtk_` prefix) are minted through the authenticated channel REST
  surface and consumed once on WebSocket upgrade. They exist so channel keys never appear
  in a URL.
- **MCP tokens** authenticate against the MCP server, either database-backed (preferred) or
  a static `MCP_SERVER_TOKEN`.

## Selecting a workspace

Almost all resources belong to a workspace. Rather than repeating the workspace in every
path, authenticated requests carry it in a header:

```
X-Workspace-Id: <workspace-uuid>
```

This is authorized, not merely read: the middleware runs after authentication and rejects
the request if the caller has no access to the workspace named in the header. Passing a
workspace id you cannot see is a `403`, not a silent fallback to a default.

## The main endpoint groups

| Prefix | What it covers |
|---|---|
| `/workflows` | Create, read, update, delete, duplicate, export; favorites; template flags; per-workflow LLM node configuration. `GET /workflows/templates` is public and unauthenticated. |
| `/executions` | Start runs and inspect them: `POST /executions` to run, then `/:id`, `/:id/logs`, `/:id/trace-snapshot`, `/active`, `/history`. Control with `/:id/cancel`, `/:id/pause`, `/:id/resume`, `/:id/approve`. |
| `/nodes` | The node catalog and their JSON Schemas. Reads are public; writes need auth. |
| `/workspaces` | Workspaces, plus nested `/members`, `/settings`, `/knowledge-bases`, `/channels`, `/channel-keys`. |
| `/credentials` | The encrypted credential vault. |
| `/documents` · `/tables` | Document and spreadsheet workspaces. |
| `/skills` · `/skill-registry` | Installed skills, and the bridge to the public registry. |
| `/schedules` | Timed and triggered runs. Mounted only when a scheduler is configured. |
| `/mcp/servers` | MCP server registry. Mounted only when MCP state is available. |
| `/llm` · `/tools` · `/settings` · `/analytics` · `/i18n` | Model providers, tool metadata, instance settings, usage analytics, and node UI translation bundles. |
| `/channel` | Channel adapter ingress. Authenticated by `chk_` keys, not JWT. |

Two groups are conditionally mounted — `/schedules` and `/mcp/servers`. If they return
`404` on an otherwise healthy system, the feature is not configured rather than broken.

## Pagination

List endpoints take `offset` and `limit` as query parameters. `limit` defaults to `20` and
is **clamped to a maximum of 100** — asking for more is not an error, you simply get 100.
A `limit` of `0` is treated as the default rather than an empty page. Where supported,
`sortBy` and `order` (`asc` / `desc`) control ordering.

Responses wrap the page:

```json
{
  "total": 137,
  "items": [ … ],
  "hasMore": true,
  "offset": 0,
  "limit": 20
}
```

Use `hasMore` rather than comparing `offset + limit` against `total`; it already accounts
for the actual number of items returned.

## Errors

Errors share one envelope:

```json
{
  "code": "VALIDATION_ERROR",
  "message": "Human-readable description",
  "details": { },
  "timestamp": "2026-08-12T10:30:00Z",
  "requestId": "…"
}
```

Match on `code`, not on `message` — messages are meant for humans and may be reworded.
`requestId` is what to quote when correlating a client failure against server logs.

One code deserves special mention: `403 DEMO_MODE_RESTRICTED` means the installation is
running with `CHENG_DEMO_MODE=true`, which denies writes and executions by an allowlist. It
indicates configuration, not a permission problem with your account.

## Real-time execution events

Workflow runs stream over WebSocket at `/ws/executions`, also reachable as
`/api/v1/ws/executions`. Authenticate with a JWT, passed either as an `Authorization`
header or as a `?token=` query parameter; channel clients connect with `?ticket=chtk_…`
instead. The stream carries node-level progress, incremental output from streaming nodes,
and terminal state, which is what makes the editor's live execution view work.

## Next steps

- [Environment variable reference](/docs/config-env-reference) — the settings that decide
  which of these endpoints are mounted.
- [Build your first workflow](/docs/quickstart-first-workflow) — the same operations
  through the editor.
