Skip to content

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 runningURL
Swagger UI — try requests in-browserhttp://127.0.0.1:8478/docs
ReDoc — clean reading referencehttp://127.0.0.1:8478/redoc
OpenAPI schema (JSON)http://127.0.0.1:8478/openapi.json

Endpoints

Method & pathPurpose
POST /api/v1/capturesCapture a URL (add ?mode=async for a background job)
POST /api/v1/captures/{id}/relinkRe-point a capture’s row at its note after you renamed the file (see refetch guide)
GET /api/v1/capturesList 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/jobsList 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/recallSemantic recall (top-k; optional min_score cosine floor — see score interpretation; ?view=lean for the token-lean shape)
GET /healthLiveness
GET /api/v1/metaDaemon 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 Accepted with a Job body and a Location: /api/v1/jobs/{id} header. Poll that URL to watch progress.
  • A dedup hit (the URL is already captured) short-circuits to 200 with the existing Capture and no job — nothing was created or queued.
  • Submit-time failures (an unsupported URL, a bad destination_path) are a synchronous 400 in 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:

StatusMeaning
queuedAccepted, waiting for a worker
runningA worker is executing the pipeline
succeededThe capture landed (possibly with degraded best-effort stages)
failedA 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 statusMeaning
pendingNot yet reached
runningIn progress
succeededDone
degradedBest-effort work failed, but the capture proceeds
skippedThe stage’s precondition wasn’t met
failedA 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. recall returns nested { capture, score } hits (the shape the Obsidian plugin parses); captures returns full Capture records. 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 with GET /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.

ResponseMeaning
200 + CaptureRe-pointed (or already correct — a no-op)
404No capture with that id
409The 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.