Skip to content

HTTP API

Everything the web UI does goes through this API, so anything the UI can do you can script.

All routes are under /api. Everything except /api/auth/* requires the login cookie.

bash
curl -s -c jar -X POST localhost:4100/api/auth/login \
  -H 'content-type: application/json' \
  -d '{"password":"…"}'

curl -s -b jar localhost:4100/api/sessions

Auth

GET /api/auth/status{ authRequired, authed }
POST /api/auth/login{ password } → sets the cookie

Workspaces

GET /api/workspaces{ root, workspaces: [{ name, path, isGit }] }
POST /api/workspaces{ name } → creates a directory; the name is slugified

Sessions

GET /api/sessions{ sessions, executor } — pinned first, then most recent. Task sessions only.
GET /api/agent/sessions{ sessions, agentHome } — conversations reached through a channel, each with the channel that owns it
POST /api/sessions{ workspace, title? }
GET /api/sessions/:idOne session
PATCH /api/sessions/:id{ title?, pinned? }
DELETE /api/sessions/:idStops it if running, then deletes it and its events

A session:

json
{
  "id": "12_A1zVAa2rk",
  "title": "test-project",
  "workspace": "/workspaces/test-project",
  "executor": "host",
  "status": "idle",
  "pinned": false,
  "live": true,
  "created_at": "…",
  "updated_at": "…",
  "last_error": null
}

status is one of idle, running, error, interrupted. live is whether a pi process is up right now, which is not the same thing — an idle session can still be live.

Prompting

POST /api/sessions/:id/prompt{ message }
POST /api/sessions/:id/abortStop the current run
POST /api/sessions/:id/ui-response{ id, value?, cancelled? } — answer an extension dialog

prompt returns as soon as pi accepts the message, not when the work finishes. Watch the event stream for progress.

A message matching a portal builtin is handled without reaching the model — see Slash commands.

Events

GET /api/sessions/:id/events?since=<seq>

Server-sent events. Replays everything after since, then tails live. Each message is one event:

json
{ "seq": 78, "type": "portal_notice", "payload": { "text": "…" } }

Track the highest seq you have seen and pass it as since when reconnecting.

Negative seq

Live-only events — extension dialogs — carry a negative seq. They are never persisted, so they must not move your cursor. Ignore anything <= 0 when tracking position, or reconnecting will skip real history.

Types worth knowing: portal_prompt, portal_status, portal_notice, agent_end, extension_ui_request, extension_ui_cancel, extension_error, stderr, plus everything pi emits.

Session config

GET /api/sessions/:id/config{ state, thinking, models, stats }
POST /api/sessions/:id/config{ provider?, modelId?, thinkingLevel?, autoCompaction?, autoRetry? }
POST /api/sessions/:id/compactCompact now
GET /api/sessions/:id/commandsThe slash command palette

GET starts pi if it is not already up, so the values are live rather than guessed. The response carries context usage and token counts.

POST returns { ok, applied, state }, where applied lists what actually changed. Only those fields are persisted — an effort change does not rewrite the model.

compact fails with a message when the session is too short for pi to bother.

Portal settings

GET /api/settings{ settings, stored, defaults, piSettingsPath, executor, workspaceRoot }
PUT /api/settings{ provider?, model?, thinkingLevel? }

settings is what pi is launched with; stored is only your explicit overrides; defaults is what an unset field falls back to. An empty string in PUT clears an override rather than storing a blank.

pi extensions

GET /api/packagesRaw pi list output
POST /api/packages{ spec }
DELETE /api/packages{ spec }
POST /api/packages/updateUpdate everything
GET /api/extensionsParsed packages with their recovered settings
PUT /api/extensions/settings{ key, value } — empty value removes the key
GET /api/pi-settingsRaw settings.json
PUT /api/pi-settings{ content } — refused unless it parses as JSON

Channels

GET /api/channels{ channels, kinds, broken, agentHome, channelsDir }
POST /api/channels{ kind, name, config }
PATCH /api/channels/:id{ name?, slug?, enabled?, config?, instructions? }
DELETE /api/channels/:idKeeps its conversations; ?sessions=delete discards them too
POST /api/channel-packages{ spec } — install a channel package
DELETE /api/channel-packages/:nameUninstall; refuses builtins

A channel's slug is what agent sessions are keyed on, and it survives the channel being deleted and recreated. Creating a channel with an explicit slug reconnects it to the conversations that slug already had.

Secrets are never returned. A channel carries secretsSet listing which secret fields have a value, and sending a blank secret keeps the stored one.

broken lists packages that failed to load, with the reason.

Give it a task, close the browser, come back later.