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

# HTTP API reference

> Reference for the Fleet Pi local web app HTTP API — the /api/chat NDJSON event stream, request and response schemas, and supporting workspace endpoints.

Generated from `openapi.json` and the file-based TanStack Start routes under `apps/web/src/routes/api/`. Regenerate the OpenAPI document with `pnpm generate:docs`.

**Base URL:** `http://localhost:3000`

All request bodies are validated with [zod](https://zod.dev/) schemas in `apps/web/src/lib/pi/chat-protocol.zod.ts`. Responses are JSON unless otherwise noted; `/api/chat` returns an NDJSON event stream.

## Chat

### POST /api/chat

Send a chat message and receive a streaming response.

**Request body**

```json theme={null}
{
  "sessionFile": "<string>",
  "sessionId": "<string>",
  "message": "<user message>",
  "model": "<provider/model id or { provider, id, thinkingLevel }>",
  "mode": "agent | plan | harness",
  "planAction": "execute | refine",
  "streamingBehavior": "steer | followUp"
}
```

**Responses**

* **200** — NDJSON stream of `ChatStreamEvent` lines. See [stream events](#stream-events).
* **400** — Validation error.

### POST /api/chat/abort

Abort the active chat session.

**Request body**

```json theme={null}
{ "sessionFile": "<string>", "sessionId": "<string>" }
```

**Responses**

* **200** — `{ "aborted": true | false }`
* **500** — `{ "message": "<error>" }`

### GET /api/chat/models

List available chat models.

**Responses**

* **200** — `{ models, selectedModelKey, defaultProvider, defaultModel, defaultThinkingLevel, diagnostics }`
* **500** — `{ message }`

### GET /api/chat/providers · POST /api/chat/providers

Read or update provider credential state. `GET` reports which providers have credentials configured (best-effort, based on env-var presence — actual auth is verified at runtime). `POST` updates the matching env var entry through the in-app config panel.

**GET response**

```json theme={null}
{
  "providers": [
    { "id": "google-genai", "name": "Google Gemini", "envVarName": "GEMINI_API_KEY", "configured": true },
    { "id": "openai", "name": "OpenAI", "envVarName": "OPENAI_API_KEY", "configured": false }
  ]
}
```

Known providers: `amazon-bedrock`, `openai`, `anthropic`, `google-vertex`, `google-genai` (Gemini), `mistral`, `groq`, `ollama`.

### GET /api/chat/commands

List slash commands available in the chat input. Populates the `/`-triggered command menu with built-ins plus any active skills and prompts. See [slash commands](/fleet-pi/chat-modes#slash-commands) for the UX and keyboard shortcuts.

**Response**

```json theme={null}
{
  "commands": [
    {
      "name": "model",
      "description": "Select provider/model or thinking level",
      "argumentHint": "[provider/id[:thinking]]",
      "source": "builtin"
    },
    {
      "name": "refactor-tech-debt",
      "description": "Prompt-based refactor helper",
      "source": "prompt",
      "passThrough": true
    }
  ],
  "diagnostics": []
}
```

`source` is one of `builtin`, `skill`, `prompt`, or `extension`. Skill and prompt entries are omitted when `enableSkillCommands` is disabled in Pi settings, and `diagnostics` will explain why.

**Responses**

* **200** — `{ commands, diagnostics }`
* **500** — `{ message }`

### POST /api/chat/new

Create a new chat session.

**Responses**

* **200** — `{ sessionFile, sessionId }`
* **500** — `{ message }`

### GET /api/chat/provenance

Fetch per-run provenance records.

**Responses**

* **200** — provenance payload
* **500** — `{ message }`

### POST /api/chat/question

Answer a `questionnaire` follow-up question raised by the assistant.

**Request body**

```json theme={null}
{
  "sessionFile": "<string>",
  "sessionId": "<string>",
  "toolCallId": "<string>",
  "answer": {
    "kind": "single | multi | text | skip",
    "questionId": "<string>",
    "selectedIds": ["<id>"],
    "text": "<string>"
  }
}
```

**Responses**

* **200** — `{ ok, message, mode, planAction }`
* **400** — Bad request.
* **404** — `{ ok: false, message }`

### GET /api/chat/resources

List available chat resources (skills, prompts, extensions, themes, packages, AGENTS.md files).

**Responses**

* **200** — `{ packages, skills, prompts, extensions, themes, agentsFiles, diagnostics }`
* **500** — `{ message }`

### POST /api/chat/resume

Resume an existing chat session.

**Request body**

```json theme={null}
{ "sessionFile": "<string>", "sessionId": "<string>" }
```

**Responses**

* **200** — `{ session, messages, sessionReset }`
* **500** — `{ message }`

### POST /api/chat/run

Execute a single recorded run.

### GET /api/chat/runs

List recorded runs for a session.

### GET /api/chat/session · DELETE /api/chat/session

Hydrate or delete a chat session by query parameters. `DELETE` removes the caller's owned mirror row in Neon (cascading run, tool, and file mutation rows) and best-effort cleans up the ephemeral JSONL file. Requires an authenticated Better Auth session and passes ownership verification against the Neon mirror.

| Name          | In    | Required | Description       |
| ------------- | ----- | -------- | ----------------- |
| `sessionFile` | query | No       | Session file path |
| `sessionId`   | query | No       | Session ID        |

**GET responses**

* **200** — `{ session, messages, sessionReset }`
* **500** — `{ message }`

**DELETE responses**

* **200** — `{ ok: true, sessionId, sessionFile }`
* **401** — Unauthorized.
* **403** — Session is not owned by the caller.
* **404** — Session not owned or missing (`{ ok: false, reason: "session-not-owned-or-missing" }`).
* **501** — Mirror disabled (`FLEET_PI_CHAT_DATABASE_URL` not set).
* **503** — Mirror unavailable.

### DELETE /api/chat/account

Erase all mirrored Pi data for the signed-in user: every row in `pi_sessions` (cascading transcripts, runs, tool output, and file mutations) and every BYOK provider credential in `pi_user_providers`. Ownership is enforced under Neon RLS and only the caller's data is touched.

Better Auth identity rows are **not** deleted — coordinate account deletion in Better Auth separately if required. Workspace files on disk are not reverted.

**Responses**

* **200** — `{ ok: true, scope: "pi-mirror", erasedSessions, erasedProviders, message }`
* **401** — Unauthorized.
* **500** — `{ ok: false, reason, message }`

### GET /api/chat/sessions

List all chat sessions.

**Responses**

* **200** — `{ sessions }`
* **500** — `{ message }`

### GET /api/chat/settings · POST /api/chat/settings

Read or patch effective `ChatPiSettings`: compaction, retry, default model and thinking level, enabled models, extensions, packages, prompts, skills, themes, transport, and steering / follow-up delivery modes.

**POST request body**

```json theme={null}
{ "settings": { "<ChatPiSettingsUpdate>": "..." } }
```

**POST response**

```json theme={null}
{
  "diagnostics": [],
  "effective": { "<ChatPiSettings>": "..." },
  "project": { "<ChatPiSettingsUpdate>": "..." },
  "projectPath": "<string>",
  "updateImpact": {
    "newSessionRecommended": false,
    "resourceReloadRequired": false
  }
}
```

## Workspace

Workspace endpoints read and write the durable layer described by the [adaptive workspace contract](/fleet-pi/adaptive-workspace).

<Info>
  Every `/api/workspace/*` route resolves the caller's user-scoped workspace and rejects unauthenticated callers with `401 Unauthorized` on protected deployments (Vercel, Neon Managed Auth, Neon Function surface, or `FLEET_PI_CHAT_RUNTIME_REQUIRE_AUTH=1`). Send either a Neon Auth `Authorization: Bearer <token>` header or an authenticated session cookie. Local development without auth configured continues to serve the repo-scoped workspace.
</Info>

### GET /api/workspace/health

Workspace contract health (manifest version, missing canonical paths, projection state).

### GET /api/workspace/file

Read a canonical workspace file.

### GET /api/workspace/item · GET /api/workspace/items

Fetch a single workspace item or list items.

### GET /api/workspace/tree

Browse the workspace tree.

### GET /api/workspace/search

Search workspace items via the projection index in `agent-workspace/indexes/`.

### GET /api/workspace/reindex · POST /api/workspace/reindex

Rebuild the projection index. The canonical files are not modified — the index is recomputed.

On protected deployments, `POST /api/workspace/reindex` is protected by a double-submit CSRF token and a per-user rate limit:

* **Rate limit** — up to **5 requests per minute per user**. Excess calls return `429 Too Many Requests` with `Retry-After: 60`.
* **CSRF token** — cookie-authenticated callers must first `GET /api/workspace/reindex` to receive a `fleet_pi_csrf` cookie (HttpOnly, `SameSite=Strict`) plus a `csrfToken` in the response body, then echo the token back on the `POST` as the `x-fleet-csrf-token` header. Same-origin `Origin` (or `Referer`) is also required. Bearer-token callers skip CSRF because they are not vulnerable to cross-site cookie replay.

**Example**

```bash theme={null}
# 1. Fetch a CSRF token (session cookie assumed set by prior sign-in)
curl -c cookies.txt -b cookies.txt \
  https://your-fleet-pi.example.com/api/workspace/reindex
# → { "csrfToken": "<token>" }, sets fleet_pi_csrf cookie

# 2. POST with the token echoed in the header
curl -b cookies.txt -X POST \
  -H "x-fleet-csrf-token: <token>" \
  https://your-fleet-pi.example.com/api/workspace/reindex
```

**Responses**

* **200** — reindex completed.
* **403** — `{ "message": "Invalid CSRF protection" }` when the CSRF token, cookie, or same-origin check fails.
* **429** — `{ "message": "Workspace reindex rate limit exceeded" }` with `Retry-After: 60`.

## Outbound network safety

The chat runtime's `web_fetch` tool only accepts **public HTTPS URLs**. Requests to `localhost`, loopback, RFC 1918 (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), link-local (`169.254.0.0/16`), or IPv6 unique-local (`fc00::/7`) addresses are rejected before any connection is opened, and DNS lookups are pinned so a public hostname cannot rebind to a private address between resolution and connect. GitHub `blob/` URLs are automatically rewritten to raw content URLs. Only `text/*` and `application/json` responses are returned; binary payloads are refused.

## Sandbox

Daytona-backed user sandboxes. Each authenticated user gets one isolated sandbox in **their** Daytona account (BYOK). Tool calls run inside the container, and a persistent volume mounted at `/home/daytona/agent-workspace` keeps the user's workspace across restarts. All routes require Better Auth.

### GET /api/sandbox/preview

Return a signed preview URL into the calling user's active Daytona sandbox.

| Name   | In    | Required | Description                                                                  |
| ------ | ----- | -------- | ---------------------------------------------------------------------------- |
| `port` | query | No       | Sandbox port to proxy. Defaults to `3000`. Must be an integer in `1..65535`. |

**Responses**

* **200** — `{ url }` — short-lived preview URL into the sandbox.
* **400** — `{ error: "Invalid port" }`
* **401** — `{ error: "Authentication required" }`
* **404** — `{ error: "No active sandbox for user" }`
* **503** — `{ error: "Sandbox not available" }` — Daytona is not enabled for the calling user. On Vercel this typically means the user has not saved a `daytona` provider secret; locally it usually means `DAYTONA_API_KEY` is unset.

### POST /api/webhooks/daytona

Receive lifecycle events from Daytona (sandbox start, stop, archive, delete). Side effects are applied only when the `x-daytona-signature` header verifies against `DAYTONA_WEBHOOK_SECRET`; otherwise the call is accepted and logged but ignored.

**Headers**

* `x-daytona-signature` — HMAC signature of the request body using `DAYTONA_WEBHOOK_SECRET`.

**Responses**

* **200** — `{ received: true }`
* **500** — `{ error: "Webhook processing failed" }`

See the [configuration reference](/fleet-pi/configuration#daytona-backed-user-sandboxes) for the required environment variables.

## Authentication

Fleet Pi routes follow a deployment-based auth policy:

* **Local development** (no `NEON_AUTH_BASE_URL` / `NEON_AUTH_URL` and not running on Vercel): chat and workspace endpoints allow anonymous access so the app works without login.
* **Vercel, Neon Managed Auth, or dual-host chat runtime**: session, catalog, and workspace routes require authentication. This covers `/api/chat/*` streaming and session routes, the catalog endpoints (`/api/chat/models`, `/api/chat/resources`, `/api/chat/commands`, `/api/chat/models/discover`), and every route under `/api/workspace/*`. Unauthenticated requests receive `401`.
* **`/api/sandbox/preview`** always requires authentication.

Neon Managed Auth clients mint a JWT via `authClient.token()` and send `Authorization: Bearer <jwt>` on every request. Better Auth clients use signed session cookies.

### `/api/auth/*`

Proxies [Neon Managed Auth](https://neon.tech) when `NEON_AUTH_BASE_URL` (or the Vercel↔Neon `NEON_AUTH_URL`) is set. Otherwise it serves [Better Auth](https://www.better-auth.com/) when `BETTER_AUTH_SECRET` is set. See [configuration](/fleet-pi/configuration#authentication-better-auth) and the [Neon Managed Auth reference](/fleet-pi/configuration#neon-managed-auth-optional) for required environment variables.

## Health

### GET /api/health

Smoke endpoint used by the quickstart.

**Responses**

* **200** — `{ "status": "ok" }`

## Stream events

`/api/chat` emits one JSON object per newline. Every event has a `type` discriminator. The full union is `ChatStreamEvent` in [`chat-protocol.ts`](https://github.com/Qredence/fleet-pi/blob/main/apps/web/src/lib/pi/chat-protocol.ts).

| `type`       | Shape                                                                              | Meaning                                                     |
| ------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `start`      | `{ id, runId, sessionFile?, sessionId, sessionReset?, diagnostics? }`              | Stream opened. Use `runId` to correlate with provenance.    |
| `delta`      | `{ text, messageId? }`                                                             | Streaming assistant text chunk.                             |
| `thinking`   | `{ text, messageId? }`                                                             | Streaming chain-of-thought when `thinkingLevel` is enabled. |
| `tool`       | `{ part, messageId? }`                                                             | Tool call or tool result rendered in the transcript.        |
| `plan`       | `{ mode, executing, completed, total, message?, state }`                           | Plan-mode progress with structured `state`.                 |
| `state`      | `{ state }`                                                                        | Generic chat state update.                                  |
| `queue`      | `{ steering: string[], followUp: string[] }`                                       | Steering / follow-up prompts queued during streaming.       |
| `compaction` | `{ phase: "start" \| "end", reason, aborted?, willRetry?, errorMessage? }`         | Session compaction lifecycle.                               |
| `retry`      | `{ phase, attempt, maxAttempts?, delayMs?, success?, finalError?, errorMessage? }` | Retry attempt fired around a Bedrock invocation.            |
| `done`       | `{ runId, message, sessionFile?, sessionId, sessionReset? }`                       | Assistant turn finished cleanly.                            |
| `error`      | `{ message, runId? }`                                                              | Terminal stream error.                                      |

Plan events carry a `ChatPlanState` shape:

```ts theme={null}
type ChatPlanState = {
  mode: "agent" | "plan" | "harness"
  executing: boolean
  pendingDecision: boolean
  completed: number
  total: number
  todos: Array<{ step: number; text: string; completed: boolean }>
  message?: string
}
```

## Related

<CardGroup cols={2}>
  <Card title="Chat modes" icon="sliders-vertical" href="/fleet-pi/chat-modes">
    Which tools each mode unlocks.
  </Card>

  <Card title="Architecture" icon="sitemap" href="/fleet-pi/architecture">
    How requests flow from the browser through the configured model provider and back.
  </Card>
</CardGroup>
