Hub API reference

[!IMPORTANT] Work in progress — not a final product: The vibey Hub API is actively in development and not yet a final product. Endpoints, models, and contracts are still evolving. Folks will be notified on the Discord server once the product is finally done.

Bottom Line Up Front (BLUF) — Install and launch the hub and every krypton client:

  • Install vibey and krypton in one line (macOS & Linux): bash curl -fsSL https://raw.githubusercontent.com/the-vibey-project/vibey/main/scripts/install.sh | sh -s -- --with-krypton krypton # starts `vibey serve` on 127.0.0.1:8765 and opens it in your browser vibey connect --yes # connects Claude Code, Claude Desktop, Codex, Cursor, VS Code & Gemini CLI to the hub over MCP
  • Install every krypton client that talks to this hub (Desktop on macOS & Linux, VS Code / VSCodium / Cursor extension, Web app, Android .apk, and iOS app): See Install, copy, reinstall, uninstall and Downloads.

The hub is vibey serve (ADR-0068): one HTTP API that every krypton client -- desktop, mobile, web, the VS Code extension -- reaches. Version 1. The machine-readable contract is the OpenAPI 3.1 document, committed at docs/reference/hub-api.json and served by a running hub at /api/v1/openapi.json. A test fails when the two differ, so the file is always the truth.

One contract with the CLI

Every document the hub returns is the one the matching command prints with --json, from the same presenter, so a client reads the same keys whichever way it asks:

Route Same document as
GET /api/v1/projects vibey projects --json
GET /api/v1/projects/{project_id}/status vibey status PROJECT_ID --json
GET /api/v1/gates[?project_id=…] vibey gates [PROJECT_ID] --json
GET /api/v1/projects/{project_id}/budget vibey budget show PROJECT_ID --json (read only)
GET /api/v1/projects/{project_id}/queue vibey queue list PROJECT_ID --json
POST /api/v1/projects/{project_id}/queue/{job_id}/bump vibey queue bump JOB_ID --json
GET /api/v1/projects/{project_id}/ledger?text=&kind=&actor=&limit= vibey ledger search … --json
GET /api/v1/loops vibey loops --json

Three routes have no single command behind them:

  • POST /api/v1/gates/{gate_id}/answer takes {"answer": {...}, "request_id": "…"}, where answer is what vibey answer --raw takes. It answers through the same service as vibey answer: once, recorded under the caller's name. The same request_id with the same answer is a no-op ("replayed": true); a gate another request answered first is 409.
  • POST /api/v1/chat/messages takes {"message": "...", "project_id": "…", "request_id": "…"} and runs a conversational question or / command (/projects, /status, /gates, /answer, /budget, /queue, /ledger, /loops, /lanes, /doctor, /new, /start, /pause, /abandon, /help) over the same hub use cases and scope checks. It returns {"reply", "tool_calls", "gates", "project_id"}, where each open gate carries is_spend and requires_scope so chat clients badge spending gates clearly.
  • GET /api/v1/lanes lists the lanes on this computer, each with its byte offset.
  • GET /api/v1/doctor runs the checks the hub can run itself (the database, hub-exposure) and says so; vibey doctor on the host is the full check.

GET /health/live and GET /health/ready need no credentials and say only whether the process is up and whether the database answers. GET /api/metrics returns vibey_bootstrap's metrics snapshot and needs view.

GET / is the hub's front door: a plain page, with no data and no credential, that a person sees when krypton opens the hub in a browser. It says the hub is running and where to go next. It is not part of the API contract (it is absent from the OpenAPI document), it holds no script, and it is the one response whose Content-Security-Policy differs from the hub's default-src 'none': it adds the SHA-256 hash of its single style block, so the only style that runs is the one in the page. Every other route and every refusal keeps the hub's policy.

Live

A ledger feed resumes from a position, never a time (sub-doctrine 10.g): the last seq a client has. Every ledger append announces itself on the database channel vibey_ledger_appended (migration 0019), and the hub reads every event after the last seq it sent, so an announcement that is missed costs latency, never an event.

Route What it does
WS /api/v1/projects/{project_id}/live?after=N Sends {"project_id", "events", "last_seq"} pages of events with seq > N, up to 200 at a time until caught up, then one page per append. A quiet feed sends {"heartbeat": last_seq} every 25 s.
GET /api/v1/projects/{project_id}/ledger/after?seq=N&limit=L The same page, for a client that polls.
WS /api/v1/lanes/live?path=P&after=B A listed lane's complete lines after byte B, as {"events_path", "from", "offset", "lines"}, checked every second. Resume from offset.
GET /api/v1/lanes/tail?path=P&after=B The same, once.

path must be the events_path of a lane GET /api/v1/lanes lists; any other path is 404, and nothing else is ever opened. A WebSocket is refused (close code 1008) when its Host or Origin is not one the hub answers, or it proves no principal; and the principal is checked again before every page, so one revoked while a feed is open is cut off at its next message. Events are carried exactly as vibey ledger search --json carries them.

Who may do what

Every /api/v1 route and /api/metrics needs a principal. On the host, that is the host token: Authorization: Bearer <content of <state_dir>/token>, which holds every scope. A paired device instead signs every request (see Pairing) and holds only the scopes the host granted it.

Scope Permits
view Every read above.
answer Answering a gate that does not spend.
spend Answering a gate that does (budget_exhausted, the deploy_* gates), with answer.
run Starting, pausing, resuming and unwinding a project (Projects, ADR-0091).
bump Bumping a job -- and only when the project's [queue.priority] sources admits vibey-hub.
workflows Sending a vibey command to the repository's GitHub-hosted runners, with the scopes of what the command does (Workflows).

Nothing is permitted without a scope. And no scope, ever, can declare paid use, lift or change a cap, change the database DSN, run migrations or touch the canon: no route offers them.

Projects

Four routes, each needing run, start and steer a project. Each runs the command the CLI already offers, on the host, so they cannot drift from it or route around a check it makes (ADR-0091).

Route Body Does
POST /api/v1/projects {"name": "...", "intake": "..."} Starts a project and its worker; answers 201 with vibey new --json's document.
POST /api/v1/projects/{id}/work/start none Starts, or starts again, that project's own worker.
POST /api/v1/projects/{id}/work/pause none Stops that project's own workers and no one else's. A worker serving every project is left running and the answer says so (409).
POST /api/v1/projects/{id}/abandon {"reason": "...", "dry_run": false} Unwinds the project for good, recorded under the device's name; with dry_run, only what it would cancel.

A device never names a path. A name is letters, digits, dots, dashes and underscores (at most 64, starting with a letter or digit), and the folder is <[hub] projects_root>/<name>, made by the host. With no projects_root declared, starting a project is a 503 that says so; pausing, resuming and unwinding still work. An unknown key in a body is a 422, never a path the host would use, and a name whose folder exists is a 409, never adopted.

Workflows

POST /api/v1/workflows/runs {"argv": [...]} runs a vibey command on the repository's GitHub-hosted runners, as vibey -w does (ADR-0085), and answers 202 with the run's request_id. GET /api/v1/workflows/runs/{request_id} says where it is, and its exit code and output once it ran. Both need workflows, and the start needs the scopes of what the command does as well.

A request id is not a secret: the run carries it in its name on GitHub. So the hub binds each id it mints to the principal that asked (a nonce and an HMAC tag under a key in <state_dir>/run.key). A device reads back only a run it started. Any other id gets 404, as an unknown run does, and GitHub is never asked. The host reads any run.

Pairing

Only the host pairs, lists and revokes; a device never can, not even for itself.

Route Who What
POST /api/v1/pairing/offers {"scopes": [...]} host A 6-digit code, valid 120 seconds and spent by the first claim, with the pairing URI vibey-pair://<host>:<port>?fp=<sha256>&code=<code>&v=1 the QR code carries. At least one scope.
POST /api/v1/pairing/claim {"code", "name"} anyone The device's id, its key (returned this once) and scopes. Rate-limited per address; five wrong codes withdraw every open offer.
GET /api/v1/devices host The paired devices, never their keys.
DELETE /api/v1/devices/{id} host Revokes the device at once: its next request is refused.

A paired device signs each request with four headers: x-vibey-device (its id), x-vibey-timestamp (seconds since the epoch, within 60 of the host's clock), x-vibey-nonce (never reused) and x-vibey-signature, which is sha256= and the hex HMAC-SHA256, under the device's key, of these lines joined by \n: device id, method (upper case), path, query, timestamp, nonce, and the hex SHA-256 of the body. A replayed nonce, a stale timestamp, a changed method, path, query or body, another device's key or a revoked device proves nothing (401). There is no session and no cookie, so each action -- a spend answer included -- is verified on its own, and there is no ambient credential for a cross-site request to ride.

Every pairing and revocation is written to every project's ledger as HubDevicePaired / HubDeviceRevoked (the device's id, name, scopes and who acted -- never its key).

Refusals

Status Meaning
401 The request proves no principal. Nothing else is said.
403 The principal's scopes do not permit it, a device asked to manage pairings, a pairing code was wrong, spent or expired, or the repository does not admit the hub's bump.
404 No such project, open gate or paired device, or no workflows run this principal started.
409 The gate was already answered by another request, the queue refused the reorder, or a project's folder or checkout is taken, or a shared worker stands in the way of a pause.
421 The Host header is not one this hub answers (the DNS-rebinding defence).
422 The body or a filter is malformed.
429 Too many requests (empty body).
503 Pairing, or the workflows routes, are not enabled on this hub; or no [hub] projects_root is declared, or the host is resting (the Sabbath window).

Every response carries Content-Security-Policy: default-src 'none', X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer and Cache-Control: no-store, and none ever carries a CORS header.

Where it listens

Loopback, unless vibey.toml declares [hub] lan = true. With the LAN declared, the hub also answers this computer's host name, its .local name, its addresses and any [hub] names.