HTTP API
HTTP API surface — REST + SSE under /v1, designed AI-first.
Status: Draft v0.1 · 2026-05-03
Base URL: http://<host>:3000/v1
Auth: Authorization: Bearer <token>
Spec: OpenAPI 3.1 served at GET /openapi.json
AI runtime: see AI.md for the full surface; this document defines its HTTP transport.
1. Design principles
Section titled “1. Design principles”The API is the canonical surface; the CLI and MCP server are adapters over it. So the API’s design constraints are stricter than either:
- JSON in, JSON out. No multipart, no form-encoded bodies (except attachment uploads).
- Resource-oriented. Nouns in URLs, verbs in methods.
- One envelope. Every response is
{ ok, data | error }. Errors follow RFC 7807 Problem Details shape insideerror. - Optimistic concurrency.
ETagon every read,If-Matchhonored on every write. - Idempotency.
Idempotency-Keyheader accepted on every write; replays return the original response. - Cursors, not offsets. All list/search endpoints paginate via opaque
cursor. - Streaming for long ops. SSE for change feeds, NDJSON for large result sets, both negotiated via
Accept. - Schema-first. Zod → JSON Schema → OpenAPI 3.1, served live from the running server.
2. Authentication
Section titled “2. Authentication”Authorization: Bearer indx_<32 hex chars>A token has zero or more scopes:
| Scope | Allows |
|---|---|
vault:read | All GET on /v1/notes/*, /v1/links/*, /v1/tags, /v1/search, /v1/canvas/*, /v1/bases/*, /v1/vault/status, /v1/events |
vault:write | PUT / PATCH / DELETE / POST on writable resources |
vault:admin | POST /v1/vault/reindex, GET/PUT /v1/vault/config, token management |
path:<glob> | Restrict any of the above to paths matching <glob> |
Missing token → 401. Wrong scope → 403. Both follow the error envelope (§4).
3. Common headers
Section titled “3. Common headers”| Header | Direction | Purpose |
|---|---|---|
Authorization | request | Bearer token (required) |
Idempotency-Key | request | Replay-safe writes (UUID v4 recommended) |
If-Match | request | Optimistic concurrency on writes |
If-None-Match | request | Conditional GET; 304 if unchanged |
Accept | request | application/json (default), application/x-ndjson, text/event-stream |
ETag | response | Strong ETag = first 16 chars of xxhash64 of content |
Last-Modified | response | mtime |
X-Indx-Actor | response | The actor recorded for the change |
X-Indx-Indexed-At | response | Index timestamp |
4. Error envelope
Section titled “4. Error envelope”{ "ok": false, "error": { "type": "https://indx.dev/errors/conflict", "code": "etag_mismatch", "title": "ETag mismatch", "status": 409, "detail": "Note Home.md has changed since you read it.", "instance": "/v1/notes/Home.md", "current_etag": "ab12cd34…", "received_etag": "00000000…" }}Every error has a stable machine code (code) and an HTTP status (status). The code is the contract; the title/detail strings can change.
4.1 Error codes (canonical)
Section titled “4.1 Error codes (canonical)”| HTTP | code | When |
|---|---|---|
| 400 | bad_request | Malformed JSON, missing required field |
| 400 | validation_failed | Zod validation error; details.errors array |
| 401 | unauthorized | No token / invalid token |
| 403 | forbidden | Token lacks scope |
| 404 | not_found | No such resource |
| 409 | etag_mismatch | If-Match failed |
| 409 | already_exists | Create with conflicting path |
| 410 | gone | Path exists in tombstone |
| 415 | unsupported_media_type | Bad Content-Type |
| 422 | parse_failed | Markdown/canvas/base parse error |
| 423 | readonly | INDX_READONLY=true |
| 429 | rate_limited | Too many requests for this token |
| 500 | internal | Unexpected — please file a bug |
| 503 | reindexing | First-boot reindex still in progress |
| 422 | ai_grounding_failed | All model citations failed verification under cite: true (AI.md §6) |
| 422 | ai_input_too_large | Resolved AI scope exceeds INDX_AI_MAX_INPUT_TOKENS |
| 429 | ai_quota_exceeded | INDX_AI_DAILY_COST_USD reached |
| 502 | ai_provider_error | Upstream chat/embeddings call failed |
| 503 | ai_unavailable | No suitable AI provider configured for this op |
| 422 | ai_schema_invalid | ai_extract.schema is not a valid JSON Schema |
| 409 | ai_apply_conflict | One or more per-path if_match checks failed under apply: true |
5. Resource model
Section titled “5. Resource model”| Path | Resource | Methods |
|---|---|---|
/v1/health | health probe | GET |
/v1/notes/{path} | a markdown note | GET, PUT, PATCH, DELETE |
/v1/notes/{path}/move | rename | POST |
/v1/notes | listing | GET |
/v1/search | search | GET |
/v1/links/{path}/backlinks | inbound links | GET |
/v1/links/{path}/forward | outbound links | GET |
/v1/links/unresolved | broken links | GET |
/v1/links/orphans | orphan notes | GET |
/v1/tags | all tags | GET |
/v1/tags/{tag}/notes | notes for a tag | GET |
/v1/tags/rename | bulk rename | POST |
/v1/canvas/{path} | a JSON Canvas | GET, PUT, PATCH |
/v1/bases/{path} | a .base file | GET, PUT |
/v1/bases/{path}/query | run the base | GET |
/v1/vault/status | health/counts | GET |
/v1/vault/reindex | trigger reindex | POST |
/v1/vault/config | runtime config | GET, PUT |
/v1/events | live changes | GET (SSE) |
/v1/ai/status | AI runtime status | GET |
/v1/ai/summarize | summarize a scope | POST |
/v1/ai/ask | grounded Q&A over the vault | POST |
/v1/ai/toc | TOC (single note) or MOC (folder/glob/tag) | POST |
/v1/ai/relate | related notes + relation typing | POST |
/v1/ai/tag | tag suggestion / application (frontmatter tags:) | POST |
/v1/ai/metadata | typed frontmatter generation / population | POST |
/v1/ai/extract | structured entity extraction into a JSON Schema | POST |
/openapi.json | spec | GET |
/mcp | MCP HTTP transport | POST/GET |
/v1/ai/* endpoints are advertised in /openapi.json iff a provider is
configured (AI.md §3). Without a provider,
calls return 503 ai_unavailable and the OpenAPI document omits them.
5.1 Path encoding
Section titled “5.1 Path encoding”Resource paths are vault-relative POSIX paths, percent-encoded once. Projects/Foo Bar.md becomes Projects/Foo%20Bar.md. The server normalizes (no .., no leading /).
6. Notes
Section titled “6. Notes”6.1 GET /v1/notes/{path}
Section titled “6.1 GET /v1/notes/{path}”Query:
include— comma-separated:outline,links,tags,frontmatter(default: all). Useinclude=to get onlybody+ metadata for cheaper reads.
GET /v1/notes/Home.md?include=outline,frontmatter{ "ok": true, "data": { "path": "Home.md", "kind": "md", "etag": "ab12cd34ef567890", "mtime_ms": 1746230400000, "size": 1234, "frontmatter": { "title": "Home", "tags": ["index"] }, "outline": [ { "level": 1, "text": "Home", "line": 0, "block_id": null }, { "level": 2, "text": "Today", "line": 4, "block_id": null } ], "body": "# Home\n\n## Today\n\n…" }}6.2 PUT /v1/notes/{path} — create or replace
Section titled “6.2 PUT /v1/notes/{path} — create or replace”PUT /v1/notes/Home.mdContent-Type: application/jsonIf-Match: ab12cd34ef567890 ← omit on createIdempotency-Key: 5e3c…
{ "frontmatter": { … }, "body": "…" }frontmatterandbodyare merged into a single file.bodymay include its own----fenced frontmatter iffrontmatteris omitted.- Returns the full resource (read-after-write) with the new ETag.
6.3 PATCH /v1/notes/{path} — structured edit
Section titled “6.3 PATCH /v1/notes/{path} — structured edit”PATCH /v1/notes/Home.mdContent-Type: application/json
{ "ops": [ { "op": "insert_after_heading", "heading": "## Today", "markdown": "- [ ] follow up with Alice\n" }, { "op": "set_frontmatter", "key": "updated", "value": "2026-05-03" } ]}Patch grammar matches the CLI (see CLI.md §3.1 and SPEC.md §6.2).
6.4 DELETE /v1/notes/{path}
Section titled “6.4 DELETE /v1/notes/{path}”Hard delete. Returns 204 with no body. The path is added to a 5-minute tombstone so subsequent reads return 410 (vs 404), giving the agent a chance to detect “this was deleted, not missing-from-the-start”.
6.5 POST /v1/notes/{path}/move
Section titled “6.5 POST /v1/notes/{path}/move”{ "to": "Archive/Home.md", "update_links": true }Returns:
{ "ok": true, "data": { "from": "Home.md", "to": "Archive/Home.md", "rewrites": 7 } }6.6 GET /v1/notes — listing
Section titled “6.6 GET /v1/notes — listing”Query:
path_glob(e.g.Projects/**)tag(repeatable)frontmatter(key=value, repeatable)limit(default 50, max 1000)cursor
Accept: application/x-ndjson streams one JSON object per line, no envelope, no cursor — useful for very large vaults.
7. Search
Section titled “7. Search”7.1 GET /v1/search
Section titled “7.1 GET /v1/search”GET /v1/search?q=runbook&mode=hybrid&tag=ops&limit=20Response:
{ "ok": true, "data": { "results": [ { "path": "Ops/Runbook.md", "title": "Runbook", "score": 0.812, "snippet": "…the **runbook** for incident response…", "tags": ["ops", "oncall"], "matched_in": ["title", "body"] } ], "next_cursor": null, "mode_used": "hybrid", "warnings": [] }}If mode=hybrid|semantic is requested without embeddings configured, mode_used will be lexical and warnings will include embeddings_unavailable.
8. Links, tags, canvas, bases
Section titled “8. Links, tags, canvas, bases”These are direct mappings of the resources in §5; payload shapes are in the OpenAPI spec. Notably:
GET /v1/canvas/{path}returns the JSON Canvas object verbatim, plus indx-addedetagandmtime_ms.PATCH /v1/canvas/{path}accepts canvas patch ops (add_node,update_node,add_edge, …).GET /v1/bases/{path}/queryreturns NDJSON rows whenAccept: application/x-ndjson, otherwise a paginated array.
9. Events (SSE)
Section titled “9. Events (SSE)”GET /v1/eventsAccept: text/event-streamAuthorization: Bearer …Each event is one JSON object per data: field:
event: note.updateddata: {"path":"Home.md","etag":"…","at":"2026-05-03T01:23:45Z","actor":{"kind":"api"}}
event: note.deleteddata: {"path":"Old.md","at":"…","actor":{"kind":"fs"}}Filters via query string: ?paths=Projects/**&kinds=note.updated,note.created.
Resume after disconnect via standard SSE Last-Event-ID (every event has a monotonic id).
10. Vault admin
Section titled “10. Vault admin”GET /v1/vault/status → counts, last reindex, embedding statusPOST /v1/vault/reindex → kick a full reindex; 202 with a job id; subscribe via /v1/eventsGET /v1/vault/config → effective merged configPUT /v1/vault/config → patch config; returns 200 + new config11. Streaming and content negotiation
Section titled “11. Streaming and content negotiation”| Endpoint | Accept: application/json (default) | Accept: application/x-ndjson | Accept: text/event-stream |
|---|---|---|---|
/v1/notes (list) | paginated array | NDJSON, no cursor | n/a |
/v1/search | paginated array | NDJSON, no cursor | n/a |
/v1/bases/{p}/query | paginated array | NDJSON | n/a |
/v1/events | n/a | n/a | SSE |
12. Idempotency
Section titled “12. Idempotency”A write request with Idempotency-Key: <uuid>:
- The first call processes normally and caches the response (status + body) keyed by
(token, method, path, key)for 24h. - Subsequent calls with the same key return the cached response with header
X-Indx-Idempotent-Replay: true. - A subsequent call with the same key but a different request body returns
409 idempotency_key_reused.
This pairs naturally with If-Match for safe agent retries.
13. Rate limiting
Section titled “13. Rate limiting”Per token, sliding window: 100 rps, 1000 burst by default. 429 with:
Retry-After: 1X-RateLimit-Limit: 100X-RateLimit-Remaining: 0X-RateLimit-Reset: 174623040114. OpenAPI
Section titled “14. OpenAPI”GET /openapi.json returns the full OpenAPI 3.1 document. The CLI indx schema openapi prints the same. Use it to:
- Generate typed clients (e.g.
openapi-typescript). - Validate request bodies before sending.
- Discover new capabilities after a server upgrade.
/openapi.json reflects the running server’s exact capabilities including any feature flags (e.g. embeddings ops are absent if not configured).
14.1 AI runtime
Section titled “14.1 AI runtime”/v1/ai/* is the HTTP transport for the operations defined in
AI.md. The contract:
- All AI ops are
POSTand accept a JSON body validated by a Zod schema in@indx/shared(SPEC §14). Request shapes matchAI.md §5field-for-field. - Responses use the same
{ ok, data | error }envelope as the rest of/v1. Idempotency-Keyis honored on every AI op (yes — even read-shaped ones, so retries don’t re-charge generation). SeeAI.md §8.2.- Streaming:
Accept: text/event-streamswitches the response to an SSE stream ofai.partial,ai.citation,ai.usage,ai.completeevents. Non-streaming clients sendAccept: application/json(default). /v1/ai/statusis aGETreturning{ enabled, provider, model, embeddings, cache, spend_today_usd }. Cheap; never depends on a provider call.vault:readis sufficient for/v1/ai/summarize,/v1/ai/ask,/v1/ai/relate,/v1/ai/toc(mode: "note"ormode: "moc"withoutwrite), and the suggest-mode of/v1/ai/tag,/v1/ai/metadata,/v1/ai/extract. Apply-mode (apply: trueon tag/metadata/extract orwriteon toc) requiresvault:writeand honors per-pathif_matchfor optimistic concurrency./v1/ai/statusrequiresvault:read.- Errors include the AI-specific codes added to §4.1 (
ai_unavailable,ai_quota_exceeded,ai_provider_error,ai_grounding_failed,ai_input_too_large,ai_schema_invalid,ai_apply_conflict).
POST /v1/ai/askAuthorization: Bearer indx_…Content-Type: application/jsonAccept: text/event-streamIdempotency-Key: 0c3e8e9c-…
{ "question": "what does the etag scheme guarantee?", "retrieve": { "mode": "hybrid", "top_k": 8 } }event: ai.partialdata: {"delta":"The ETag is the first 16 hex of"}
event: ai.citationdata: {"citation":{"path":"SPEC.md","etag":"…","anchor":"6.1 Atomic write"}}
event: ai.completedata: {"answer":"…","confidence":"high","citations":[…],"usage":{…},"warnings":[]}15. Versioning
Section titled “15. Versioning”- URL prefix
/v1is stable; additive changes only. - Breaking changes ship under
/v2with/v1kept available for one minor release. - A
Deprecation: <date>header appears on any path scheduled for removal, with aLink: <docs>; rel="deprecation".