Skip to main content
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 schemas from @workspace/pi-protocol/chat-protocol.zod (packages/pi-protocol). Responses are JSON unless otherwise noted; /api/chat returns an NDJSON event stream.

Hosted and local sessions

On the Cloudflare deployment (FLEET_PI_DEPLOYMENT=cloudflare), new Agent chat sessions are hosted. Each signed-in user has one Pi Durable Object (UserPiDurable, bound as USER_PI_DURABLE), and its SQLite storage is the transcript. Hosted sessions carry a sessionId that starts with durable: and no sessionFile. Neon keeps auth, settings, and an optional session projection. Requests that send a sessionFile, or a sessionId without the durable: prefix, use the legacy JSONL session path. Local development always uses JSONL session files.

Chat

POST /api/chat

Send a chat message and receive a streaming response. Hosted turns stream from the caller’s Durable Object. Each turn applies the conversation’s selected model, so you can switch models on an existing hosted session. A changed API key, base URL, or API reopens the harness. Request body
Responses
  • 200 — NDJSON stream of ChatStreamEvent lines. See stream events.
  • 400 — Validation error.
  • 401 — Hosted turn without an authenticated user.
  • 409 — { message: "An agent turn is already in progress." } when a hosted turn is running and the new message cannot join or queue behind it.

POST /api/chat/abort

Abort the active chat session. Request body
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 }

POST /api/chat/models/discover

Discover remote models for a configured provider, for example a custom provider saved without model ids. Request body
Responses
  • 200 — { providerId, models }, where each model has the same shape as the /api/chat/models catalog entries (key, provider, id, name, reasoning, input, contextWindow, maxTokens, available, …).
  • 401 — Unauthorized.

GET /api/chat/providers · POST /api/chat/providers · DELETE /api/chat/providers

Read, save, or remove provider credentials. GET reports which providers have credentials configured. POST saves encrypted BYOK credentials for a provider. DELETE removes them. GET response
Entries can also carry authType (apiKey or oauth), providerFamily, displayName, baseUrl, api, and modelIds. Saved openai-chat-completions instances return the baseUrl you saved for them. Catalog providers include amazon-bedrock, openai, openai-chat-completions, anthropic, google, google-vertex, mistral, groq, openrouter, vercel-ai-gateway, github-copilot, ollama, and daytona for sandbox credentials. POST request body
providerId and apiKey are required. models is optional and accepts up to 64 ids. DELETE request body
POST and DELETE responses
  • 200 — { success, providers }
  • 401 — Unauthorized.

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 for the UX and keyboard shortcuts. Response
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 }

POST /api/chat/fork

Fork a hosted conversation from its latest entry. The fork keeps the parent conversation’s model. Only hosted (durable:) sessions on the Cloudflare deployment can be forked. Request body
Responses
  • 200 — { sessionId, conversationId } for the new conversation.
  • 400 — { message: "Only a hosted conversation can be forked." }
  • 401 — Unauthorized.
  • 404 — { message: "Session not found" }, including deleted sessions.
  • 409 — { message: "An agent turn is already in progress." }
  • 503 — { message: "Hosted agent runtime is unavailable." }

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
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
Responses
  • 200 — { session, messages, sessionReset }
  • 500 — { message }

GET /api/chat/run

Fetch a single recorded run by its id query parameter. Responses
  • 200 — Run detail.
  • 401 — Unauthorized.
  • 404 — Not found.

GET /api/chat/runs

List recorded runs for a session, selected by the sessionId or sessionFile query parameter. Responses
  • 200 — Run list.
  • 401 — Unauthorized.

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

Hydrate or delete a chat session by query parameters. For a JSONL session, 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. For a hosted session (durable: sessionId, no sessionFile), DELETE only hides the conversation in the caller’s Durable Object, because Pi Durable has no per-conversation delete. To erase hosted transcripts, use DELETE /api/chat/account. DELETE requires an authenticated session and passes ownership verification. 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, or the hosted delete failed ({ ok: false, reason: "delete-failed" }).

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. On the Cloudflare deployment, the route also clears the caller’s Durable Object storage, which removes every hosted transcript. 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" | "pi-mirror+hosted", erasedSessions, erasedProviders, erasedHostedTranscripts, 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 · PATCH /api/chat/settings

Read or patch effective ChatPiSettings: compaction, retry, default provider, model and thinking level, enabled models, skill commands, extensions, packages, prompts, skills, themes, transport, code mode (codemode), MCP servers (mcpServers), and steering / follow-up delivery modes. PATCH persists project overrides and hot-reloads active runtimes. Hosted conversations apply saved compaction, retry, thinking, and steering settings on their next turn. PATCH request body
PATCH response

Workspace

Workspace endpoints read and write the durable layer described by the adaptive workspace contract.
Every /api/workspace/* route resolves the caller’s user-scoped workspace and rejects unauthenticated callers with 401 Unauthorized on protected deployments (Cloudflare, Neon Managed Auth, 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.
When the signed-in user has no Daytona sandbox on a hosted deployment, workspace routes and GET /api/chat/commands serve the built-in workspace stored in the user’s Durable Object. They return 403 with { "message": "daytona_credential_required" } only when the deployment has no Durable Object binding. In that case GET /api/workspace/health reports a degraded 503 instead.

GET /api/workspace/health

Workspace contract health (manifest version, missing canonical paths, projection state) and seed template state. A 200 response returns status, workspaceAvailable, bootstrapComplete, projectionStatus and template. template is { version, pendingUpdateVersions }:
  • version is the template version last applied to the workspace, or null when none was recorded (for example, local workspaces).
  • pendingUpdateVersions lists, in ascending order, the template versions with updated copies waiting for review under .fleet/template-updates/.
template is null when the workspace is unavailable. The degraded 503 body has no template field.

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
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. 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 the Cloudflare deployment this typically means the user has not saved a daytona provider secret; locally it usually means DAYTONA_API_KEY is unset.

GET /api/sandbox/settings · POST /api/sandbox/settings

Read Daytona sandbox configuration and readiness, or update the local Daytona environment configuration. GET response
daytonaApiKeyConfigured is true when DAYTONA_API_KEY is set on the server or you saved a daytona provider secret. sandboxReady is true once Fleet Pi has a cached Daytona sandbox for your user. POST request body
Responses
  • 200 — Sandbox status (GET) or configuration saved (POST).
  • 401 — Unauthorized.
  • 403 — POST only: invalid CSRF protection.

POST /api/webhooks/daytona

Receive lifecycle events from Daytona (sandbox start, stop, archive, delete). Fleet Pi verifies the x-daytona-signature header as an HMAC-SHA256 of the raw request body keyed by DAYTONA_WEBHOOK_SECRET, compared in constant time. When the secret is unset, local deployments accept and log the call but ignore it, and hosted deployments return 503. Headers
  • x-daytona-signature — Hex-encoded HMAC-SHA256 of the raw request body, optionally prefixed with sha256=.
Responses
  • 200 — { received: true }
  • 401 — { error: "invalid signature" }
  • 503 — { error: "webhook secret is not configured" } on a hosted deployment.
  • 500 — { error: "Webhook processing failed" }
See the configuration reference 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 Cloudflare): chat and workspace endpoints allow anonymous access so the app works without login.
  • Cloudflare (FLEET_PI_DEPLOYMENT=cloudflare), Neon Managed Auth, or FLEET_PI_CHAT_RUNTIME_REQUIRE_AUTH=1: 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 when NEON_AUTH_BASE_URL (or NEON_AUTH_URL) is set. Otherwise it serves Better Auth when BETTER_AUTH_SECRET is set. See configuration and the Neon Managed Auth reference for required environment variables.

GET /api/auth/session

Return the active authentication session for the caller, or null when nobody is signed in.

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. Every turn ends with a terminal done frame. When the user stops a turn, done carries stopReason: "aborted" and message holds whatever streamed before the abort. When a turn fails, an error frame comes first, followed by done with stopReason: "error". A done frame without stopReason means the turn completed normally.

Workspace status

The first start frame of a request can carry a workspace field that tells the client whether the model has workspace tools:
On a hosted deployment without a Daytona key, reason is daytona_credential_required. The model then works on the built-in workspace with only the read, search, write, and edit tools. Add a Daytona key under Settings → Providers to enable shell and sandbox tools. daytona_sandbox_not_ready means a key is configured but the sandbox is still warming up. Later start frames in the same request omit the field.

Plan state

Plan events carry a ChatPlanState shape:

Chat modes

Which tools each mode unlocks.

Architecture

How requests flow from the browser through the configured model provider and back.
Last modified on November 10, 2026