Server API
phr serve: the HTTP and Server-Sent Events API behind the browser, the desktop and the phone apps.
phr serve # 127.0.0.1:8790, default folder ~/PHRONEN
phr serve --cwd ~/Projects # a different default folder
phr serve --host 0.0.0.0 # reachable from your phone on the same network (token required)
phr serve --allow-origin https://my.app # let another browser origin call itAuthentication
- Authorization: Bearer <token> on every request except /v1/health. An EventSource cannot set headers: POST /v1/stream-ticket?path=<stream path> with the header, then open the stream with ?ticket=<ticket> (short-lived, that path only).
- The token is $PHRONEN_SERVER_TOKEN, or ~/.phronen/server.token (created on first start, mode 600; phr serve prints it).
- Protected requests require the server token. Allowed origins control CORS response access and do not authenticate callers.
Endpoints
| Method and path | What it does |
|---|---|
| GET /v1/health | {ok, version, engine} |
| GET /v1/info | {version, default_cwd, home, modes, efforts} |
| GET /v1/sessions | recent conversations, newest first (?cwd=&limit=) |
| POST /v1/sessions | {cwd?, mode?, effort?, text?, images?} → 201 {session, running}; with text the first turn starts at once |
| GET /v1/sessions/{id} | {session, mode, effort, running, todos, transcript, last_seq, pending_permissions, pending_requests}; requests include request_token |
| DELETE /v1/sessions/{id} | delete a conversation |
| POST /v1/sessions/{id}/messages | {text, images?} → 202; 409 while a turn is running. Text starting with / runs a command |
| POST /v1/sessions/{id}/commands | {line: "/cost"} → {text, data, action, error} |
| GET /v1/sessions/{id}/events | the event stream (?after=<seq>) |
| POST /v1/sessions/{id}/permissions/{request} | {decision: allow | always | deny, request_token, feedback?}; echo the reviewed request's 32-character lowercase hexadecimal token; stale or missing tokens do not consume the wait |
| POST /v1/sessions/{id}/interrupt | stop the running turn |
| GET /v1/commands · /v1/agents · /v1/mcp | slash commands, subagents, MCP servers (?cwd=) |
| GET /v1/files | a folder listing or a file, inside the session's project only (?session=&path=) |
| GET /v1/usage | the device's cost ledger: total, by day, by kind of work, top conversations |
| GET /v1/doctor | installation checks |
| POST /v1/login | {key} — save the credential on the server's device |
The event stream
The stream first sends event: hello with {running, last_seq}, then every event after the ?after sequence number (a replay), then live events. Each event has an id: line with its sequence number and JSON data with type, session_id, ts and seq; a subagent's events also carry parent_id — the Task call that started it. Reconnect with after=<the last seq you saw> and nothing is lost or repeated. A ping comment arrives about every 15 seconds.
| type | Meaning |
|---|---|
| turn_start · turn_end | a turn began · ended (stop_reason, duration_ms, turn_cost_usd, tool_calls) |
| text_delta · text_done | streamed reply text · the complete reply, which replaces the streamed text |
| reasoning_delta | PHRONEN is thinking — show it as activity, not content |
| tool_call_delta · tool_call_progress | a tool call is being prepared · a large one is streaming |
| tool_start · tool_output_delta · tool_end | a tool began (id, name, title, input) · live output · finished (ok, output, meta, duration_ms) |
| todos | the plan changed |
| permission_request · permission_resolved | PHRONEN is waiting for an answer · it was answered |
| usage | one call was metered (cost_usd is provider-reported) |
| status · error | a transient notice · something failed; the turn may continue |
| compacted · title | the conversation was summarized · it got its title |
| subagent_start · subagent_end | a Task subagent started · finished |
tool_end.meta carries, as they apply: path, diff (unified), added, removed, preview (a new file's start), exit_code, urls (local addresses a command printed — show a live preview), background (the id of a server left running) and image.
const base = "http://127.0.0.1:8790";
const { session } = await (await fetch(`${base}/v1/sessions`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cwd: "/Users/me/shop" }),
})).json();
const events = new EventSource(`${base}/v1/sessions/${session.id}/events?after=0`);
events.onmessage = (m) => { const e = JSON.parse(m.data); if (e.type === "text_delta") render(e.text); };
await fetch(`${base}/v1/sessions/${session.id}/messages`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "Build me an ecommerce website" }),
});