API reference

Base URL: http://127.0.0.1:8080. JSON in, JSON out. On localhost no auth is needed; when a key is active, send Authorization: Bearer <key> (see config).

Memories

Route What it does
POST /memories Write 1–100 memories. Exact duplicates → unchanged; near-duplicates supersede as a new version (old kept); else created. {"dedupe":false} bypasses.
GET /memories List latest, non-forgotten memories.
GET /memories/:id One memory + its full version chain (forgotten versions included — inspection hides nothing).
PATCH /memories/:id Content change → new version (409 + latestId if the target isn’t latest). Flag-only changes update in place.
POST /memories/:id/forget Soft-forget the whole chain; {"undo":true} reverses it.
DELETE /memories/:id Hard-delete the chain + provenance links. The only true eraser.

The chain views — GET /memories/:id and PATCH responses — carry validFrom / validTo on every version row (ISO or null = open-ended); the flat GET /memories list and POST results don’t. Supersedes and content edits stamp the flip instant on both sides in one transaction, so a chain is a gapless timeline of what was believed when — queryable with asOf on search (below). Rows written before this feature have null windows and count as always-valid.

curl -s localhost:8080/memories -H 'content-type: application/json' \
  -d '{"containerTag":"user_123","memories":[{"content":"John prefers dark mode","isStatic":true}]}'

Documents

Route What it does
POST /documents Ingest {title?, content, containerTag?} — structure-aware markdown chunking, embedded and searchable.
GET /documents / GET /documents/:id List / read a document with its chunks and quality flags.
DELETE /documents/:id Delete document + chunks + links.
curl -s localhost:8080/search -H 'content-type: application/json' \
  -d '{"q":"what theme does John like","searchMode":"memories"}'

searchMode: memories (semantic + full-text recall, rank-fused, recency-weighted), documents (vector + full-text over chunks, rank-fused), or hybrid (both lists fused). recency: false turns off time-decay for a request. Memory results also get an MMR diversity pass so near-duplicate memories don’t crowd out distinct ones: on by default when limit >= 5, diversify: false turns it off, diversify: true forces it at any limit. It reorders only — no extra model, no re-embedding. Results carry similarity (raw cosine evidence; 0 for keyword-only hits) and score (the fused ranking score); the returned order is the ranking authority (with diversity on, a redundant high-scorer may rank below a distinct lower-scorer), plus a traceId — the receipt.

asOf asks “what was believed then?”: instead of latest-only, memory search returns the versions whose validity window covers that instant — including superseded ones (validFrom inclusive, validTo exclusive; the flip boundary belongs to the newer version). It must be an unambiguous instant: an ISO 8601 date-time with an explicit timezone (2026-07-03T12:00:00Z, 2026-07-03T07:00:00-05:00) or a plain date (2026-07-03 = UTC midnight), year 0001–9999. Anything else — including timezone-less date-times, whose meaning would depend on the server — is a 400. Two boundaries to know: memories written before windows existed match every asOf (a pre-feature chain returns all its versions), and forgetting/expiry apply at query time — a fact forgotten since then stays hidden even for a past asOf, unless you ask for forgotten memories. Memories only — documents don’t version, so chunk results ignore it.

Profile

GET /profile / PUT /profile — static + dynamic facts injected as context on proxied chats.

Export / import (portability)

GET /export (optional ?containerTag=) returns everything as one versioned JSON document (format: "bellamente-export", version: 1): memory chains with their versions, validity windows and forgotten status, per-container profiles, ingested documents, and document-level provenance. No embeddings are in the file — it stays small and works across embedder tiers.

POST /import restores such a file: fresh ids (old ids remapped, chain relations preserved), version numbers and validity windows verbatim, embeddings regenerated by the local embedder (never trusted from the file), documents re-chunked through the real ingestion path. Re-importing the same file is a no-op (chain-level and document-level dedup); a malformed chain fails alone while the rest lands. The response reports imported / skipped / failed / embedFailures. Traces are never exported (receipts are device-local), and chunk-level provenance doesn’t survive re-chunking — document-level links do.

Inspect (the receipts)

GET /inspect — recent traces. GET /inspect/:id — one trace: what was searched, retrieved, injected, scores, latency, and status (answered, streamed, capture, errors…).

Chat proxy

POST /v1/chat/completions — OpenAI-compatible. Forwards to your local model (BELLA_UPSTREAM_BASE_URL), injects the searchMemory tool + profile context, runs the memory round when the model asks (buffered and streamed), and traces everything. Diagnostic headers on every response:

Header Meaning
x-bella-trace-id The trace for this turn (open it in /inspect).
x-bella-memory-round true when recall results were injected into the final answer.
x-bella-context-modified true when tool/profile context was injected.
x-bella-search-results How many memories fed the answer.
x-bella-streaming true when the body streams.

Health

GET /health{"ok":true,"service":"bellamente","auth":"none"|"required"} — no auth, ever.