# API routes (/docs/api-routes)



API routes are one surface in the **frontend integration**
[thematic island](/docs/concept-clusters#what-lives-outside-the-cluster-pattern) —
siblings of [react](/docs/react), [server](/docs/server),
[query](/docs/query), and [devtools](/docs/devtools).

The eight routes below are the canonical HTTP + SSE wire protocol
that runtime clients speak. The Next.js handlers in `@pleach/core`
are the reference implementation, but the shapes are
language-agnostic — an independent native client consumes them in
production, which is what keeps the wire honest.

Mount the routes under `/api/harness/*` (the path prefix is
convention, not contract — re-mount wherever you like; the
relative shapes are what matters).

The shipped, published route factory is `createPleachRoute` from
`@pleach/core/quickstart` — a Web-standard POST handler for the
execute path:

```typescript
// app/api/harness/sessions/[id]/execute/route.ts (Next.js)
import { createPleachRoute } from "@pleach/core/quickstart";

// Provider auto-detected from the environment; pass
// { provider, plugins, tools, storage } to override.
export const POST = createPleachRoute();
```

<Callout type="info">
  The full eight-route handler set documented below
  (`HarnessServer` + `ROUTES`) is an internal substrate surface — see
  [Server](/docs/server). It is NOT a published `@pleach/core/server`
  subpath today, so mount it from the reference Next.js handlers or
  implement the wire contract directly. `createPleachRoute` above is the
  one published factory and covers the single-POST execute path.
</Callout>

<SourceMeta source="{ label: &#x22;src/server/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/server&#x22; }" />

## Route catalog [#route-catalog]

| Route                                | Method | Purpose                                    |
| ------------------------------------ | ------ | ------------------------------------------ |
| `/api/harness/health`                | GET    | Health check + component diagnostics       |
| `/api/harness/sessions`              | GET    | List sessions for the authenticated user   |
| `/api/harness/sessions`              | POST   | Create a new session                       |
| `/api/harness/sessions/[id]`         | GET    | Retrieve session state                     |
| `/api/harness/sessions/[id]`         | PUT    | Update session state                       |
| `/api/harness/sessions/[id]`         | DELETE | Delete a session                           |
| `/api/harness/sessions/[id]/sync`    | POST   | Version-vector sync (conflict detection)   |
| `/api/harness/sessions/[id]/execute` | POST   | Execute a message (SSE streaming response) |

## `GET /api/harness/health` [#get-apiharnesshealth]

Health probe + component-level diagnostics. Cheap; safe to hit
from a load balancer.

**Response 200**

```json
{
  "ok": true,
  "version": "1.1.0",
  "components": {
    "storage":      { "ok": true },
    "checkpointer": { "ok": true },
    "providers":    { "ok": true, "configured": ["anthropic"] }
  }
}
```

**Response 503** — runtime disabled via `FEATURE_HARNESS_V2_RUNTIME=false`.

## `GET /api/harness/sessions` [#get-apiharnesssessions]

List sessions owned by the authenticated user.

**Query params**

| Param      | Type      | Purpose                                     |
| ---------- | --------- | ------------------------------------------- |
| `limit`    | `number`  | Page size (default 50, max 200)             |
| `cursor`   | `string`  | Opaque pagination cursor                    |
| `archived` | `boolean` | Include archived sessions (default `false`) |

**Response 200**

```json
{
  "sessions": [
    { "id": "018f...", "createdAt": "...", "lastActiveAt": "...", "title": "..." }
  ],
  "nextCursor": "01jc..."
}
```

## `POST /api/harness/sessions` [#post-apiharnesssessions]

Create a new session.

**Request body**

```json
{
  "provider": { "type": "anthropic" },
  "model":    { "id": "claude-sonnet-4-5" },
  "tools":    { "enabled": ["search", "calculator"] }
}
```

All fields optional; defaults match `SessionRuntime.createSession`.

**Response 201**

```json
{ "session": { "id": "018f...", "createdAt": "...", "..." } }
```

## `GET /api/harness/sessions/[id]` [#get-apiharnesssessionsid]

Retrieve a session's full state.

**Response 200** — the `SessionState` shape (messages,
pendingToolCalls, completedToolCalls, pendingJobs, artifacts,
versionVector, etc.).

**Response 404** — session not found or not visible to the
authenticated user. Code `2001`.

## `PUT /api/harness/sessions/[id]` [#put-apiharnesssessionsid]

Update session state. The request body is the next `SessionState`;
the server validates the version vector and rejects stale writes
with `3001`.

**Request body**

```json
{
  "version": 7,
  "versionVector": { "client_abc": 4, "server": 3 },
  "messages": [/* ... */],
  "tools": { "enabled": ["search"] }
}
```

**Response 200** — the updated state.

**Response 409** — version conflict. Code `3001`. Body:

```json
{
  "code": "3001",
  "error": "Version conflict",
  "remote": { /* the server's current state */ }
}
```

The client should resolve the conflict (typically by surfacing a
chooser UI) and re-PUT with the merged result.

## `DELETE /api/harness/sessions/[id]` [#delete-apiharnesssessionsid]

Delete a session. Cascades into the checkpointer; audit-ledger
rows are preserved (append-only).

**Response 204** — no content.

## `POST /api/harness/sessions/[id]/sync` [#post-apiharnesssessionsidsync]

Version-vector sync. The client pushes its local vector + queued
changes; the server returns missing-from-remote changes plus the
merged vector.

**Request body**

```json
{
  "clientId": "client_abc",
  "vector":   { "client_abc": 5 },
  "changes":  [/* SessionChange[] */]
}
```

**Response 200**

```json
{
  "mergedVector":  { "client_abc": 5, "server": 3 },
  "remoteChanges": [/* changes the client hasn't seen yet */],
  "applied":       3,
  "rejected":      0
}
```

**Response 409** — concurrent vector that the merger couldn't
resolve. Code `3001` or `3002`.

## `POST /api/harness/sessions/[id]/execute` [#post-apiharnesssessionsidexecute]

Execute a message. &#x2A;*SSE streaming response.** Each event from
`runtime.executeMessage` becomes one SSE `data:` frame.

**Request body**

```json
{
  "content": "Summarize the latest changelog entries.",
  "fileReferences": []
}
```

**Response 200** — `Content-Type: text/event-stream`. Each line:

```
data: {"type":"message.delta","delta":"The"}\n\n
data: {"type":"tool.started","toolCall":{...}}\n\n
data: {"type":"tool.completed","toolCall":{...},"result":{...}}\n\n
data: {"type":"message.complete","message":{...}}\n\n
data: {"type":"step.end","step":"post-turn"}\n\n
```

See [Stream events](/docs/stream-events) for the full event
catalog.

### Aborting mid-stream [#aborting-mid-stream]

Close the SSE connection (`EventSource.close()` or fetch
`AbortController.abort()`). The runtime cancels in-flight
provider calls and flushes a final ledger row with
`outcome.status: "user-aborted"`.

### Reconnection [#reconnection]

The runtime emits `stream.reconnecting` and
`stream.disconnected` events on retry. A reference client
reconnects on `EventSource.error` and replays from the last
seen event id. The server is idempotent on `messageId` —
re-submitting the same message after a reconnect doesn't fork
the turn.

## Auth headers [#auth-headers]

The reference handlers expect:

| Header                          | Purpose                                  |
| ------------------------------- | ---------------------------------------- |
| `Authorization: Bearer <token>` | User auth — surfaces as `runtime.userId` |
| `X-Organization-Id`             | Multi-tenant scoping                     |
| `X-Client-Id`                   | Sync version-vector key for this client  |

Mock-mode (`HARNESS_MOCK_MODE=true`) accepts all requests as
`anonymous` for local development.

## Error response shape [#error-response-shape]

Every non-2xx body follows the same shape:

```json
{
  "code": "2001",
  "error": "Session not found",
  "recoveryHint": "The session id may be stale; try refreshing the session list."
}
```

`code` is the structured error code — see
[Error codes](/docs/error-codes) for the catalog.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="HarnessServer" href="/docs/server" description="Framework-agnostic handlers behind these routes — mount into Express, Hono, or raw node:http." />

  <Card title="React" href="/docs/react" description="The client hooks that speak this wire contract." />

  <Card title="Query" href="/docs/query" description="Server-side read API over the same persisted data these routes write." />

  <Card title="DevTools" href="/docs/devtools" description="Browser-console surface for inspecting session state these routes return." />
</Cards>
