Hub API reference¶
[!IMPORTANT] Work in progress — not a final product: The
vibeyHub 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
vibeyandkryptonin 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}/answertakes{"answer": {...}, "request_id": "…"}, whereansweris whatvibey answer --rawtakes. It answers through the same service asvibey answer: once, recorded under the caller's name. The samerequest_idwith the same answer is a no-op ("replayed": true); a gate another request answered first is 409.POST /api/v1/chat/messagestakes{"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 carriesis_spendandrequires_scopeso chat clients badge spending gates clearly.GET /api/v1/laneslists the lanes on this computer, each with its byteoffset.GET /api/v1/doctorruns the checks the hub can run itself (the database,hub-exposure) and says so;vibey doctoron 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.