Grounded Docs
v0.2.0 · alpha Download Changelog GitHub
>_ docs / surfaces / HTTP API
§ 03 Surfaces

HTTP API

The API is the lowest common denominator: anything that can make an HTTP request can use Grounded. It's a small REST surface with the same return shapes as the console and MCP, served by the same process on the same origin.

// Getting in

Base URL & auth #

The service listens on GROUNDED_API_HOST (default 127.0.0.1) and GROUNDED_API_PORT (default 7437). Auth is off by default for localhost. Set GROUNDED_API_TOKEN (or install with --token) to require a bearer token — GET /health and GET /llms.txt stay exempt (an agent has to be able to read how to authenticate before it has a token); a bad token returns 401. The token is a single shared secret for the whole cabinet: scope and agent values on a request are self-declared by the caller, not enforced per-token — see doc lanes for what that means for scoping.

terminal :7437
>curl -s localhost:7437/health
>curl -s localhost:7437/facts \
-H 'authorization: Bearer $TOKEN'
// The surface

Routes #

MethodPathPurpose
GET/healthInstalled version, adapter, embeddings, add-on capabilities, Databox state, and counts. Unauthenticated.
GET/openapi.jsonOpenAPI 3.1 document.
GET/llms.txtThe agent-facing manual — routes, envelope shape, lane semantics, gotchas. Unauthenticated, plain markdown.
POST/recallHybrid recall → cited cards. Doc lane defaults to scopes: ["global"].
POST/impactDependency pre-flight for a literal subject (container, port, path) — lexical-only, crosses lanes citation-only. See below.
POST/briefAssemble a startup brief. Related-docs lane defaults to docScopes: ["global"]. Pass timezone (IANA) to render session dates in your zone — default UTC; an unknown zone is a 400.
GET/get/:typedIdFull record by fact:/session:/doc: id.
GET/factsList facts (?scope&scopes&status&limit&offset; status defaults to active, or pass archived/all).
POST/factsAdd a fact (201).
PATCH/facts/:idEdit a fact in place (only sent fields change).
DEL/facts/:idDelete a fact. Idempotent: 200 {deleted, id} — deleted:false means the row was already absent, never a 404.
GET/sessionsList sessions (?project&limit).
POST/sessionsAdd a session (201).
GET/sessions/:idOne session.
PATCH/sessions/:idEdit a session in place — a partial patch: omitted fields keep their stored values, and the row is re-embedded only when summary or details change. Accepts summary, details, project, workspace, agent, machine, tags, source; any other key is a 400.
DEL/sessions/:idDelete a session — hard delete of the row and its index entries, not reversible. Idempotent: 200 {deleted, id} — deleted:false means the row was already absent, never a 404.
POST/docs/ingestIngest paths → IngestReport (tag with a scope lane, default global).
POST/docs/pruneReconcile doc rows against disk → {missing, removed}; pass {remove:true} to delete orphans instead of marking them missing.
GET/docsList docs (?source&scope&scopes&documents&limit&offset; unfiltered by scope unless you pass one — see doc lanes).
GET/docs/:idOne doc chunk.
GET/visionList vision (?scope&status; default active).
POST/visionSet vision — edits the one active record per scope in place (201).
DEL/vision/:idDelete a vision record. Idempotent: 200 {deleted, id} — deleted:false means the row was already absent, never a 404.
GET/addonsThe add-on catalog and each add-on's state.
POST/addons/:id/installInstall and turn on an add-on (202, runs as a job). Local only.
POST/addons/:id/disableTurn an add-on off. Local only.
GET/databoxDatabox boxes and their state; meta.enabled: false when Databox is off.
GET/databox/:box · /databox/:box/schemaOne box's status and last build; its tables and columns.
POST/databox/:box/queryOne read-only SQL statement: {sql, limit?, timeoutMs?}.
POST/databox/:box/indexRebuild the index: {force?, wait?}. Local only.
GET/sharingLAN sharing status and the pairing code. Local only.
POST/sharingTurn sharing on and open a pairing window: {minutes?}, 1–60, default 3. Local only.
DEL/sharingTurn sharing off. Local only.

Local only means the route answers on the computer running Core and is refused with 403 over LAN sharing.

// Before you remove something

Dependency pre-flight #

POST /impact takes a subject, not a query — a literal token like a container name, a port, or a path, not a natural-language question. Body: {subject, limit?, sources?, project?, scopes?}. It's the check you run before you stop, remove, delete, or rename infrastructure: what depends on this? A dependency tripwire, not a search.

It's lexical-only by construction — there's no lexicalOnly option because there's no vector lane to turn off. That means impact holds on the zero-LLM path: set the embedding provider to none and dependency checks keep working exactly as before.

Impact is the one operation that crosses a lane boundary. Recall drops out-of-lane docs entirely; impact returns them citation-only: path, citation, and scope survive, title and snippet come back null, and inScope: false says why. You learn a dependency exists and where to look, without its content crossing the boundary — declare the lane in scopes to read it. Lane gating is docs-only: facts and sessions are never laned, so they always come back inScope: true, scope: "global".

meta.available counts withheld hits too — a found-but-hidden document isn't an absent one. And limit is authoritative here: it overrides the ranking source caps, because a dependency pre-flight isn't a top-10 reading list. Citations are chunk-grained: doc:{source}/{path}#chunk{N}. The TypeScript client exposes this as client.impact(subject, opts); the MCP tool is ground_impact (see MCP tools).

// What comes back

The {data, meta} envelope #

Every list-shaped response — facts, sessions, docs, vision, recall, and impact — comes back as an envelope, not a bare array: {data, meta}. data is the array itself; meta is a DeliveryMeta: {returned, available, truncated, limit, bySource?}. available is a real computed count of what matched — never data.length — so a caller can tell "this is everything" from "this is page one of more" without a second round trip.

terminal :7437
>curl -s localhost:7437/facts?limit=8
{ "data": [ …8 facts… ], "meta": { "returned": 8, "available": 24, "truncated": true, "limit": 8 } }

Recall's data holds compact cards — sourceType, typedId, title, score, matchedBy, citation, and a snippet under 200 characters, never full bodies — and meta.bySource breaks the count down per source type. A brief returns startupNote, vision, recentSessions, facts, and relatedDocs, plus rendered text when you ask for markdown — its list-shaped members carry the same envelope. On the brief's facts lane that envelope gains one more field, meta.indexed: the count of facts rendered as compressed index lines rather than full text. It is never folded into returned, and an indexed fact is not in droppedItems — it survived, just compressed. These are the exact shapes the console renders and MCP returns.

// When things go wrong

Errors #

Errors carry a stable code and map to HTTP status: validation → 400, not found → 404, an embedding failure → 503, storage/config → 500. An unknown path returns 404 {"code":"NOT_FOUND"}. DELETE is the exception: /facts/:id, /sessions/:id and /vision/:id are idempotent and never 404. GET on a missing row still does.

// The spec

OpenAPI #

The full OpenAPI 3.1 document is served live at GET /openapi.json — point your client generator or an API explorer at it. A thin typed TypeScript client (packages/client in the source repo) wraps the same surface if you'd rather not hand-roll fetches.