# REST API

> HTTP endpoints exposed by gno serve for search, documents, graph data, models, jobs, and workspace automation.

Section: Reference  
Canonical: https://gno.sh/docs/api  
Markdown: https://gno.sh/docs/api.md

## Compiled project context

POST /api/context/compiled/preview accepts {capsule, budgetTokens, budgetBytes?}; POST /api/context/compiled/check accepts {capsule, markdown}. Local SDK file helpers add compile/check/refresh. [Read the compiled context guide](https://gno.sh/docs/compiled-context) for limits, indexed freshness, inclusion, and recovery.

The REST API is the HTTP version of the local GNO engine. Start it with `gno serve`, then call `/api/*` from curl, scripts, desktop launchers, internal dashboards, or small apps. It is best when you want automation without embedding the SDK or installing an MCP client.

## What this is

- **Search API** — BM25, vector, hybrid query, and cited ask endpoints.
- **Workspace API** — documents, folders, collections, tags, links, graph, note presets, content-type metadata, and autocomplete.
- **Operations API** — status, sync, jobs, model pull status, and connector detection.

## Launch the server

```
gno serve # default port 3000
gno serve --port 8080
gno serve --detach # browser workspace + API in background
```

## Request lifetime and admission

Disconnecting a REST query or Ask request cancels its retrieval, expansion, generation, and verification, and no late result is sent. An embedding or rerank step that is already running may finish before its capacity frees up. Cancelled work is never retried automatically.

Once the server returns a job ID, the job keeps running even if the request that started it disconnects. Cancelling the job or stopping the server stops further work. Completed checkpoints are kept and unfinished embedding work stays pending.

When the server's read queue is full, requests get HTTP 429 `RATE_LIMITED`; a server that is unavailable or shutting down returns HTTP 503 `UNAVAILABLE`. Retry as a new explicit request when capacity or service is available. See [shared shutdown bounds](https://gno.sh/docs/architecture#resident).

If the index holds a collection the server's config does not list (you removed it from the config file by hand while the server ran, or another client indexed a collection this config omits), requests return HTTP 409 `STALE_COLLECTIONS` with a message naming the collections, and nothing is deleted. Add the collection back to the config, or run `gno cleanup` with the config that owns the index.

## Read endpoints

- `GET /api/health` — process liveness only. A successful response does not claim that retrieval is ready.
- `GET /api/status` — index stats, collection health, model state, background watcher state, onboarding state, and the shared`activation` object. Use `activation` for collection readiness: lexical proof, independent semantic state, persisted connector evidence, and projection completeness. Each request checks every collection folder: while one is missing or unreadable, its collection carries `rootError` (`{ code, message }`, never the folder path), `healthy` is false, and `health.checks` includes a `folder-access` warning. Its documents stay indexed, and both clear on the next request once the folder is back.
- `GET /api/capabilities` — feature availability for the active install.
- `GET /api/collections`, `GET /api/tags`, `GET /api/docs`, `GET /api/doc` — enumerate and fetch local content.
- `GET /api/metadata?collection=notes&values=20` — typed metadata keys with their types, document counts and most common values. `values` must be 1-100 (default 10); anything else returns `400 VALIDATION`.
- `GET /api/doc-asset` — stream original source bytes for a document. It supports `HEAD` and one HTTP `Range` request with `200`, `206`, or `416` responses; the native PDF viewer uses it for progressive same-origin loading and Download original.
- `GET /api/doc/:id/sections` — extracted heading outline (anchor, level, line, title). Compatible with Web UI readable `#anchor` links and SDK `getSections()`.
- `POST /api/doc/:id/section-targets` and `POST /api/doc/:id/section-targets/resolve` — create or conservatively resolve a durable `SectionTargetV1`. Exact and recovered responses include citation lines; ambiguous, stale, and missing omit citation and must not be navigated or cited.
- `GET /api/doc/:id/links`, `GET /api/doc/:id/backlinks`, `GET /api/doc/:id/similar`, `GET /api/graph` — graph and relationship data.
- `GET /api/events` — server-sent document change events for live UIs.
- `GET /api/traces`, `GET /api/traces/:traceId`, and loopback-only mutation routes — inspect, label, export, delete, or purge private local retrieval receipts.

Status is passive for connectors and models: it does not start MCP children, initialize or download models, or call remote inference. Connector evidence is a bounded projection of saved verification receipts. Only the first 64 target/collection pairs are listed; when `connectorProjection.truncated` is true, `connectorProjection.omitted` counts the unlisted pairs as passed, failed, incomplete or not applicable, and connector health is decided from every pair.

Status also projects `contentTypeBoost.rules` as rule IDs plus normalized factors and `contentTypeBoost.rulesFingerprint` as the full ranking fingerprint. It never exposes configured path prefixes.

The `chunking` status object reports configured and applied size/overlap, layout state, and pending document/mirror counts. Applied parameters are null for empty/mixed layouts. This is separate from embedding backlog and source-refresh errors. Reading status never changes the policy or starts a rebuild. See [chunking configuration](https://gno.sh/docs/configuration#chunking).

```
curl localhost:3000/api/doc-asset?uri=gno://notes/papers/spec.pdf \
  -H 'Range: bytes=0-65535' -o spec.part.pdf
```

The pinned PDF.js worker, character maps, and standard fonts are also served same-origin under `/vendor/pdfjs/*`. Only the known PDF.js files are served; no CDN is involved.

Models are resolved, downloaded, and loaded on first use, and an existing vector index starts without loading its model. Capability flags describe what is configured, whether or not a model is loaded. Loaded-model counts do not measure GPU memory. Polling status does not keep a model loaded: by default an idle model unloads after five minutes and reloads on the next request.

## Search endpoints

```
curl -X POST localhost:3000/api/search -d '{"query":"exact term"}'
curl -X POST localhost:3000/api/vsearch -d '{"query":"concept"}'
curl -X POST localhost:3000/api/query -d '{"query":"research question"}'
curl -X POST localhost:3000/api/ask -d '{"query":"what changed?"}'
curl -X POST localhost:3000/api/ask \
  -H 'content-type: application/json' \
  -d '{"query":"Who owns the launch decision?","verify":true}'
```

Search bodies mirror the CLI: `collection`, `limit`, `since`, `until`, `category`, `author`, `intent`, `exclude`, `tagsAny`, `tagsAll`, and mode-specific controls such as `noExpand`, `noRerank`, `candidateLimit`, and `queryModes`.

`POST /api/search` and `POST /api/query` also accept `collections`, a list of 1 to 256 collection names, to search several collections at once. The filter applies before the result limit, and the response lists the searched collections, sorted, in `meta.collections`. Sending both `collection` and `collections`, an empty list, or more than 256 names returns `400 VALIDATION`; names that are not configured return `404 NOT_FOUND` naming each one.

```
curl -X POST localhost:3000/api/query \
  -H 'content-type: application/json' \
  -d '{"query":"rollout plan","collections":["work","ops"]}'
```

Supported owner filters apply before the corresponding candidate budget. Vector and hybrid language filtering selects matching chunks; standalone lexical language remains reserved. Caller scope intersects filters, and an empty allowlist denies all. Excluded owners cannot consume the nearest-neighbor window, but score thresholds, deduplication, or limited eligible coverage can still produce short or empty results. Query/Ask evidence and generation budgets are unchanged.

Vectors are tied to each document's title and text and to the model and runtime that produced them, so documents with the same body but different titles can rank differently. If stored vector data is missing or corrupt, semantic search reports a failure and hybrid falls back to keyword results. Normal `gno embed` completes or repairs coverage.

Hybrid fallback reports `meta.vectorsUsed: false` if no vector search succeeded and `meta.mode: "bm25_only"` for wholly lexical retrieval. A successful vector search with no matches can report `vectorsUsed: true`. A model that failed to load is different from a search with no matches: keep the fallback details and send the request again once the model loads.

Search results include `source.absPath` when the hit is a resolvable file-backed document and the caller is on the same host. A remote caller (one that is not a same-host client: loopback peer, loopback `Host`, no forwarding headers) never receives `absPath` on any `/api/*` response, including search, query, ask, `/api/doc`, capture, and remember receipts; it opens results by `uri` and the collection-relative `relPath`. If `absPath` is absent, show the URI tail and do not offer file-open for that row. Default snippets skip leading YAML frontmatter and prefer document prose. Each `/api/search` snippet comes from the chunk that best covers the query terms, with the terms in `<mark>` tags, and `snippetRange` is that chunk's line span. A frontmatter-dominated snippet window falls back to stripped chunk prose. `line` follows that trimmed display range. The Web UI document page is a separate surface: `/doc?uri=<encodeURIComponent(uri)>`, not `GET /api/doc`.

Retrieval bodies may include up to 16 `projectHints`. These values are opaque and untrusted: the server does not resolve or echo them, does not check them against the filesystem, and does not let them change ranking. The response shape is the same with or without the field. REST diagnose returns the `query-diagnose@1.0` shape and omits `affinity`; affinity details come only from local CLI diagnose.

`POST /api/ask` keeps the existing raw Ask behavior when `verify` is absent or false. With `"verify": true`, it generates from one closed Context Capsule and returns the Capsule, freshness receipt, four-state per-claim verdicts, exact evidence IDs and line spans, coverage, gaps, semantic verifier state, and explicit abstention. The verified Ask body is closed: unknown fields fail validation.

Every substantive claim must be supported for the draft to be returned. Otherwise the response withholds it and reports `answerStatus: "abstained"`. A contradiction requires positive conflicting evidence; missing evidence is `insufficient`, not contradicted. Unavailable or failed semantic verification remains visible rather than silently falling back to an unverified answer.

```
curl localhost:3000/api/query \
  -H 'content-type: application/json' \
  -d '{
    "query": "authentication",
    "collection": "notes",
    "limit": 20,
    "tagsAny": ["security"]
  }'
```

## Context Capsules

`POST /api/context` compiles exact indexed evidence for one goal into a deterministic Capsule. The active server supplies the canonical index. The budget covers the complete payload—not each document separately—and the result includes exact URI/line spans, source and passage hashes, coverage gaps, omission counts, and requested capability fallbacks. It is returned to the caller and is not persisted.

Tag filters are normalized, lowercased, deduplicated, and validated before retrieval. Result and candidate limits cover the whole request across all collections, and the same request returns the same Capsule.

```
curl localhost:3000/api/context \
  -H 'content-type: application/json' \
  -d '{"goal":"Compare launch proposals","collections":["work"],"budgetTokens":12000, "depthPolicy":"fast"}'
```

`POST /api/context/verify` accepts a complete saved Capsule and returns a read-only receipt classifying evidence as unchanged, stale, or missing, with independent fingerprint and ranking state. Index mismatch and malformed Capsules fail before evidence reads. Add `"format":"md"` for the trust-delimited readable projection. Each untrusted block uses a collision-resistant Markdown fence derived from its content, so indexed text cannot forge the closing boundary. JSON is canonical by default.

Context API errors are deliberate: invalid input, filters, budgets, or identity return `400`; no evidence returns `404`; source, index, context, mutation, and provenance conflicts return `409`; tokenizer unavailability returns `503`; retrieval, load, snapshot, and runtime failures return `500`. Error messages come from a fixed public catalog; internal paths, causes, and stack traces are not returned.

## Knowledge Delta

```
GET /api/changes?since=<ISO-8601-or-cursor>&collection=notes&limit=100
GET /api/diff?ref=<document-ref>&change=<opaque-change-id>
GET /api/impact?ref=<document-ref>&collection=notes&maxDepth=3&maxNodes=100&maxEdges=250
```

These read-only endpoints share the CLI, MCP, and SDK contracts. Changes contain bounded identity, hash, lifecycle, and structural metadata—not source bodies. Diff discloses partial, expired, or unavailable retained history. Impact returns one bounded evidence path per dependent document; repeat `collection=` to limit it to those collections.

Saved-Capsule registration management remains CLI-only. The local `/api/events` stream can emit a closed `capsule-reverified` event after persistence, but never a question, label, path, URI, hash, receipt, credential, Capsule, passage, or source content.

Restoring an identical file at its former path reactivates it on the next successful sync and adds one `reactivate` change. Repeated unchanged syncs add nothing. Graph repair covers affected references in other collections. The journal stays metadata-only.

## Write and job endpoints

- `POST /api/collections` and `DELETE /api/collections/:name` — add or remove indexed folders.
- `POST /api/sync` — trigger reindexing from the browser or an automation, for every collection, one collection, or just some of its files. See [Reindex with POST /api/sync](https://gno.sh/docs/api#sync).
- `POST /api/capture` — capture a note with structured provenance, optional preset scaffolds, and a receipt that separates write, sync, and embed state.
- `GET /api/requests/:requestId` — look up the optional `requestId` sent with `POST /api/capture`, `POST /api/memory/remember`, or `PUT /api/docs/:id` before retrying. See [Retry-safe writes](https://gno.sh/docs/retry-safe-writes).
- `POST /api/docs`, `PUT /api/docs/:id`, `POST /api/docs/:id/refactor-plan`, `POST /api/docs/:id/rename`, `POST /api/docs/:id/move`, `POST /api/docs/:id/duplicate`, `POST /api/docs/:id/deactivate` — create and manage editable documents.
- `POST /api/folders` — create a folder inside a collection.
- `POST /api/models/pull`, `GET /api/models/status`, `GET /api/jobs/active`, `GET /api/jobs/:id` — model downloads and long-running job polling.
- `GET /api/connectors` — inspect configured integration targets without starting them. `POST /api/connectors/verify` — explicitly run one read-only, collection-scoped retrieval proof for an installed MCP target.

Reference-safe rename and move are deliberately two-step. Preview returns the canonical `file-refactor-preview@1.0` plan, including every examined reference, `canApply`, and a deterministic SHA-256 `planDigest`. Apply requires that exact digest, `schemaVersion: "1.0"`, and `confirmation: "apply"`. Current state is replanned before mutation, so stale, ambiguous, malformed, unsupported, read-only, occupied, and cross-collection plans fail closed. `applied_with_sync_pending` means the filesystem committed and only the later index refresh needs Update All; it is not a failed file move.

```
curl localhost:3000/api/connectors/verify \
  -H 'content-type: application/json' \
  -d '{"connectorId":"cursor-mcp","collection":"notes"}'
```

Connector verification is active only when this endpoint or the Web Connectors action is invoked. It does not edit client configuration. Skill targets return an explicit runtime-unverifiable result because GNO cannot prove that the client loaded or executed an installed skill file.

Use `/api/capture` for second-brain capture with `source:` frontmatter and presets such as `person`, `company-project`, `meeting`, or `idea-original`. Use `/api/docs` for raw note creation without provenance capture semantics. `/api/capture` writes the note and syncs it into text search before it responds, so a `201` capture is retrievable. If the file lands but sync fails, the response is `500 CAPTURE_SYNC_FAILED` with the written path in `details`; run `gno update`. Capture content must be text, `/api/capture` does not accept legacy `overwrite`, and capture writes fail instead of replacing a late-arriving file. The browser clipper route `POST /api/capture/clip` keeps its own contract (`202` plus a sync job, and an `Idempotency-Key`).

### Reindex with POST /api/sync

With no body, `POST /api/sync` reindexes every collection. `collection` limits it to one, `gitPull` runs `git pull` first, and `paths` re-reads only the listed files of `collection`, given relative to its root or as absolute paths inside it, at most 256 files. A listed file that no longer exists is removed from the index.

```
curl -X POST localhost:3000/api/sync \
  -H 'content-type: application/json' \
  -d '{"collection":"notes","paths":["a.md","sub/b.md"]}'
```

The response is `202` with a `jobId` and a `status`: `running` when the sync starts at once, or `queued` when another job is running or waiting. A sync for a collection that already has one waiting joins it and returns the same `jobId`, so any number of requests cause at most one more pass: listed files are added to it (past 256 files it becomes a whole-collection pass), and a whole-collection sync widens it to the whole collection. `GET /api/jobs/:id` reports `queued` until the pass starts, plus its `scope` (`collection`, and `paths` or `null` for the whole collection). Waiting syncs live in the server process; any still waiting when it stops do not run, and the index stays consistent.

- `400 VALIDATION` — a bad body, an empty `paths` list or one over 256 files, or `paths` without `collection` or together with `gitPull`.
- `400 INVALID_PATH` — a path outside the collection (including through a symlink), a directory, or a path its pattern, include, or exclude rules leave out.
- `409 CONFLICT` — another process holds the index write lock when the sync would start at once.
- `429 QUEUE_FULL` — 64 different syncs are already waiting. Retry after the running job finishes.

Only reindexing queues. Other jobs, such as adding a collection, are still refused with `409 CONFLICT` while a job runs.

## Security model

- **Loopback only.** `gno serve` binds the API to the local loopback interface. The API token does not turn it into a remotely reachable server.
- **CSRF protection.** Mutating browser requests need a same-origin local origin or an API token.
- **Optional token.** Set `GNO_API_TOKEN` and send `X-GNO-Token` from non-browser clients.

```
export GNO_API_TOKEN="secret"
gno serve
curl -H "X-GNO-Token: secret" localhost:3000/api/status
```

### Host paths and remote callers

A host absolute path shows the owner's home directory and folder layout, so only callers on the owner's machine receive one. A caller counts as same-host when the socket peer is loopback, the `Host` header names a loopback host, and no forwarding header is present. Every other caller gets `/api/*` JSON without these host path fields:

- `source.absPath` on search, query, and ask results and on `/api/doc`.
- Top-level `absPath` on capture and remember receipts, and in `CAPTURE_SYNC_FAILED` error details.
- The file's `path` on document create, editable copy, rename, move, duplicate, trash, and create-folder responses, and both `path` and the `file://` `uri` on a document save (`PUT /api/docs/:id`).

Remote callers identify and open a document by its `gno://` `uri` plus the collection-relative `relPath`, which is all `GET /api/doc?uri=` and `GET /api/doc-asset?uri=` need. A capture or remember error for a file that was written but not indexed names it by `gno://` URI on every surface. The same-host Web UI, the CLI, and the SDK keep `absPath` for Reveal and file-open; a remote Web UI opens originals through `/api/doc-asset`. Streamable HTTP MCP applies the same rule to every caller; see [MCP security posture](https://gno.sh/docs/mcp#security).

Configuration paths follow the same rule. A remote caller names a collection by its `name` and gets no:

- `configPath`, `dbPath`, or collection root `path` on `/api/status`, and no collection root `path` on `/api/collections` (list, create, and update).
- Suggested-folder, model cache, or model file `path` on `/api/status`, and no connector install `path` on `/api/connectors`.

On your own machine the Collections page, dashboard, and connector list show these paths as before. A remote Web UI shows the same pages without the path lines and without suggested-folder quick picks.

## Typed metadata filters

POST /api/search, /api/query, /api/query/diagnose, /api/ask, and /api/context accept the same filter object. Invalid predicates return a client error with a field path. Document detail returns stored typedMetadata and metadataError; do not infer types in clients.

[Read the metadata guide: examples, operators, missing fields, limits, and recovery.](https://gno.sh/docs/typed-metadata)

## Agent session archive

On a server started with the dedicated archive config and index: GET /api/sessions/status and POST /api/sessions/import with {sourceId, dryRun?, limit?}. Host paths are rejected. GET /api/sessions/discover, POST /api/sessions/sources, DELETE /api/sessions/sources/:id, and POST /api/sessions/init with {archive, collection} are same-host client only and return 403 otherwise. Same-host means the socket peer is loopback, the Host header names a loopback host, and no forwarding header is present; mutating requests also need the CSRF conditions, so a browser page on another origin is refused while a local process on the same machine (the Web UI, curl, a script) is treated as the owner. Errors carry the specific code in details.sessionsCode. Receipts, status, and errors carry no session content or host paths. [See the session error codes](https://gno.sh/docs/sessions#error-codes).

### Automation

```
POST   /api/sessions/automation/run            { "profileId": "claude" }
PUT    /api/sessions/automation/:id            { "sources": ["claude-code"], "cadence"?: "30m", "limit"?: 200, "retries"?: 3 }
GET    /api/sessions/automation/:id/preview
POST   /api/sessions/automation/:id/enable     { "hook"?: { "harness": "claude-code" }, "schedule"?: { "cadence"?: "30m" } }
POST   /api/sessions/automation/:id/disable    { "hook"?: true, "schedule"?: true }
DELETE /api/sessions/automation/:id
```

- `run` is open to any allowed client (CSRF-protected). It runs a configured profile through the importer and returns the shared sessions-automation-run object; unknown keys are rejected. It cannot enable triggers or widen sources.
- Every other automation route answers only a same-host client: remote requests cannot create profiles or enable machine integrations. enable installs the hook only into the profile's recorded settings file or the default $CLAUDE\_CONFIG\_DIR/settings.json (else \~/.claude/settings.json); a hook.settings field is rejected, so another settings file can only be chosen with the CLI. preview returns host paths, so it also applies the CSRF Origin check although it is a GET: a cross-origin page gets 403 CSRF\_VIOLATION. PUT, enable, and preview return the preview object (sources with host paths, destination collections, the hook command and settings file, the schedule, and the daemon prerequisite); disable and DELETE return `{ profileId, hook, schedule, pendingCleared, running, removed, warnings }`.
- GET /api/sessions/status includes the automation block (daemon state, timezone, and profiles). Changes are written to the archive config and applied to the running server; status also applies changes the CLI made to the config file, and a config file the server cannot read answers 500 with SESSIONS\_RUNTIME\_FAILURE. DELETE /api/sessions/sources/:id also succeeds for a source that is no longer registered. gno serve itself never drains pending hook work or runs schedules; that happens in gno daemon on the archive (see [Automation](https://gno.sh/docs/sessions#automation-opt-in)).
