Skip to content
Hallway is out (npm i -g hallway): v0, v1 and v2 have shipped, apart from checking it all on a real Mac. See the roadmap.

HTTP API

The daemon serves a JSON API on its own port (http://localhost:4255 by default). The dashboard, the CLI and the MCP server all use it, and so can you. Every route is behind the localhost security rules: writes must be Content-Type: application/json.

Every route below is in the current release. In fronted mode the same API is also reachable through https://hallway.localhost (or your custom domain’s apex).

MethodPathPurpose
GET/api/health{ ok: true, version }: the daemon’s own health.
GET/api/stateEverything the dashboard needs, in one call: apps with live state, bookmarks, observed listeners, what was lost at the last reboot (lost), a config summary and any file errors.
GET/api/configThe resolved config.yaml, with an ETag.
GET/api/appsDeclared and discovered apps, with current state.
GET/api/apps/:slugOne app: config, state, recent history, identity and file errors.
PUT/api/apps/:slugCreate or replace a declared app (ETag / If-Match).
PATCH/api/apps/:slugPartial update, as a JSON merge patch (null removes a key).
DELETE/api/apps/:slugRemove a declared app (its file moves to backups/removed/; never hard-deleted).
GET/api/apps/:slug/history?window=24h|7d|90dUptime buckets and latency figures. The 7- and 90-day windows come from hourly rollups.
GET/api/apps/:slug/thumbThe thumbnail image, with an ETag: a favicon, a screenshot, or a generated 640×400 PNG card. The app’s thumbKind says which. See Thumbnails.
POST/api/thumbs/refreshTake new screenshots now: {"slug": "..."} for one app, {} for every healthy one. Answers {"taken": n} once they’re done; 409 when screenshots are off.
POST/api/apps/:slug/service/:actionstart, stop or restart a managed service.
GET/api/apps/:slug/logs?lines=200The tail of a managed service’s logs.
GET/api/bookmarksAll bookmark groups.
GET/api/bookmarks/:groupOne group, with an ETag.
PUT / PATCH / DELETE/api/bookmarks/:groupCreate or replace, merge-patch, or remove a group (removing moves its file to backups/removed/).
GET/api/bookmarks/:group/icon?url=...The cached favicon of a link in that group (only for a URL that is in the group); 204 when there is none yet.
GET/api/observedObserved listeners, with identity.
POST/api/observed/:id/claimBody { slug, name?, expected? }: writes apps/<slug>.yaml from the observation.
POST/api/observed/:id/ignoreAdd it to ignore.yaml (by port, process or directory).
POST/api/scanRun a scan now.
GET/api/lostLost at last reboot: { lost }, null when there’s nothing. Also lost in /api/state.
POST/api/lost/:id/restart, /api/lost/:id/serviceBody {} or { "slug": "..." }. Start it again, once or as a service, as hallway run would. Answers 200 even when it didn’t come up, with ok: false, the reason and the last log lines.
POST/api/lost/:id/dismiss, /api/lost/dismissDismiss one, or the whole notice.
GET/api/eventsThe live event stream (below).
GET/llms.txt, /llms-full.txt, /agent-guide.mdThe agent guide, with live data.
POST/mcpThe MCP server, Streamable HTTP and stateless. GET and DELETE get 405.

Reads return an ETag (a hash of the file’s bytes). Writes may send If-Match; if the file has changed since, the write gets 409 Conflict and the caller should re-read.

GET /api/events is a Server-Sent Events stream: a hello event first, then change events with incrementing IDs, and a : ping comment every 25 seconds to keep the connection alive. Bursts of file writes are debounced (about 150 ms per item) into one event.

type ServerEvent =
| { type: 'app'; slug: string; reason: 'config' | 'removed' | 'error' }
| { type: 'apps'; reason: string } // the list changed
| { type: 'state'; slug: string; state: AppState } // health transitions only, not every probe
| { type: 'bookmarks'; group?: string }
| { type: 'observed'; reason: 'scan' | 'claimed' | 'ignored' | 'config' }
| { type: 'thumb'; slug: string } // a favicon, screenshot or card changed
| { type: 'config' }
| { type: 'reboot'; lost: string[] }; // the lost-at-reboot notice changed

Treat an event as a hint to refetch the affected resource, not as data: that is what the dashboard does.