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 |