ai-memory HTTP API Reference
Complete reference for every endpoint the ai-memory serve daemon
exposes. All endpoints are prefixed with /api/v1/ unless noted.
Base URL
Default: http://127.0.0.1:9077.
Configure via ai-memory serve --host <host> --port <port>. Production
deployments should always bind TLS: --tls-cert + --tls-key.
Authentication
API key
When an api_key is configured (the top-level api_key = "…" field in
config.toml, or injected via AI_MEMORY_API_KEY by the Plan-C
container entrypoint — there is no --api-key CLI flag on serve),
every endpoint except /api/v1/health requires one of:
- Header:
x-api-key: <key>— the canonical credential channel. - Query parameter:
?api_key=<key>— DEPRECATED (#1574): URL-embedded credentials leak into access logs,Refererheaders, and proxy logs. Still accepted at v0.7.0 for back-compat (the daemon emits a once-per-process WARN on first use); slated for rejection at v0.8.
Failure → 401 {"error": "missing or invalid API key"}.
When mTLS fingerprint pinning is enforced (--mtls-allowlist), the
/api/v1/sync/* federation paths bypass the api-key check — the mTLS
handshake plus the X-Memory-Sig signed-message gate (#791/#1031) are
the stronger authentication step there.
AI_MEMORY_REQUIRE_API_KEY=1 hard-refuses a keyless daemon start on
ANY bind host, including loopback (#1458).
Agent identity — X-Agent-Id
Optional on writes. Identifies the caller for governance + attribution.
X-Agent-Id: ai:claude-opus-4.7@host.local
X-Agent-Id: alice
X-Agent-Id: host:prod-web-01:pid-12345-a1b2c3d4
Resolution (v0.7.0, header-first — the pre-v0.7.0 body-wins precedence was the #874-class spoof vector):
- The
X-Agent-Idheader is the authoritative identity slot. - A body
agent_idfield (or?agent_id=query param where accepted) is a refinement that MUST match the header-resolved id; a mismatch is rejected with 403 (agent_id_body_header_mismatch/agent_id_query_header_mismatch). - With no header and no body claim, a per-request anonymous id
(
anonymous:req-<uuid8>) is synthesized and logged at WARN.
Validation pattern: ^[A-Za-z0-9_\-:@./]{1,128}$.
Admin-gated endpoints (v0.7.0 #943/#945/#946 cluster)
Corpus-scale endpoints require an admin caller: GET /stats,
POST /gc, GET /export, POST /import, GET /agents,
POST /forget, GET /namespaces (list form), GET /taxonomy,
GET /archive, GET /archive/stats, the seven /skill/* routes, and
PUT /agents/{id}/pubkey (#1539 — bind an agent’s Ed25519 attestation
public key: body {"pubkey_b64": "<base64 32-byte key>"}, response
{"bound": true, "agent_id": "..."}; the pubkey is validated as a real
curve point and the agent must already be registered. Gives attesting
clients under AI_MEMORY_REQUIRE_AGENT_ATTESTATION=1 a first-party
enrollment surface instead of an out-of-band DB write).
The admin allowlist is [admin] agent_ids = [...] in config.toml
(plus AI_MEMORY_ADMIN_AGENT_IDS); when empty (the default) these
endpoints return 403 to every caller. Per
#1570, on a
deployment with no api_key configured, a bare self-asserted
X-Agent-Id naming an admin id is NOT trusted for admin-role
resolution (boot emits a WARN); set AI_MEMORY_ADMIN_HEADER_TRUST=1
only to restore the legacy header-trust posture on isolated/mTLS-fronted
deployments.
mTLS (Layer 2 peer mesh)
When --mtls-allowlist is set, every TCP connection must present a
client certificate whose SHA-256 fingerprint appears (hex, optional
: separators, # comments) on the allowlist file. Peers without a
listed cert cannot even open the TCP connection.
See docs/ADMIN_GUIDE.md § “Peer-mesh security” for setup.
Response envelopes
Compression (v0.7.0, #1579 B4)
The daemon gzip-compresses responses when the request advertises
Accept-Encoding: gzip (standard tower-http CompressionLayer;
measured ~4.6× smaller recall payloads). Requests without the header
receive identity-coded responses, byte-identical to earlier releases.
The SSE surface (GET /api/v1/approvals/stream) is never compressed —
text/event-stream is exempted so events flush immediately.
Success (2xx)
JSON body, shape depends on endpoint. Common patterns:
{ "memory": { … } }
{ "memories": [ … ], "count": 5 }
{ "id": "abc123" }
{ "ok": true }
Error (4xx, 5xx)
Uniform envelope:
{ "error": "descriptive message" }
Status codes you’ll commonly encounter:
| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 202 | Accepted — governance pending |
| 400 | Bad request — validation, parse, or limit error |
| 401 | Unauthorized — missing / invalid API key |
| 403 | Forbidden — governance denied |
| 404 | Not found |
| 409 | Conflict — duplicate (title, namespace) |
| 500 | Internal server error |
| 503 | Service unavailable |
Limits
- Bulk / list / sync page-size cap: 1000 items by default
(
/memories/bulk,/sync/push, and the per-page cap on list responses). Operator-tunable (#1156 follow-up) via the[limits].max_page_sizeconfig field or theAI_MEMORY_MAX_PAGE_SIZEenv var (precedence: env > config > compiled default1000). The cap bounds per-request in-memory materialization to guard against OOM under an unboundedlimit=./importis capped at the compiledMAX_BULK_SIZE(1000), independent ofmax_page_size. - Request body size: 2 MiB (
HTTP_BODY_LIMIT_BYTESinsrc/lib.rs). - Recall: capped at 50 per request.
- Sync/since: capped at 10,000 per request.
- Per-agent write quotas (daily memories, storage bytes, daily links)
are operator-tunable via
[limits].max_memories_per_day/max_storage_bytes/max_links_per_day(or the matchingAI_MEMORY_MAX_*env vars). Defaults: 1000 memories/day, 100 MiB, 5000 links/day. SeeCONFIG_SCHEMA.md. - No per-client rate limiting at the HTTP layer — all writes contend
for a single
Mutex<Connection>. Batch or throttle at the caller.
Admission control — overload shedding (v0.8.0, #1733 Pillar-4 4.A)
A global in-flight concurrency cap is opt-in via
[limits].max_inflight_requests / AI_MEMORY_MAX_INFLIGHT_REQUESTS
(precedence: env > config > compiled default 0 = disabled). When set
to a positive n, the daemon admits at most n concurrent in-flight
requests and sheds the rest at the outermost layer (before timeout,
body decode, or handler work) with a typed 503:
{ "error": "server_overloaded", "code": "OVERLOADED", "max_inflight": 64 }
The shed response carries a Retry-After: 1 header. GET /api/v1/health,
GET /api/v1/metrics, and the bare GET /metrics are EXEMPT from the
cap so liveness/readiness probes and Prometheus scrapes survive an
overload (otherwise the orchestrator’s health probe would be shed, the
node killed, and graceful shedding would become a crash-loop). Shed
events increment the ai_memory_admission_shed_total Prometheus counter
and emit a sampled WARN. When the knob is unset / 0 / non-positive, no
admission layer is composed and behaviour is byte-identical to a build
without admission control.
The Memory object
{
"id": "uuid-v4",
"tier": "mid",
"namespace": "global",
"title": "Memory title",
"content": "Memory body",
"tags": ["tag1", "tag2"],
"priority": 5,
"confidence": 0.95,
"source": "api",
"access_count": 3,
"created_at": "2026-04-19T10:30:00Z",
"updated_at": "2026-04-19T10:30:00Z",
"last_accessed_at": "2026-04-19T12:00:00Z",
"expires_at": "2026-04-26T10:30:00Z",
"metadata": {
"agent_id": "ai:claude-opus-4.7",
"scope": "private",
"custom_field": "value"
}
}
tier is one of "short" | "mid" | "long" (see Tier enum in
src/models/memory.rs). last_accessed_at and expires_at are omitted
from the JSON when not set — they are NOT serialized as null.
Fields marked in metadata are preserved across update / upsert /
sync / consolidate.
Health + metrics
GET /api/v1/health
No authentication required. Returns daemon liveness.
Response
{ "status": "ok", "service": "ai-memory" }
curl http://127.0.0.1:9077/api/v1/health
GET /metrics and GET /api/v1/metrics
Prometheus text exposition format. Scrape from Prometheus, alertmanager, or Grafana Agent.
curl http://127.0.0.1:9077/metrics
GET /api/v1/stats
Structured database stats (counts by tier/namespace, links, size, last GC). Admin-gated (#946 cluster).
{
"total": 150,
"by_tier": [{"tier":"short","count":20},{"tier":"mid","count":100},{"tier":"long","count":30}],
"by_namespace": [{"namespace":"global","count":90}],
"expiring_soon": 5,
"links_count": 23,
"db_size_bytes": 524288
}
Memory CRUD
POST /api/v1/memories — create
{
"title": "Quick note",
"content": "Content",
"tier": "mid",
"namespace": "global",
"tags": ["urgent"],
"priority": 7,
"confidence": 0.9,
"source": "api",
"ttl_secs": 604800,
"expires_at": "2026-05-08T10:30:00Z",
"metadata": {"custom": "data"},
"agent_id": "alice",
"scope": "private",
"signature": "base64-std-detached-ed25519-sig",
"created_at": "2026-05-08T10:30:00Z"
}
ttl_secs is HTTP-only — the MCP memory_store tool exposes
expires_at instead (also accepted on this HTTP endpoint). See the
HTTP ↔ MCP parameter coverage table at the bottom of this document.
An optional kind field is also accepted. Omitting it keeps the
observation default; a supplied value MUST be one of the canonical
variants (observation, reflection, persona, concept, entity,
claim, relation, event, conversation, decision) or the request
is rejected with 400 (#1467 — this endpoint previously coerced an
unknown kind to observation; it now rejects to match the CLI and MCP
surfaces). See docs/memory-kind-vocab.md.
Agent attestation (signature + created_at) — #626 Layer-3
A caller MAY present a detached Ed25519 signature to upgrade the write
from a claimed agent_id to a cryptographically attested one.
The signature is computed over the canonical SignableWrite envelope
(agent_id + namespace + title + kind + created_at +
sha256(content)) and encoded as standard base64. When signature
is present, created_at (RFC 3339) is required — it is the exact
timestamp that was signed.
- The daemon verifies the signature against the agent’s bound public key
(registered via
memory_agent_register+ bind-key). On success it stampsmetadata.attest_level = "agent_attested"and adopts the signedcreated_atverbatim. - A
signaturewhosecreated_atis outside a ±300 s freshness window is rejected. - An unsigned write lands
metadata.attest_level = "claimed"under the default permissive posture. When the operator setsAI_MEMORY_REQUIRE_AGENT_ATTESTATION(truthy), an unsigned write is rejected instead. This flag governs only the unsigned-write disposition; a presented signature is always verified regardless.
This wire is identical across the three store surfaces (MCP
memory_store, this HTTP endpoint, and the CLI --sign path).
- 201 Created with
{ "id": "...", "tier": "mid", "namespace": "...", "title": "...", "agent_id": "..." }. - 202 Accepted (governance pending) with
{ "status": "pending", "pending_id": "...", "action": "store" }. - 400 when
signatureis present butcreated_atis missing or not RFC 3339. - 403 with
{ "code": "ATTESTATION_FAILED" }when a signature fails verification, or when an unsigned write is rejected underAI_MEMORY_REQUIRE_AGENT_ATTESTATION. - 400 / 403 / 500 per validation / governance / server error.
curl -X POST http://127.0.0.1:9077/api/v1/memories \
-H "X-API-Key: KEY" -H "X-Agent-Id: alice" \
-H "Content-Type: application/json" \
-d '{"title":"Meeting notes","content":"Q2 roadmap","tier":"mid"}'
GET /api/v1/memories — list
Query params: namespace, tier, limit (default 20, capped at
max_page_size — compiled default 1000), offset, min_priority,
since, until, tags (comma list), agent_id.
{ "memories": [ … ], "count": 1 }
GET /api/v1/memories/{id} — get
UUID or unique prefix. Returns memory + its links.
{
"memory": { … },
"links": [{"source_id":"…","target_id":"…","relation":"related_to","created_at":"…"}]
}
PUT /api/v1/memories/{id} — update
All fields optional. Tier never downgrades.
{ "title": "New", "priority": 8, "tier": "long" }
- 200 on success, 409 on
(title, namespace)collision, 404 on missing.
DELETE /api/v1/memories/{id} — delete
Archives before delete when archive_on_gc=true.
- 200 OK
{"deleted": true}or 202 when governance is pending.
POST /api/v1/memories/bulk — batch create
Body is a JSON array of CreateMemory objects, ≤ max_page_size
items (compiled default 1000; operator-tunable via
[limits].max_page_size / AI_MEMORY_MAX_PAGE_SIZE). Exceeding the cap
returns 400 Bad Request with an error echoing the configured cap.
{ "created": 998, "errors": ["item 17: title is required", … ] }
Recall + search
GET /api/v1/recall and POST /api/v1/recall
Hybrid recall (FTS5 + semantic + blend). Mutates the database (touches, auto-promotes).
Query / body fields: context (required), namespace, limit
(default 10, max 50), tags, since, until, as_agent,
budget_tokens, format (json default | toon | toon_compact —
v0.7.0 #1579 B4; the TOON variants return text/plain rendered by the
same encoder the MCP tools use, toon_compact ≈ 79% smaller than the
JSON envelope; an unrecognised value is a 400).
{
"memories": [ { …, "score": 0.87 } ],
"count": 5,
"tokens_used": 234,
"budget_tokens": 3000
}
curl -X POST http://127.0.0.1:9077/api/v1/recall \
-H "Content-Type: application/json" \
-d '{"context":"quarterly planning","limit":10}'
v0.8.0 §2.5 read-time attested-provenance decoration (#1709). Under verbose provenance (the MCP default; HTTP opts in), each recall row carries read-time-composed fields in addition to score — all decoration-only (the stored confidence ranking contribution is unchanged):
provenance_tier— composed from the row’sconfidence_source+ the strongest incident link attestation:signed_peer>curator_derived>self_signed>unsigned_caller.confidence_tier,freshness_state,latest_link_attest_level— the existing v0.7.0 Gap-7 decoration.-
scheduled_validity(validexpiringexpired) — present only whenAI_MEMORY_CONFIDENCE_DECAYis enabled and the row has a validity anchor (expires_at, elsecreated_at+ tier TTL); recomputed deterministically from the anchor against an hour-quantized “as-of” bucket (no exp-decay write).
When a confidence_tier filter is requested, the response envelope adds a meta object reporting what the filter dropped, so count: 0 is distinguishable from “no memory”:
{ "memories": [ … ], "count": 0,
"meta": { "confidence_filtered_out": 4, "had_filtered_candidates": true } }
These fields are uniform across MCP memory_recall, HTTP recall, and memory_session_start.
GET /api/v1/search
Read-only FTS5 keyword search. Same filter params as list, plus q
(required) and format (json default | toon | toon_compact —
v0.7.0 #1579 B4, same semantics as recall above).
{ "results": [ … ], "count": 3, "query": "urgent deadline" }
Note (HTTP ↔ MCP parity): The MCP
memory_recall,memory_search, andmemory_listtools accept the same optionalformatparameter (json|toon|toon_compact). As of v0.7.0 (#1579 B4) the HTTP recall + search endpoints exposeformattoo (HTTP defaults tojsonfor backwards compat; MCP defaults totoon_compact). HTTPmemory_listdoes not yet acceptformat. The MCPmemory_recalltool requires thecontextparameter — thequery/qalias ladder is HTTP-only (#1606); an MCP call passingqueryis refused with “context is required”. MCP additionally accepts acontext_tokensarray (v0.6.0.0 contextual recall — recent conversation tokens biasing the query embedding at 70/30) that the HTTP body does not surface.
Lifecycle
POST /api/v1/memories/{id}/promote
Bump to long tier. 200 / 202 / 404.
POST /api/v1/forget
{ "namespace": "scratch", "pattern": "deprecated", "tier": "short" }
At least one filter required. Admin-gated. Returns {"deleted": N}.
POST /api/v1/consolidate
{
"ids": ["id1","id2","id3"],
"title": "Summary",
"summary": "Merged content",
"namespace": "global",
"tier": "long"
}
201 with {"id":"consolidated-uuid","consolidated":3}.
POST /api/v1/gc
Immediate garbage collection. Empty body. Admin-gated. Returns
{"expired_deleted":N}.
Links
POST /api/v1/links
{ "source_id": "abc", "target_id": "def", "relation": "supersedes" }
Relations (nine at v0.8.0; was six at v0.7.0, four at v0.6.x): related_to, supersedes, contradicts, derived_from, reflects_on (recursive-learning Task 1/8), derives_from (WT-1-A atomisation — atom row → parent memory), decomposes_into, depends_on, advances (the last three are the v0.8.0 Pillar-2 typed-cognition Goal/Plan/Step relations, #1709). Canonical enum in src/models/link.rs::MemoryLinkRelation (COUNT = 9).
GET /api/v1/links/{id}
Returns inbound + outbound links for a memory under {"links": [...]}. Each row in the array surfaces the full link envelope including the v0.7 temporal-validity columns (valid_from, valid_until, observed_by) and the attestation columns (signature, attest_level, signed_at) — wired through db::get_links per issue #860.
Knowledge Graph + taxonomy (v0.6.3)
These endpoints operate on the temporal-validity knowledge graph
(memory_links with valid_from / valid_until / observed_by
columns added in schema v15) and the namespace taxonomy. See
docs/MIGRATION-v0.6.2-to-v0.6.3.md for the schema changes and
docs/USER_GUIDE.md for the matching MCP tools.
GET /api/v1/taxonomy
Walk live (non-expired) memories grouped by namespace into a hierarchical tree. Admin-gated (#945).
Query params: prefix (optional, restricts walk; root is an accepted
alias — prefix wins when both are supplied), depth (max 8 =
MAX_NAMESPACE_DEPTH, default 8), limit (1-10000, default 1000).
{
"tree": [
{ "namespace": "alphaone", "count": 0, "subtree_count": 47, "children": [...] }
],
"total_count": 47,
"truncated": false
}
POST /api/v1/check_duplicate
Embedding cosine-similarity duplicate detection.
{
"title": "Project uses PostgreSQL 15",
"content": "The main database is PostgreSQL 15 with pgvector for embeddings.",
"namespace": "my-app",
"threshold": 0.85
}
Response:
{
"is_duplicate": true,
"threshold": 0.85,
"nearest": { "id": "...", "title": "...", "namespace": "...", "similarity": 0.92 },
"suggested_merge": "...",
"candidates_scanned": 412
}
threshold is clamped to a 0.5 floor. Requires the semantic feature
tier or higher — without an embedder the endpoint returns 503
(Service Unavailable); threshold mismatches return 200 with
is_duplicate: false.
POST /api/v1/entities
Register an entity-as-typed-memory. Idempotent on
(canonical_name, namespace).
{
"canonical_name": "PostgreSQL",
"namespace": "my-app",
"aliases": ["pg", "postgres"],
"metadata": {}
}
Response: {"entity_id":"ent-...","canonical_name":"PostgreSQL","namespace":"my-app","aliases":["pg","postgres","PostgreSQL"],"created":true}.
Returns 409 if a non-entity memory with the same
(title, namespace) exists.
GET /api/v1/entities/by_alias
Resolve an alias to its canonical entity.
Query params: alias (required), namespace (optional; without it,
picks the most-recently-created match across namespaces).
{
"found": true,
"entity_id": "ent-...",
"canonical_name": "PostgreSQL",
"namespace": "my-app",
"aliases": ["pg", "postgres", "PostgreSQL"]
}
found: false (and null fields) when the alias resolves to nothing.
GET /api/v1/kg/timeline
Ordered timeline of links anchored at a source. Skips links with NULL
valid_from.
Query params: source_id (required), since / until (RFC 3339,
optional), limit (1-1000, default 200).
{
"source_id": "...",
"events": [
{ "target_id": "...", "relation": "depends_on", "valid_from": "...", "valid_until": null, "observed_by": "..." }
],
"count": 1
}
POST /api/v1/kg/invalidate
Mark a link superseded by setting valid_until. Does NOT delete
the link — historical queries pinned to valid_at < now still see
it. Idempotent.
{
"source_id": "...",
"target_id": "...",
"relation": "depends_on",
"valid_until": "2026-04-26T03:00:00Z"
}
Response: {"found":true,"valid_until":"...","previous_valid_until":null}.
Federation: invalidations apply locally and propagate asynchronously via the sync-daemon — they are NOT quorum-broadcast.
POST /api/v1/kg/query
Recursive-CTE traversal of the temporal knowledge graph rooted at a source memory.
{
"source_id": "...",
"max_depth": 3,
"valid_at": "2026-04-26T00:00:00Z",
"allowed_agents": ["ai:claude-code@host:pid-12345"],
"limit": 200
}
Constraints: max_depth defaults to 1 when omitted and must be in
1..=5 (KG_QUERY_MAX_SUPPORTED_DEPTH; depth 0 errors,
depth > 5 errors). allowed_agents: [] (empty array) returns zero
rows; omit the field to skip the agent filter entirely.
Response:
{
"source_id": "...",
"max_depth": 3,
"memories": [
{
"target_id": "...",
"title": "...",
"target_namespace": "my-app",
"relation": "depends_on",
"valid_from": "...",
"valid_until": null,
"observed_by": "...",
"depth": 1,
"path": "src->tgt"
}
],
"paths": ["src->tgt->..."],
"count": 1
}
Ordering: depth ASC, COALESCE(valid_from, link_created_at) ASC,
link_created_at ASC.
Namespaces
GET /api/v1/namespaces
Lists namespaces with live-memory counts (admin-gated, #945). With
?namespace=<ns> the same route instead fetches that namespace’s
standard (query-string twin of the {ns}/standard path form below).
{ "namespaces": [{"namespace":"global","count":50},{"namespace":"project-x","count":30}] }
GET /api/v1/namespaces/{ns}/standard — get namespace standard
Query: inherit (boolean, default false). When true, returns the
full N-level resolved chain (global * → ancestors → namespace) instead
of the single namespace’s standard.
{ "namespace": "engineering/auth", "standards": [ … ], "chain": ["*","engineering","engineering/auth"], "count": 3 }
Returns 200 with count: 0 and an empty standards array when no
standard is set. Equivalent MCP tool: memory_namespace_get_standard
(src/mcp/tools/namespace.rs).
POST /api/v1/namespaces/{ns}/standard — set namespace standard
Body: { "id": "<memory-id>", "parent": "<optional-parent-namespace>", "governance": { … } }.
governance accepts write / promote / delete (each any |
registered | owner | approve), approver (ApproverType), and
inherit (boolean, default true). Equivalent MCP tool:
memory_namespace_set_standard (src/mcp/tools/namespace.rs).
DELETE /api/v1/namespaces/{ns}/standard — clear namespace standard
Removes the namespace’s pinned standard (the standard memory itself is
not deleted; only the namespace_meta.standard_id link). Equivalent
MCP tool: memory_namespace_clear_standard (src/mcp/tools/namespace.rs).
Archive
GET /api/v1/archive — list archived memories
Admin-gated (#943). Query: namespace, limit (default 50,
clamped 1-1000; limit=0 → 400), offset.
{ "archived": [ … ], "count": 24 }
Equivalent MCP tool: memory_archive_list (src/mcp/tools/archive.rs).
A POST /api/v1/archive form also exists (archive an explicit list of
memory ids; ≤ 1000 ids per request).
POST /api/v1/archive/{id}/restore — restore archived memory
Path param: id (archived memory id). On success the row is removed
from archived_memories and re-inserted into memories with
original_tier and original_expires_at re-applied where present.
Equivalent MCP tool: memory_archive_restore (src/mcp/tools/archive.rs).
DELETE /api/v1/archive?older_than_days=30 — purge archived memories
Query: older_than_days (optional). Without the query param, all
archived rows are eligible. Returns {"purged": N}. Equivalent MCP
tool: memory_archive_purge (src/mcp/tools/archive.rs).
GET /api/v1/archive/stats — archive counters
Admin-gated (#943).
{ "archived_total": 24, "by_namespace": [{"namespace":"global","count":18}, … ] }
Equivalent MCP tool: memory_archive_stats (src/mcp/tools/archive.rs).
Agents + governance
POST /api/v1/agents
{ "agent_id": "alice", "agent_type": "human", "capabilities": ["read","write"] }
agent_type accepts human, system, or any ai:<name> form
(ai:claude-opus-4.7, ai:gpt-5, etc.).
GET /api/v1/agents
Admin-gated (#946). Returns {"agents":[…],"count":N}.
GET /api/v1/pending — list pending governance actions
Query: status=pending|approved|rejected, limit (default 100, max 1000).
{ "pending": [ { "id": "…", "action_type": "store", "namespace": "…", "status": "pending", "approvals": [ … ] } ], "count": 3 }
Equivalent MCP tool: memory_pending_list (src/mcp/tools/pending.rs).
POST /api/v1/pending/{id}/approve — approve pending action
Path param: id. Stamps decided_by with the caller’s X-Agent-Id.
200 if consensus reached (and the governed action is executed). 202 if
still collecting approvers. Equivalent MCP tool: memory_pending_approve
(src/mcp/tools/pending.rs).
POST /api/v1/pending/{id}/reject — reject pending action
Path param: id. Returns {"rejected":true,"id":"…","decided_by":"alice"}.
Equivalent MCP tool: memory_pending_reject (src/mcp/tools/pending.rs).
Sync / federation
POST /api/v1/sync/push
Peer-to-peer push with timestamp-aware merge.
{
"sender_agent_id": "peer-remote-1",
"memories": [ { … up to max_page_size (default 1000) … } ],
"dry_run": false
}
Response includes applied, noop, skipped, receiver_agent_id,
receiver_clock.
Federation headers (v0.7.0 secure defaults). Under
AI_MEMORY_FED_REQUIRE_SIG=1 (default, #791) the request must carry an
Ed25519 X-Memory-Sig over the body, attributed via X-Peer-Id to an
enrolled peer key; under AI_MEMORY_FED_REQUIRE_NONCE=1 (default,
#922) a fresh per-message X-Memory-Nonce is also required (the
signature binds body || 0x00 || nonce), so byte-replays produce
401 x_memory_nonce_replay. GET /api/v1/sync/since enforces the same
signed-message gate over canonical GET bytes
(method || path || query, #1031).
GET /api/v1/sync/since
Query: since (RFC3339, optional), limit (default 500, max 10000),
peer (attribution tag).
{ "count": 5, "limit": 500, "memories": [ … ] }
Import / export
GET /api/v1/export
Admin-gated. Returns
{"memories":[…],"links":[…],"count":N,"exported_at":"…"}.
POST /api/v1/import
Admin-gated. Body matches export shape. ≤ 1000 memories per call
(compiled MAX_BULK_SIZE). Returns {"imported":N,"errors":[…]}.
Preserves original metadata.agent_id into
metadata.imported_from_agent_id.
Webhooks (v0.6.0.0)
Three endpoints under /api/v1/subscriptions — create them via MCP
tools or the REST surface. Dispatch is SSRF-hardened (rejects
private-range IPs; requires https:// unless loopback).
POST /api/v1/subscriptions — register webhook
Body: { "url": "https://…", "events": "memory_store,memory_delete", "secret": "<shared-secret>", "namespace_filter": "…", "agent_filter": "…" }.
events is a comma-separated string (default "*"). Canonical
event types (WEBHOOK_EVENT_TYPES in src/subscriptions.rs):
memory_store, memory_promote, memory_delete,
memory_link_created, memory_link_invalidated,
memory_consolidated, approval_requested. Stores secret as a
SHA-256 hash; dispatched events carry an
X-AI-Memory-Signature: sha256=<hex> HMAC header. Returns the new
subscription id. Equivalent MCP tool: memory_subscribe
(src/mcp/tools/subscribe.rs).
DELETE /api/v1/subscriptions?id=<id> — unregister webhook
Returns {"deleted": true}. Equivalent MCP tool: memory_unsubscribe
(src/mcp/tools/subscribe.rs).
GET /api/v1/subscriptions — list subscriptions
Returns {"subscriptions":[…],"count":N}. Each entry includes url,
events, created_at, dispatch_count, failure_count. Equivalent
MCP tool: memory_list_subscriptions (src/mcp/tools/subscribe.rs).
Federation (v0.7, opt-in via --quorum-writes)
When ai-memory serve --quorum-writes N --quorum-peers URL,URL,… is
set, every write fans out to peers and returns only once W-1 peer
acks land within --quorum-timeout-ms.
- 201 +
{"quorum_acks": W}when quorum is met. - 202 Accepted +
{"quorum_met":false,"acks":X,"needed":Y,"reason":"unreachable|timeout|id_drift","durability":"local"}when the local write committed but quorum was not met (v0.8.1 W3 / gap G12).
The local write is durably committed and never rolled back, so an
under-replicated write is reported as a 202 Accepted, not a 5xx
(the pre-v0.8.1 503 + Retry-After: 2 misreported a locally-durable
write as a service failure). The replication state is carried in the
body; the sync-daemon’s eventual-consistency loop + the federation
push-DLQ converge peers afterwards (per ADR-0001), so there is no
client retry to perform. A genuine local write failure still
returns the appropriate error status.
Curl recipes
# Health
curl http://127.0.0.1:9077/api/v1/health
# Store a memory
curl -X POST -H "Content-Type: application/json" \
http://127.0.0.1:9077/api/v1/memories \
-d '{"title":"hi","content":"there","tier":"mid"}'
# Recall
curl -X POST -H "Content-Type: application/json" \
http://127.0.0.1:9077/api/v1/recall \
-d '{"context":"what did I store","limit":5}'
# Incremental sync pull since a timestamp
curl 'http://127.0.0.1:9077/api/v1/sync/since?since=2026-04-01T00:00:00Z&limit=1000'
# Prometheus scrape
curl http://127.0.0.1:9077/metrics
HTTP ↔ MCP parameter coverage
A small set of parameters are surfaced by only one transport. The MCP
tool schema is authoritative via the per-tool <ToolName>Request
structs in src/mcp/tools/<name>.rs (schemars-derived; consumed by
registered_tools() in src/mcp/registry.rs and projected to
tools/list by tool_definitions()). The HTTP body / query types in
src/models/ and the route handlers in src/handlers/ are
authoritative for HTTP.
| Tool | Param | HTTP | MCP | Notes |
|---|---|---|---|---|
memory_store |
ttl_secs |
✓ | ✗ | HTTP-only; the MCP tool exposes expires_at (also accepted by HTTP). |
memory_store |
expires_at |
✓ | (via update) |
HTTP body accepts; documented in the POST /api/v1/memories example. |
memory_store |
signature |
✓ | ✓ | #626 Layer-3 — std-base64 detached Ed25519 over the SignableWrite envelope; upgrades agent_id claimed→agent_attested. Same wire on both transports. |
memory_store |
created_at |
✓ | ✓ | #626 Layer-3 — RFC 3339; required when signature is present (the signed timestamp, adopted verbatim; ±300 s freshness window). |
memory_recall |
format |
✗ | ✓ | MCP-only; HTTP responses are always JSON. |
memory_recall |
context_tokens |
✗ | ✓ | MCP-only (v0.6.0.0 contextual recall). |
memory_search |
format |
✗ | ✓ | MCP-only. |
memory_list |
format |
✗ | ✓ | MCP-only. |
These gaps are intentional for v0.6.3.1 and tracked for parity follow-up — they are NOT drift in the doc surface, just transport-level surface-area differences captured here so operators don’t re-derive them.
v0.7.0 net-new endpoints
The HTTP routes added since v0.6.4. All accept the same auth +
agent-identity headers documented above. Wire-shape source of truth is
the per-domain handler modules under src/handlers/; the route-path
SSOT is src/handlers/routes.rs (#1558 batch 4), registered by the
router in src/lib.rs.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/quota/status |
K8 quota status — read the calling agent’s daily quota row. Auto-inserts a default row on first call. See docs/k8-quotas.md. MCP: memory_quota_status. |
GET |
/api/v1/approvals/stream |
K10 SSE approval channel — server-sent events for pending-approval state changes. See docs/k10-sse-approvals.md. |
POST |
/api/v1/approvals/{pending_id} |
K10 approval decide path — body {"decision":"approve|deny","remember":"once|session|forever"}, HMAC-gated via X-AI-Memory-Signature. |
POST |
/api/v1/auto_tag |
LLM auto-tag endpoint (v0.7 smart-tier surface; 503 when no LLM is configured). |
POST |
/api/v1/expand_query |
HTTP parity for the MCP memory_expand_query tool. |
POST |
/api/v1/kg/find_paths |
KG chain-walk over HTTP; Cypher on AGE / recursive-CTE on SQLite. |
POST |
/api/v1/find_paths |
Alias for /api/v1/kg/find_paths (#934 — legacy callers). |
POST |
/api/v1/links/verify |
Ed25519 link verification surface — wire shape: {verified, attest_level, signature_present, observed_by, source_id, target_id, relation, findings}. |
DELETE |
/api/v1/links |
Delete a link. Returns {"deleted": N}. |
GET |
/api/v1/contradictions |
Detect contradiction candidates (similar titles in a namespace). |
POST |
/api/v1/memory_load_family |
HTTP parity for the always-on memory_load_family MCP loader. |
POST |
/api/v1/capture_turn |
#1416 — L4 layered-capture HTTP mirror of MCP memory_capture_turn (idempotent per-turn write via MemoryStore::capture_turn_idempotent). |
POST |
/api/v1/share |
#1095 — copy a memory into the recipient agent’s _shared/<from>→<to>/ namespace; body {source_memory_id, target_agent_id}. MCP: memory_share. |
POST |
/api/v1/session/start |
HTTP parity for memory_session_start (auto-recall session boot). |
GET |
/api/v1/capabilities |
Capabilities envelope (schema_version "3"; Accept-Capabilities header negotiates v1/v2). MCP: memory_capabilities. |
POST |
/api/v1/notify |
Agent-to-agent inbox message. Sender resolved from X-Agent-Id only (#901); body agent_id must match or 403. MCP: memory_notify. |
GET |
/api/v1/inbox |
Read the calling agent’s inbox. MCP: memory_inbox. |
GET POST DELETE |
/api/v1/skill/list, /api/v1/skill/register, /api/v1/skill/{id}, /api/v1/skill/{id}/resource, /api/v1/skill/{id}/export, /api/v1/skill/{id}/promote, /api/v1/skill/{id}/compose |
Cluster E API-2 (#767) — Agent Skills HTTP parity for the seven memory_skill_* MCP tools. Admin-gated. |
POST |
/api/v1/memory_smart_load, /api/v1/memory_reflect, /api/v1/memory_recall_observations, /api/v1/memory_reflection_origin, /api/v1/memory_dependents_of_invalidated, /api/v1/memory_export_reflection, /api/v1/memory_atomise, /api/v1/memory_calibrate_confidence, /api/v1/memory_verify, /api/v1/memory_replay, /api/v1/memory_subscription_replay, /api/v1/memory_subscription_dlq_list, /api/v1/memory_rule_list, /api/v1/memory_check_agent_action |
#1111 — 14 thin HTTP wrappers around the same-named MCP substrate handlers (src/handlers/route_1111.rs); wire envelopes are byte-equal across MCP and HTTP. |
GET |
/api/v1/tools/list |
MCP tools/list mirror for harness ops — returns the live tool surface for the daemon’s profile (101 at full, 7 at core) — SSOT: Profile::full()/core().expected_tool_count() in src/profile.rs. |
Total HTTP surface: 78 unique URL paths / 92 production route registrations on the sqlite-backed daemon (and the postgres-backed daemon under
--features sal-postgres). Authoritative count:grep -oE '"/[^"]*"' src/handlers/routes.rs | sort -u | wc -l= 78 (77/api/v1/*paths + the bare/metrics), pinned byEXPECTED_PRODUCTION_UNIQUE_PATHS_COUNTinsrc/lib.rs. The two v0.8.0 net-new paths were the #1718 coordination write surfaces below; the v0.9.0 #1859 G13-mem lineage read surface (GET /api/v1/memories/{id}/lineage) added the 78th unique path.
v0.8.0 net-new endpoints
The #1718 Pillar-1 distributed-coordination write surfaces. Only these two coordination paths are exposed over HTTP; the rest of the coordination toolset (action CRUD, leases, checkpoints, routines) is MCP-only.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/actions/{id}/transition |
Coordination action-state transition (handlers::transition_action) — local CAS write + W-of-N federation fanout. MCP: memory_action_transition. |
POST |
/api/v1/signals |
Signed inter-agent signal send (handlers::send_signal) — local write + W-of-N fanout. MCP: memory_signal_send. |
v0.7.0 net-new MCP tools
The 31 MCP tools added since v0.6.4 are documented inline in
src/mcp/registry.rs and enumerated in
docs/MIGRATION_v0.7.md §”New MCP tools”.
Highlights for HTTP-equivalent surfaces:
| MCP tool | HTTP equivalent | Notes |
|---|---|---|
memory_load_family |
POST /api/v1/memory_load_family |
Always-on. |
memory_quota_status |
POST /api/v1/quota/status |
K8. |
memory_find_paths |
POST /api/v1/kg/find_paths |
J7. |
memory_verify |
POST /api/v1/links/verify |
H4. |
memory_pending_list / memory_pending_approve / memory_pending_reject |
GET /api/v1/pending, POST /api/v1/pending/{id}/approve, POST /api/v1/pending/{id}/reject |
K10. The MCP tool names changed from the v0.7-alpha drafts (memory_approval_pending / memory_approval_decide); the HTTP paths are stable. |
memory_agent_register / memory_agent_list |
POST /api/v1/agents, GET /api/v1/agents |
meta family. Register an NHI agent (agent_type, capabilities) in _agents (refreshes last_seen_at, preserves registered_at) and list every registered agent (ordered by registered_at). agent_id is CLAIMED, not attested — pair with attestation (#626 Layer-3) for a security boundary. |
For the canonical full inventory (101 entries advertised at --profile full — 100 callable + the always-on memory_capabilities): grep -oE 'crate::mcp::[a-z_]+::[A-Za-z]+Tool' src/mcp/registry.rs | sort -u | wc -l returns 101 — the registered_tools() iterator in src/mcp/registry.rs is the source of truth. The v0.8.0 net-new tools were the coordination families memory_action_*, memory_lease_*, memory_signal_*, memory_checkpoint_*, and memory_routine_*.
See also
docs/USER_GUIDE.md— MCP tool reference (parallel to this HTTP doc).sdk/typescript/README.md— TypeScript SDK using these endpoints.sdk/python/README.md— Python sync + async SDK.docs/CLI_REFERENCE.md— corresponding CLI surface.docs/SECURITY.md— API key + mTLS + governance.docs/TROUBLESHOOTING.md— common error scenarios.