Skip to content
PHRONEN
Menu

Product

ProductPricingCompanyEnterpriseDownload

Proof

EvidenceSecurity

Account

Sign inCreate accountSalesContact

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 it

Authentication

  • 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 pathWhat it does
GET /v1/health{ok, version, engine}
GET /v1/info{version, default_cwd, home, modes, efforts}
GET /v1/sessionsrecent 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}/eventsthe 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}/interruptstop the running turn
GET /v1/commands · /v1/agents · /v1/mcpslash commands, subagents, MCP servers (?cwd=)
GET /v1/filesa folder listing or a file, inside the session's project only (?session=&path=)
GET /v1/usagethe device's cost ledger: total, by day, by kind of work, top conversations
GET /v1/doctorinstallation 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.

typeMeaning
turn_start · turn_enda turn began · ended (stop_reason, duration_ms, turn_cost_usd, tool_calls)
text_delta · text_donestreamed reply text · the complete reply, which replaces the streamed text
reasoning_deltaPHRONEN is thinking — show it as activity, not content
tool_call_delta · tool_call_progressa tool call is being prepared · a large one is streaming
tool_start · tool_output_delta · tool_enda tool began (id, name, title, input) · live output · finished (ok, output, meta, duration_ms)
todosthe plan changed
permission_request · permission_resolvedPHRONEN is waiting for an answer · it was answered
usageone call was metered (cost_usd is provider-reported)
status · errora transient notice · something failed; the turn may continue
compacted · titlethe conversation was summarized · it got its title
subagent_start · subagent_enda 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" }),
});