REST API
The daemon exposes an HTTP API at 127.0.0.1:8478. The same operations back the
MCP server and the Obsidian plugin.
Interactive reference (always in sync)
Because every Khiip install runs the daemon locally, the authoritative, always-current API reference is the one the daemon serves itself — generated from the live code, so it never drifts from the version you’re running (no auth token required for these):
| When the daemon is running | URL |
|---|---|
| Swagger UI — try requests in-browser | http://127.0.0.1:8478/docs |
| ReDoc — clean reading reference | http://127.0.0.1:8478/redoc |
| OpenAPI schema (JSON) | http://127.0.0.1:8478/openapi.json |
Endpoints
| Method & path | Purpose |
|---|---|
POST /api/v1/captures | Capture a URL (add ?mode=async for a background job) |
POST /api/v1/captures/{id}/relink | Re-point a capture’s row at its note after you renamed the file (see refetch guide) |
GET /api/v1/captures | List captures (newest first; ?view=lean for the token-lean shape) |
GET /api/v1/jobs/{id} | Poll one capture job (terminal jobs embed the result capture) |
GET /api/v1/jobs | List capture jobs (newest first; optional ?status=) |
GET /api/v1/captures/{id}?format=… | Get a capture (format: capture JSON / payload JSON / vault markdown / legacy markdown) |
POST /api/v1/captures/{id}/refetch?dimension=… | Refetch (extraction / re-extract / re-render / media / wayback — see the refetch guide) |
GET /api/v1/recall | Semantic recall (top-k; optional min_score cosine floor — see score interpretation; ?view=lean for the token-lean shape) |
GET /health | Liveness |
GET /api/v1/meta | Daemon metadata |
All /api/v1 routes require the Bearer token in ~/.config/khiip/auth.toml — the CLI,
Obsidian plugin, and MCP server auto-discover it. /health, /docs, /redoc, and
/openapi.json are exempt from auth.
Background capture jobs (0.2.4)
POST /api/v1/captures runs synchronously by default: it blocks until the
capture lands and returns 201 + the Capture. Add ?mode=async to run it in
the background instead:
202 Acceptedwith aJobbody and aLocation: /api/v1/jobs/{id}header. Poll that URL to watch progress.- A dedup hit (the URL is already captured) short-circuits to
200with the existingCaptureand no job — nothing was created or queued. - Submit-time failures (an unsupported URL, a bad
destination_path) are a synchronous400in both modes; no job row is created.
Polling a job
GET /api/v1/jobs/{id} returns the Job. A terminal job (succeeded /
failed) embeds its result capture inline — no redirect to follow. A terminal
job whose capture was later deleted embeds capture: null.
GET /api/v1/jobs?status= lists jobs newest-first (optionally filtered by job
status); list rows are lean — capture is always null, so poll the single-job
endpoint for the embedded result.
The Job shape
A Job carries id, op ("capture"), url, capture_id (null until the
capture is written), status, stage (the current/last stage name), a stages
array, an optional error, and timestamps.
Job status is one of:
| Status | Meaning |
|---|---|
queued | Accepted, waiting for a worker |
running | A worker is executing the pipeline |
succeeded | The capture landed (possibly with degraded best-effort stages) |
failed | A fail-hard stage killed the capture; see error |
Each entry in stages is { name, status, ... } (plus optional started_at /
ended_at / detail; the media stage also carries current / total
download counters).
Stage names — a fixed set of eight:
extract · enrich · media · preserve · wayback · render · write · embed
Per-stage status is one of:
| Stage status | Meaning |
|---|---|
pending | Not yet reached |
running | In progress |
succeeded | Done |
degraded | Best-effort work failed, but the capture proceeds |
skipped | The stage’s precondition wasn’t met |
failed | A fail-hard stage that killed the capture |
On the base tier the enrich stage always reports skipped (enrichment is a
Khiip Plus stage).
error, when present, is { stage, message }. It is set only when a
fail-hard stage kills the capture — degraded and skipped stages never produce a
job error. error.stage is always one of extract or write (the two stages
whose failure ends a capture); error.message wording is informational and may
change between releases.
Lean vs. full rows (0.2.4)
GET /api/v1/recall and GET /api/v1/captures both accept ?view=:
full(default) — the complete records.recallreturns nested{ capture, score }hits (the shape the Obsidian plugin parses);capturesreturns fullCapturerecords. Unchanged from earlier releases.lean— a flat, token-lean projection built for LLM agents, so a large recall response doesn’t blow an agent’s context window. Drill down into any hit withGET /api/v1/captures/{id}.
A lean row carries: id, url, source, title, author, recorded_at,
vault_path, description, excerpt, excerpt_from, and payload_chars
(recall’s lean hits add score). excerpt is the head of the capture’s primary
text, capped at 500 characters with a trailing … only when it was truncated;
excerpt_from names which field it came from; payload_chars is the exact size
of the full payload JSON, so an agent can budget the drill-down before making it.
The recall response envelope (query, embedder_model, embedder_dimension,
results) is identical in both views — only the per-hit shape changes — and
scores and ranking are the same either way.
Re-linking a renamed note (0.2.4)
If you rename a captured note in your vault, its database row still points at the
old path. Refetch and re-render self-heal this when the note can be uniquely
re-identified, and POST /api/v1/captures/{id}/relink runs the same repair on
demand. It never creates or moves files — only the row’s cached vault path
changes.
| Response | Meaning |
|---|---|
200 + Capture | Re-pointed (or already correct — a no-op) |
404 | No capture with that id |
409 | The note can’t be uniquely resolved — its id matches no vault note, matches more than one, or the matched note’s frontmatter no longer has the shape Khiip writes. The detail says which. |
See the refetch & validate guide for how this fits
with khiipd validate --fix.