Skip to content

REST API

HTTP/JSON is Memoturn’s primary protocol: stateless, serverless-friendly, and trivially callable from agent tools. The surface has three groups: the agent-memory API (per profile), the data-plane API (per database), and the control-plane platform API.

A machine-readable OpenAPI 3.1 spec covers every route below — use it for codegen, request validation, or exploring the API in any OpenAPI viewer. A test in the repository keeps it in lockstep with the router.

Addressing: one base URL per database (https://{db}.{region}.memoturn.dev). Against a single node — the prototype default — the same data-plane routes are addressed as /v1/db/{db}/..., where {db} may be name or name@branch. A branch can also be selected with the Memoturn-Branch header.

Every response carries a Memoturn-Txid header. Requests may carry Memoturn-Min-Txid for a read-your-writes floor and Memoturn-Consistency: primary|cached to pick a read mode. A write may carry Memoturn-Durability: durable to be acked only after it ships to object storage. See consistency.

Authentication is a bearer token: a per-database or namespace JWT for memory and data-plane routes, the platform key for control-plane routes. See security.

Per-profile routes. A profile is one database named {ns}--{profile}; isolation between profiles is structural — no operation touches two profiles. Full semantics: memories and recall.

MethodPathDescription
POST/v1/memory/{ns}/{profile}/memoriesBatch ingest (idempotent; auto-creates the profile)
POST/v1/memory/{ns}/{profile}/recallHybrid keyword + topic + vector query
POST/v1/memory/{ns}/{profile}/extractServer-side LLM distill then ingest (503 if the node has no extractor)
GET/v1/memory/{ns}/{profile}/memories/{id}One memory with its supersession chain
DELETE/v1/memory/{ns}/{profile}/memories/{id}Forget (hard delete)
GET/v1/memory/{ns}/{profile}/sessionsList sessions
DELETE/v1/memory/{ns}/{profile}/sessions/{sid}End session: delete its task memories (?turns=true drops the transcript too)
GET/v1/memory/{ns}/{profile}/policyThe profile’s governance policy: override + effective values (read scope)
PUT/v1/memory/{ns}/{profile}/policySet or clear a tighten-only profile override (admin scope; loosening is 409)
POST/v1/memory/{ns}/{profile}/erasuresVerifiable erasure: hard-forget now, history rewrite + signed receipt after the grace window (202 + coupon)
GET/v1/memory/{ns}/{profile}/erasuresList erasure coupons (newest first)
GET/v1/memory/{ns}/{profile}/erasures/{id}One coupon — completed carries the signed receipt
GET/v1/memory/{ns}List profiles in the namespace (namespace token)

A batch is atomic and returns one txid; concurrent batches may group-commit and share a txid. Memory IDs are content-addressed, so re-ingesting the same memory is a no-op reported as duplicate. Embeddings are bring-your-own by default; with node-side auto-embedding enabled, omitted embeddings are filled in outside the write path.

POST /v1/memory/acme/alice/memories
{ "memories": [
{ "type": "fact", "topic_key": "user.editor-theme", "summary": "prefers dark mode",
"content": {"preference": "dark"}, "keywords": "theme ui", "embedding": [0.1, 0.2] },
{ "type": "event", "summary": "deployed v2 to prod",
"content": {"version": "v2"}, "session_id": "s-417", "source": "claude-code" },
{ "type": "task", "summary": "follow up on refund #88", "content": {}, "ttl": 86400 }
] }
201
{ "results": [
{ "id": "mem_9f2c...", "status": "created", "superseded": ["mem_31ab..."] },
{ "id": "mem_77d0...", "status": "created", "superseded": [] },
{ "id": "mem_c4e1...", "status": "created", "superseded": [] }
],
"txid": 42 }

status is created, revived (an old superseded memory re-asserted and active again), or duplicate (already active). Ingesting a fact or instruction whose topic_key has an active memory supersedes the old row in the same transaction — history is preserved, not deleted. source records which agent wrote the memory; it is excluded from the content hash, so duplicate/revived keep the first writer’s attribution.

At least one of query (keyword channel), embedding (vector channel), or topic_key (exact topic channel) is required. Channels are merged by reciprocal-rank fusion; superseded and expired rows are dropped; each hit reports the channels that found it. An empty result is a valid answer.

POST /v1/memory/acme/alice/recall
{ "query": "what theme does the user like?", "embedding": [0.1, 0.2],
"topic_key": "user.editor-theme", "types": ["fact"], "source": "claude-code", "k": 8 }
200
{ "memories": [
{ "id": "mem_9f2c...", "type": "fact", "topic_key": "user.editor-theme",
"summary": "prefers dark mode", "content": {"preference": "dark"},
"keywords": "theme ui", "session_id": null, "source": "claude-code",
"created_at": 1765000000, "superseded_by": null, "score": 0.064,
"channels": ["topic", "keyword", "vector"] }
],
"txid": 42 }

session_id and source narrow recall to one session’s or one agent’s memories; include_superseded: true exposes superseded rows; include_turns: true (requires embedding) additionally searches the verbatim transcript and returns matching turns in a separate turns array, never mixed into the fused ranking. See sessions.

Opt-in per node (MEMOTURN_EXTRACT_API_KEY); unconfigured nodes return 503. The LLM call runs before any database write and is structured-output-constrained; proposals then flow through the ordinary idempotent ingest. dry_run: true returns proposals without writing. See extraction. A namespace whose governance policy sets ai_egress.extract: deny gets a 403 before any model is called (the same applies to /ask via ai_egress.ask).

POST /v1/memory/acme/alice/extract
{ "turns": [ {"role": "user", "content": "I'm vegan now"} ],
"session_id": "s-417", "source": "claude-code", "dry_run": false }

Per-database routes — the multi-model substrate under every profile. See data model.

MethodPathDescription
POST/v1/sqlAtomic statement batch
GET / PUT / DELETE/v1/kv/{ns}/{key}KV read / write (?ttl=) / delete; reads accept ?consistency=
GET/v1/kv/{ns}?prefix=List keys by prefix
POST/v1/docs/{collection}/findQuery documents (filter, sort, limit, skip)
POST/v1/docs/{collection}/insertInsert documents
POST/v1/docs/{collection}/updateUpdate by filter ($set/$unset/$inc/$push)
POST/v1/docs/{collection}/deleteDelete by filter
POST/v1/docs/{collection}/indexesIndex a document path (generated column + B-tree)
POST/v1/vectors/{collection}Upsert an embedding
POST/v1/vectors/{collection}/searchANN search
POST/v1/memory/{session}/turnsAppend a transcript turn
GET/v1/memory/{session}/turns?last=20Read the recent window
POST/v1/memory/{session}/searchSemantic search over turn embeddings
POST/v1/syncShip this branch’s state to object storage now
POST /v1/db/agent-42/sql
{ "stmts": [ { "q": "SELECT count(*) FROM orders WHERE status = ?", "params": ["open"] } ] }
200
{ "results": [ [ { "count(*)": 3 } ] ], "txid": 17 }

User SQL cannot reference reserved __memoturn_ tables (KV, docs, memories, transcripts), however the name is quoted. Sandbox escapes — ATTACH, VACUUM INTO, PRAGMA writable_schema — are rejected, and so is transaction control (BEGIN, COMMIT, SAVEPOINT, …; trigger bodies are fine): each statement batch is already one atomic unit, and the engine owns transaction boundaries. Benign read-only PRAGMAs (integrity_check, table_info) pass. Mutating statements need write scope; a read token gets 403.

The value is the raw request/response body. PUT /v1/kv/scratch/plan?ttl=3600 writes with a TTL; GET /v1/kv/scratch/plan?consistency=primary forces a strongly consistent owner read (default is cached — eventually consistent, txid disclosed). Prefix listing returns:

GET /v1/kv/scratch?prefix=step:
{ "keys": ["step:1", "step:2"] }
POST /v1/db/agent-42/docs/notes/find
{ "filter": { "kind": "fact", "score": { "$gt": 0.5 } },
"sort": { "score": -1 }, "limit": 10 }
200
{ "docs": [ { "_id": "01J...", "kind": "fact", "score": 0.9 } ] }

insert takes { "docs": [...] } and returns { "ids": [...] }; update takes { "filter", "update", "multi" } and returns { "modified": n }; delete returns { "deleted": n }; indexes takes { "path": "score" }.

POST /v1/db/agent-42/vectors/notes/search
{ "vector": [0.1, 0.2], "k": 8 }
200
{ "hits": [ { "id": "01J...", "distance": 0.13 } ] }

Turns: POST /v1/memory/{session}/turns with { "role": "user", "content": {...}, "embedding": [...] } returns { "seq": n }; GET .../turns?last=20 and POST .../search with { "vector", "k" } both return { "turns": [...] }.

Platform routes, authenticated with the platform key.

MethodPathDescription
POST/v1/databasesCreate a database ({name})
GET/v1/databases?cursor=List databases
DELETE/v1/databases/{db}Delete a database — write tokens minted before the deletion are revoked (403); see security
POST/v1/databases/{db}/branchesFork a branch ({name, from?, checkpoint?, ttl?}; ttl makes a burner branch)
POST/v1/databases/{db}/branches/{branch}/checkpointTag the current state ({name})
POST/v1/databases/{db}/branches/{branch}/rewindRewind to a checkpoint or txid ({to})
POST/v1/databases/{db}/tokensMint a per-database token ({scope, expires_in})
POST/v1/namespaces/{ns}/tokensMint a namespace token covering every profile under it
GET/v1/namespaces/{ns}/policyThe namespace governance policy
PUT/v1/namespaces/{ns}/policySet the namespace governance policy (retention, memory aging, audit, AI egress)
GET/v1/namespaces/{ns}/audit?from=&to=&action=&profile=&outcome=&limit=&cursor=Page the namespace audit stream — platform key or a namespace admin token
GET/v1/databases/{db}/usageUsage counters
POST /v1/databases/agent-42/tokens
{ "scope": "write", "expires_in": 3600 }
200
{ "token": "eyJ..." }

Token scopes are read (recall, get), write (ingest, forget, session end), and admin (checkpoint, rewind). Branch operations are O(1) manifest writes — see branching.

The request surface is bounded by default — every knob is per node, tunable via configuration:

  • Bodies over 32 MiB return 413; requests have a 30 s wall-clock budget and a global in-flight cap (1024).
  • Control endpoints (provision, branches, tokens) are rate-limited (10 req/s sustained); overruns return 429.
  • Memory ingest batches cap at 1,000 memories per request; recall k and result limits clamp to 1,000.
  • Document filters reject nesting deeper than 32 levels and $in/$nin arrays over 1,000 items.

The same surface is available through the CLI, the TypeScript SDK, the Python SDK, and the MCP server.