HTTP API
The read endpoints, their ETags, and the one response that is not JSON.
HTTP/1.1 and JSON, with one deliberate exception. The manifest goes out as raw bytes, because JSON-escaping a TSV adds bytes and parse cost to the payload transferred most often.
The same routes serve a unix socket and a TCP listener. The socket still speaks HTTP underneath.
Read endpoints
GET /v1/{tenant}/manifest[?bundle=sales] the manifest, ETag is the snapshot digest
GET /v1/{tenant}/snapshot {"snapshot": "…", "concepts": n}
POST /v1/{tenant}/get {"ids": [...], "section"?, "offset"?, "limit"?, "find"?}
POST /v1/{tenant}/search {"q": "...", "k"?: n, "expand"?: false, "bundle"?: "..."}The tenant segment is optional when a token identifies one, so /v1/manifest and
/v1/acme/manifest both work. It is required when the server has no tokens at all, which only
happens on a unix socket.
curl -H 'Authorization: Bearer read-only' http://127.0.0.1:7777/v1/manifest
curl -X POST -H 'Authorization: Bearer read-only' -H 'content-type: application/json' \
-d '{"ids":["orders"],"section":"schema"}' http://127.0.0.1:7777/v1/getConditional requests
The manifest carries an ETag set to the snapshot digest, or to digest:bundle when narrowed.
Send If-None-Match with the ETag you hold and an unchanged manifest answers 304 with no body.
curl -H 'Authorization: Bearer read-only' -H 'If-None-Match: "9ac37f10832c…"' \
http://127.0.0.1:7777/v1/manifestBetween turns of a conversation that is the whole transfer skipped. snapshot is the cheaper check
when you only want to know whether anything moved.
Responses
| Endpoint | Content type | Shape |
|---|---|---|
manifest | text/tab-separated-values | Raw TSV, comment lines then a header row |
snapshot | application/json | {"snapshot": "…", "concepts": n} |
get | application/json | An object of slices keyed by id, missing ids absent |
search | text/tab-separated-values | Manifest rows plus a trailing pos column, with # query and # hits comments |
get returns an object rather than an array, so a missing id is simply not a key. Nothing errors
when one of a batch does not exist. Each value is a slice — {"text", "start", "end", "total"},
plus "matches" and "matchCount" when find was set — so a caller always knows how much text
exists beyond what it received.
Get parameters
| Field | Default | Effect |
|---|---|---|
ids | required | Concept ids from the manifest |
section | none | One named section instead of the whole document |
offset | 0 | Character offset into the addressed text |
limit | none | Maximum characters returned per concept |
find | none | Literal case-insensitive phrase; the slice becomes a window around the first occurrence, 2,000 characters unless limit says otherwise, and offset is ignored |
The HTTP API never truncates unless asked. The 15,000-character default cap lives at the MCP boundary only, because that is the one caller that is a prompt rather than a program.
search returns manifest rows and never bodies. Each direct hit ends in a pos cell — the offset
where the query's words cluster densest in the body, - when they only match the row — so the
follow-up get can be {"offset": pos, "limit": 2000} instead of the whole document. Narrow with
search, then fetch what you chose with get.
Search parameters
| Field | Default | Effect |
|---|---|---|
q | required | The query. An empty string is a 400. |
k | 8 | Ranked matches before link expansion |
expand | true | false disables the one-hop expansion over the link graph |
bundle | none | Rank only within this bundle, and expand only into it |
The expansion is capped at k and ranked by how many hits point at a target, so a hub concept with
sixteen outgoing links cannot drag most of the manifest into the result.
Status codes
| Code | When |
|---|---|
| 200 | Fine |
| 204 | A write or delete on the source API succeeded |
| 304 | If-None-Match matched the current ETag |
| 400 | A malformed body, or a missing concept path |
| 401 | Missing or unknown bearer token |
| 403 | The token does not grant that tenant, or may read but not write |
| 404 | No such route, no such concept, no such bundle |
| 405 | Wrong method for the route |
| 409 | The tenant has no source directory, or the server cannot recompile |
| 412 | A precondition failed on a write |
| 413 | A concept larger than 1,000,000 bytes |
| 428 | A write with no precondition header |
Errors carry a plain-text message, not a JSON envelope.
404 is also the catch-all
Anything thrown that is not an explicit HttpError becomes a 404 carrying its message. A failed
compile during sync therefore arrives as "not found" rather than as a 500, so a client should
read the body instead of switching on the status alone.
Write endpoints
Creating, changing and deleting concepts goes through the source API, which writes Markdown rather than snapshots. Those routes are covered in Editing over the network.
GET /v1/{tenant}/source
GET /v1/{tenant}/source/{bundle}/{path}
PUT /v1/{tenant}/source/{bundle}/{path}
DELETE /v1/{tenant}/source/{bundle}/{path}
DELETE /v1/{tenant}/bundles/{bundle}
POST /v1/{tenant}/syncNo HTTP request ever writes a snapshot
The watcher recompiles from source, so a snapshot written directly would be overwritten within seconds. Writing source is the path the design endorses, and it leaves the read API above untouched.