Skip to content

Web API reference

ZettelForge ships a FastAPI backend (web/app.py) that exposes the memory system over HTTP and serves the management SPA. Start it with:

python -m web.app --host 127.0.0.1 --port 8088

The server reports itself as ZettelForge v2.7.0. All paths under /api/ are guarded; the SPA at / is not.

Authentication

Every /api/* route depends on a single guard. Supply your key in either header:

X-API-Key: <key>
Authorization: Bearer <key>

Behavior depends on ZETTELFORGE_WEB_API_KEY:

Condition Result
Key set, request matches Allowed (constant-time compare)
Key set, missing or wrong 401 Valid API key required
Key unset, caller is loopback (127.0.0.1, ::1, localhost) Allowed — single-tenant local mode
Key unset, caller is non-loopback 503 Set ZETTELFORGE_WEB_API_KEY before exposing the web API

Binding to 0.0.0.0 or :: without a key set is refused at startup. Set the key before exposing the API beyond localhost.

Rate and write limits

Control Env var Default
Requests per minute, per key/IP ZETTELFORGE_WEB_RATE_LIMIT_PER_MINUTE 60
Concurrent writes ZETTELFORGE_WEB_WRITE_CONCURRENCY 2
Max query length ZETTELFORGE_WEB_MAX_QUERY_CHARS 1000
Max content length ZETTELFORGE_WEB_MAX_CONTENT_CHARS 50000
Max k ZETTELFORGE_WEB_MAX_K 50
Max sync limit ZETTELFORGE_WEB_MAX_SYNC_LIMIT 200

Exceeding the rate limit returns 429 Rate limit exceeded. Write endpoints return 429 when the write semaphore is exhausted.

Memory endpoints

POST /api/recall

Retrieve notes for a query.

Request:

Field Type Default Constraints
query string required 1–1000 chars
k int 10 1–50
domain string null ≤100 chars

Response:

{
  "query": "APT28 infrastructure",
  "results": [
    {
      "id": "...",
      "content": "...",
      "domain": "cti",
      "tier": "a",
      "confidence": 0.0,
      "created_at": "...",
      "entities": ["..."],
      "context": "..."
    }
  ],
  "count": 1,
  "latency_ms": 42
}

content is truncated to 500 chars and entities to 10 per result.

POST /api/remember

Store one note.

Field Type Default Constraints
content string required 1–50000 chars
domain string cti 1–100 chars
source_type string manual 1–100 chars
source_ref string "" ≤500 chars
evolve bool true

Response: { "note_id", "status", "entities", "latency_ms" }. Returns 429 Write capacity exhausted; retry shortly when the write semaphore is full.

POST /api/ingest

Bulk version of remember.

Request: { "items": [ { "content", "source_type"="manual", "domain"="cti", "evolve"=true } ] } — 1 to 100 items.

Response:

{
  "total": 3,
  "succeeded": 2,
  "failed": 1,
  "results": [
    { "note_id": "...", "status": "...", "entities": ["..."], "success": true, "error": null }
  ]
}

Per-item failures (including skipped when write capacity is exhausted) are reported in results; the call itself returns 200.

POST /api/synthesize

Generate an answer from retrieved notes.

Field Type Default Constraints
query string required 1–1000 chars
format string direct_answer one of direct_answer, synthesized_brief, timeline_analysis, relationship_map
k int 10 1–50

An unsupported format returns 400 Unsupported synthesis format. Response: { "query", "format", "synthesis", "sources_count", "latency_ms" }.

Synthesis format availability

In ZettelForge OSS, the extended formats (synthesized_brief, timeline_analysis, relationship_map) fall back to direct_answer output. Full multi-format synthesis is provided by the hosted ThreatRecall.ai SaaS. synthesize also depends on a configured LLM provider; without one it returns a well-formed result with no answer.

Knowledge graph endpoints

GET /api/graph/nodes

{ "nodes": [ { "id", "label", "type", "tier", "aliases", "confidence", "created_at" } ], "count" }

GET /api/graph/edges

{ "edges": [ { "id", "source", "target", "relationship", "created_at" } ], "count" }

GET /api/entities

Paginated entity index.

Query parameters: offset (≥0, default 0), limit (1–500, default 50), type, tier, q (substring filter on entity value).

Response: { "entities": [...], "total", "offset", "limit" }.

System endpoints

GET /api/health

Full system snapshot:

{
  "status": "ok",
  "version": "2.7.0",
  "edition": "community",
  "edition_name": "ZettelForge",
  "storage_backend": "sqlite",
  "embedding_provider": "...",
  "embedding_model": "...",
  "llm_provider": "...",
  "llm_model": "...",
  "llm_local_backend": "...",
  "enrichment_queue_depth": 0,
  "queue_status": "idle",
  "governance_enabled": false,
  "pii_enabled": false,
  "uptime_seconds": 0.0,
  "data_dir": "...",
  "graph_node_count": 0,
  "graph_edge_count": 0
}

GET /api/stats

{ "version", "edition", "edition_name", "total_notes", "notes_created", "retrievals", "entity_index" }

GET /api/storage

{ "total_notes", "entity_count", "graph_node_count", "graph_edge_count" }

GET /api/version

{ "version", "notes", "edition" } — used by the SPA header.

GET /api/telemetry

Aggregated counters from today's telemetry JSONL: total_queries, queries_today, recall_count, synthesis_count, syntheses_today, avg_latency_ms, p50_ms, p95_ms, top_intents. Returns zeroed fields when no telemetry file exists yet.

GET /api/history

Recent telemetry entries. Query parameters: limit (1–500, default 100), days (1–30, default 5). Returns a JSON array.

GET /api/logs

Tail of the structlog file. Query parameters: lines (1–10000, default 100), level (optional filter). Response: { "logs": [...], "truncated" }. Returns an empty list with an error field when no log file is configured.

Server-sent event streams

Path Stream
GET /api/logs/stream Tails the structlog file (text/event-stream)
GET /api/telemetry/stream Tails today's telemetry JSONL (text/event-stream)

Both emit data: <line>\n\n frames and handle log rotation.

Configuration endpoints

GET /api/config

Returns the full resolved config as JSON. Sensitive keys (such as api_key, password, and token) are recursively redacted to ***.

GET /api/config/meta

{ "restart_required_fields": [...] } — the dotted config paths that require a process restart, including backend, embedding.provider, llm.provider, llm.model, storage.data_dir, and the logging fields.

PUT /api/config

Apply config changes in memory. Accepts nested or dotted keys ({"retrieval.default_k": 5}). Response:

{
  "applied": ["retrieval.default_k"],
  "pending_restart": ["llm.model"],
  "message": "Config updated in-memory. Some changes require a restart."
}

Fields in restart_required_fields are reported under pending_restart and take effect only after a restart.

SaaS-only endpoints

POST /api/sync

OpenCTI synchronization is not available in ZettelForge OSS. In an OSS install this endpoint returns:

HTTP 501
{ "error": "OpenCTI sync requires an integration not present in this build." }

OpenCTI ingestion is provided by the hosted ThreatRecall.ai SaaS. See Two products, one core for the OSS-versus-SaaS split.

GET /api/edition reports the running build's capability set in its features map. On an OSS install edition is community and the integration-dependent features (OpenCTI sync, TypeDB STIX ontology, alias resolution over TypeDB, Sigma generation, context injection, multi-tenant auth) resolve to false; those capabilities belong to ThreatRecall.ai SaaS.

HTML routes

Path Auth Serves
GET / none Management SPA
GET /config guarded Config editor page