Skip to content

API Reference

Base URL: https://core.mnemoverse.com/api/v1

This is the REST base (auth via X-Api-Key) — it is not an MCP endpoint. MCP connectors for Claude / ChatGPT / editors use https://mcp.mnemoverse.com/mcp instead — see Agent Setup.

All /memory/* endpoints require authentication via X-Api-Key header or Authorization: Bearer header. Health endpoints (/health, /health/ready) are public and unauthenticated. Internal administrative endpoints exist but use separate authentication and are not part of the public API.

Authentication

Include your API key in every request:

bash
# Option 1: X-Api-Key header (recommended)
curl -H "X-Api-Key: mk_live_YOUR_KEY" ...

# Option 2: Bearer token
curl -H "Authorization: Bearer mk_live_YOUR_KEY" ...

Endpoints

POST /memory/write

Store a single memory atom.

Request:

FieldTypeRequiredDescription
contentstringYesThe insight, pattern, or lesson to remember (1-10,000 chars)
conceptsstring[]NoKey concepts for Hebbian associations (up to 256 items)
domainstringNoNamespace: general, user:X, project:Z (default: general)
metadataobjectNoArbitrary key-value metadata; serialized UTF-8 JSON must be at most 65,536 bytes
fingerprintstringNoOptional exact-match fingerprint (max 512 characters)
external_refstringNoClient-provided unique reference for idempotent writes
supersedesstring[]NoAtom IDs, in your own organization, that this write replaces (up to 32 items). See "Supersede semantics" below

Response:

FieldTypeDescription
storedbooleanTrue if the atom passed the importance gate
atom_idUUIDUUID of the stored atom, or null if filtered
importancefloatComputed importance score [0, 1]
reasonstringWhy stored or filtered
supersededUUID[]IDs of atoms this write marked as superseded. Always present; [] when supersedes was omitted or empty

Supersede semantics: pass supersedes to mark this write as the correction for one or more earlier atoms. All targets and the new atom are committed in a single transaction: if any target fails validation, the write rolls back entirely and none of the targets are touched. A target is not deleted; it gains a superseded_by pointer to the new atom and stays fetchable by ID (see GET /memory/atoms/{atom_id} below). supersedes links atoms, not domains, so the new atom can live in a different domain than what it replaces.

Errors specific to supersedes:

CaseResponse
A target ID is malformed, repeated in the list, or the list has more than 32 items400 VALIDATION_ERROR
This write's own domain is xroom:* and supersedes is non-empty400 VALIDATION_ERROR
A target does not exist in your organization404 NOT_FOUND (a foreign-org atom and a nonexistent one return the identical 404, deliberately: an anti-enumeration measure, not a bug)
A target already has a successor409 CONFLICT, naming that target's immediate successor's ID (not the head of a longer chain)
A target was claimed by a concurrent write between your read and this write409 CONFLICT: re-read that revision and supersede it instead
A target is a secret atom (Vault-owned lifecycle)409 CONFLICT

With multiple targets, validation runs in list order and stops at the first failure: the whole write rolls back, nothing partial is stored, and no target is touched.

Read-side filtering is deployed, and switched off (as of 2026-08-29)

The filter that hides a replaced revision from default reads now ships in the API, behind a server-side setting that is off in production today. While it is off, POST /memory/read, /memory/recent, /memory/query, and batch fetches return superseded revisions exactly as before — so supersedes does not yet make a stale fact stop showing up in search results. It records the correction; it does not yet hide the original.

True either way, on or off: a replaced revision carries superseded_by, the revision that replaced it carries supersedes, read items report both fields regardless of the setting, and GET /memory/atoms/{atom_id}/chain walks the chain from any revision in it, within its lookup budget and reporting any break rather than failing.

When the setting is enabled, search and feed reads return live tips only. History stays reachable through include_history on a read, by ID, by external_ref, and through the chain endpoint — the two point lookups are never filtered.

Nothing you send or parse changes when it flips: supersedes / superseded_by are reported either way and include_history is already accepted, so no client code needs rewriting. What comes back does change — that is the point of the switch — and Reading history lists the behaviours that move with it.


POST /memory/write-batch

Store up to 500 atoms in one request.

supersedes is not accepted on either batch route: an item carrying it fails the whole request with 422 VALIDATION_ERROR. Per-item transactional semantics have no honest answer in a batch — if item 3 supersedes an atom and item 5 fails, "what did the batch do" has no true and useful answer — so supersede-writes go one at a time to POST /memory/write, where each gets its own transaction.

Request:

FieldTypeRequiredDescription
itemsWriteRequest[]YesArray of write requests (1-500)

Response:

FieldTypeDescription
total_countintTotal atoms processed
stored_countintAtoms that passed importance gate
resultsobject[]Per-atom results with index, stored, atom_id, importance, reason, and error. reason explains the gate decision when available

POST /memory/write_batch

Use the hyphenated /memory/write-batch instead

Both spellings are served, but they are not the same endpoint — they are two handlers with different behaviour, and the difference is one you will only notice when something has already gone wrong.

/memory/write-batch supports the Idempotency-Key header: if the network drops after the server processed your batch, retrying with the same key returns the cached response instead of writing everything twice.

/memory/write_batch has no idempotency. A retry after a timeout writes the batch again — which is exactly the duplicate-write scenario the key exists to prevent.

The underscore form is kept only for callers already written against it.

Otherwise identical to /memory/write-batch: the same request and response shapes, the same per-item tenant resolution.


POST /memory/read

Query memory with semantic search + Hebbian expansion.

Request:

FieldTypeRequiredDescription
querystringYesNatural language query (1-5,000 chars)
top_kintNoRequested number of results (1-100, default: 10). Not a hard cap: association expansion can return more, and the relevance floor can return fewer
domainstringNoFilter by domain (null = all)
min_relevancefloatNoMinimum relevance threshold (0-1, default: 0.3)
include_associationsboolNoExpand via Hebbian associations (default: true)
include_historyboolNoReturn superseded revisions alongside live ones (default: false). Grants eligibility under the normal ranking, not chain completeness — see Reading history
conceptsstring[]NoConcept hints to bias search
author_principalstringNoRooms: filter by authoring principal (authored atoms)
author_agentstringNoRooms: filter by authoring agent
author_client_envstringNoRooms: filter by authoring client environment
author_is_externalboolNoRooms: only atoms from external (non-owner) members

Response:

FieldTypeDescription
itemsMemoryItem[]Matching memories, ordered by relevance
episodic_hitbooleanTrue if exact fingerprint match found
query_conceptsstring[]Concepts extracted from query
expanded_conceptsstring[]Concepts after Hebbian expansion
search_time_msfloatSearch duration in milliseconds

MemoryItem fields:

FieldTypeDescription
atom_idUUIDUnique identifier
contentstringStored text content
relevancefloatFinal score (similarity * valence modulation)
similarityfloatRaw cosine similarity
valencefloatOutcome polarity [-1, +1]
importancefloatImportance score [0, 1]
sourcestringHit source: episodic, semantic, or hebbian
conceptsstring[]Associated concepts
domainstringDomain namespace
metadataobjectArbitrary metadata
provenanceobjectAuthorship record — present on authored (Rooms) atoms
supersedesUUID[]Revisions this item replaced. Omitted when it replaced nothing
superseded_byUUIDThe revision that replaced this one. Omitted while this item is the live tip; present means this item is history. Reported as a fact of the data regardless of the read-side setting — while filtering is off, this field is how you tell a replaced revision from a live one

Reading history

include_history is accepted by POST /memory/read, /memory/read-batch (per query), /memory/recent and /memory/query. It asks for superseded revisions alongside live ones, and a few things are worth knowing before you rely on it:

  • Eligibility, not completeness. History competes under the same ranking, min_relevance and top_k/limit as everything else, so a read can legitimately return part of a chain and no read promises all of it. For the whole chain, use GET /memory/atoms/{atom_id}/chain.
  • History is returned where it is stored. A revision keeps its own domain, so a read scoped to the successor's domain does not surface a predecessor that lives elsewhere. Use an unscoped read, GET /memory/atoms/{atom_id}, or the chain endpoint.
  • One exception to that domain rule, and it is on the fingerprint channel. Not on query text: when a read carries the optional fingerprint field and it resolves to a revision that has since been replaced, the live tip is pulled in alongside it even if that tip lives in another domain — the fingerprint index is keyed by tenant and hash alone, so it is domain-blind. Both are seated ahead of the ranked hits, which fill the remaining top_k slots. Two caveats: this fires once read-side filtering is on and does not wait for include_history, so a caller who never asked for history can receive a replaced revision this way; and the tip is best-effort — if the chain above the matched revision is broken or runs deeper than 32 links, you get the matched revision alone, and its superseded_by still tells you it is history.
  • History is returned, not reinforced. Once filtering is on, a superseded revision that comes back through include_history is not counted as a use: its access_count and last_accessed_at do not move, and it forms no new associations. Live items in the same response are unaffected.
  • You cannot feature-detect it. The read endpoints ignore request fields they do not recognise rather than rejecting them, so sending include_history to a deployment that predates it returns 200 with the field silently dropped, never a 422. No response field reports whether the server-side filter is on, either.
  • No effect while read-side filtering is off (nothing is hidden, so nothing needs unhiding), and inert for xroom:* domains, whose atoms cannot be superseded at all. Rooms are a /memory/read and /memory/recent feature: POST /memory/read-batch and /memory/query refuse an xroom: domain with 400 VALIDATION_ERROR before any of this applies.

POST /memory/read-batch

Batch query up to 50 queries in one request. include_history is a field of each query, not of the batch: one batch can ask for history on one question and the current answer on the next.

Request:

FieldTypeRequiredDescription
queriesReadRequest[]YesArray of read requests (1-50)

Response:

FieldTypeDescription
resultsReadResponse[]Per-query results

POST /memory/recent

The queryless feed: the newest entries first, with no search. Where /memory/read answers "what do I know about X" and returns what matches, this answers "what happened lately" and is complete by construction — nothing is skipped because nothing is ranked away. One thing can be filtered rather than ranked away: once read-side filtering is enabled, superseded revisions leave the feed unless you pass include_history.

Paged by cursor rather than truncated. When more entries exist the response carries next_cursor; passing it back continues the listing with no skips and no duplicates, which LIMIT/OFFSET cannot guarantee while writes are landing.

Request:

FieldTypeRequiredDescription
domainstringNoRestrict to one domain (null = all your domains). An xroom:<room_id> address resolves through the same membership check as /memory/read
sincestringNoOnly entries created at or after this ISO-8601 instant, inclusive. Naive datetimes are read as UTC
untilstringNoOnly entries created at or before this instant, inclusive. Pair with since for a closed window
exclude_authorstringNoDrop entries written by this author principal — the "everyone but me" read in a shared room
include_historyboolNoInclude superseded revisions in the feed (default: false) — see Reading history
limitintegerNoPage size, 1–100 (default 20)
cursorstringNoContinuation cursor from a previous response's next_cursor

Response:

FieldTypeDescription
itemsRecentItem[]Newest first. Each carries atom_id, content, domain, created_at, concepts, and provenance when authorship was recorded, plus supersedes when the entry replaced something and superseded_by when something replaced it, each key omitted otherwise
next_cursorstring | nullPresent when older entries remain; null at the end of the feed

An empty feed is 200 with items: [] — never a 404.

Secrets are excluded from this feed by design. A queryless listing would make every secret's alias enumerable on each first page; they have their own surface at GET /vault/secrets.


POST /memory/query

Advanced query with multi-domain and metadata filtering.

Request:

FieldTypeRequiredDescription
querystringYesNatural language query
domainsstring[]NoFilter by multiple domains
metadata_filterFilter[]NoJSONB metadata conditions (eq, contains, in)
top_kintNoRequested number of results (default: 10). Not a hard cap: association expansion can return more, and the relevance floor can return fewer
min_relevancefloatNoMin relevance (default: 0.3)
include_associationsboolNoHebbian expansion (default: true)
include_historyboolNoReturn superseded revisions alongside live ones (default: false) — see Reading history
conceptsstring[]NoConcept hints

POST /memory/feedback

Report outcome (success/failure) for memories. Updates valence and Hebbian associations.

Request:

FieldTypeRequiredDescription
atom_idsUUID[]YesAtoms to update
outcomefloatYesOutcome signal: -1.0 (failure) to +1.0 (success)
conceptsstring[]NoConcepts to reinforce
query_conceptsstring[]NoOriginal query concepts (enables co-activation learning)
domainstringNoDomain for the feedback

Response:

FieldTypeDescription
updated_countintNumber of atoms updated
avg_valencefloatAverage valence after update
coactivation_edgesintHebbian edges created/updated

POST /memory/consolidate

Not enabled

Consolidation is off on the hosted service and we do not currently recommend turning it on. The endpoint responds 503 with a typed reason rather than consolidating. Nothing about your memories changes; the rest of the API is unaffected.

Consolidation groups similar memories under a prototype. Two things keep it off. It has never once completed on the hosted service — every run in the retained window failed, silently, because the failure was swallowed by a background path. And a prototype competes with its own sources for the same result slots, so a summary can crowd out the specific memory you were looking for. Neither of those is something to enable and find out about later, and no customer is close to needing the compression. The design we intend to build instead adds the summary as a layer over memories that stay addressable.

There is also no capacity pressure behind it. Measured on 2026-08-13: the largest tenant on the hosted service held 2,429 memories against a free-tier allowance of 10,000, and the whole store came to roughly 16,000 across 200 tenants. No tenant is near its own limit, so nothing is waiting on compression.

Request:

FieldTypeRequiredDescription
domainstringNoDomain to consolidate (null = general)

Response: while consolidation is disabled the endpoint returns 503 with a reason field. The success shape below is documented for the self-hosted engine, where an operator may enable it deliberately.

FieldTypeDescription
domainstringDomain consolidated
atoms_beforeintAtom count before
atoms_afterintAtom count after. With soft consolidation (the default) the sources are kept and the prototypes are added, so this count rises rather than falls, and compression_ratio comes out below 1
prototypes_createdintNew prototype atoms
singletons_protectedintVon Restorff protected atoms
compression_ratiofloatatoms_before / atoms_after
duration_msfloatDuration in milliseconds

GET /memory/stats

Get memory statistics for your tenant.

Response:

FieldTypeDescription
total_atomsintTotal atoms stored — every revision, replaced ones included. This is the number your plan's atom limit is measured against, so a supersession chain of five revisions counts as five
episodesintEpisode-type atoms
prototypesintPrototype atoms (from consolidation)
singletonsintProtected singleton atoms
hebbian_edgesintCOUNT(*) of stored association rows for the tenant. Mixes within-atom pairs (re-derivable from atom content) and cross-atom learned associations indiscriminately, with no dedup: a weak health signal. Don't use it to judge learning progress.
episodic_fingerprintsintNumber of distinct episodic-content fingerprints across stored atoms
domainsstring[]Active domain names
avg_valencefloatAverage outcome valence
avg_importancefloatAverage importance score

GET /memory/atoms/

Fetch a single atom by ID. Returns the full atom envelope (content, concepts, importance, valence, domain, created_at, etc.). Returns 404 NOT_FOUND if the atom does not exist in your tenant.

The envelope also carries this atom's own links in the supersession chain: supersedes (UUID[], the atoms this one replaces) and superseded_by (UUID or null, the atom that replaced this one, if any). This entry point always returns a superseded atom in full, whatever the read-side setting does. For the rest of the chain — everything reachable from this revision in both directions — use GET /memory/atoms/{atom_id}/chain.


GET /memory/atoms/{atom_id}/chain

Every revision reachable from one atom by supersession links. Hand it any revision — the oldest, the newest, one in the middle — and get the whole chain: the walk goes up through superseded_by to the live tip and down through supersedes, which is a list, because one correction can consolidate several predecessors. Starting from any one of several predecessors a single correction consolidated therefore still reveals the others.

include_history on a read grants eligibility under ranking and never promises a complete chain. This is the endpoint that does. It resolves revisions by ID, so a chain is available whether or not read-side filtering is enabled: the chain is a fact about the data, not a behaviour of the filter.

Response:

FieldTypeDescription
atom_idUUIDThe revision the walk started from
revisionsChainRevision[]Every revision found, in topological order along the supersedes links: a predecessor never follows something that replaced it. created_at decides only between branches the links leave genuinely unordered. Find the current revision by the is_head flag, never by position — a truncated or broken chain has no head to end on
danglingUUID[]Chain pointers that resolved to nothing — the revision they name was destroyed by a delete (DELETE /memory/atoms/{atom_id}, or a DELETE /memory/domain/{domain} wipe that took a revision stored in that domain), or belongs to another tenant. Breaks in a chain are reported here, not raised as errors: you already hold these IDs, they came off rows you can read
truncatedbooleantrue when the walk spent its lookup budget (64 lookups per call) and stopped early, so revisions is part of the chain rather than all of it. The budget counts every lookup, dangling misses included, so a badly broken chain can set this with far fewer than 64 revisions returned — read it as "the walk stopped early", never as a chain length. There is no continuation parameter: the walk always spreads outward from the ID in the path, so to reach the far end of a long chain, call again with an ID from the edge of the revisions you got back

ChainRevision carries id, content, domain (revisions of one chain may live in different domains), concepts, created_at and is_head, plus supersedes and superseded_by under the same omission rule read items follow: the key is absent, not null, when there is nothing to report. In a chain that rule always bites — the head has no superseded_by, the oldest revision has no supersedes — so parse both as optional.

is_head is true for the live tip, the revision nothing has replaced. It is false for every historical revision, and also for a revision whose successor was destroyed (that successor's ID then appears in dangling) — a broken chain has no head, and naming one would rewrite history.

An unknown atom ID and one belonging to another tenant both return 404 NOT_FOUND, deliberately indistinguishable, the same anti-probing rule GET /memory/atoms/{atom_id} follows. An atom that lives in a shared room answers 404 here too: like every by-ID surface, this one resolves against your own account rather than through room membership.


DELETE /memory/atoms/

Destructive administrative operation, not a normal step in the memory lifecycle

Deletion is a destructive, administrative/service-level operation. It is not recommended as part of routine memory maintenance. If a stored fact is wrong or stale, prefer writing a corrected memory with supersedes over deleting the old one: it records the correction and keeps the old revision auditable instead of destroying it outright. Reach for this endpoint for genuine cleanup (secret exposure, legal takedown, tenant offboarding), not as an "update" mechanism.

Supersede instead of delete

The write side of supersession is live: pass supersedes on POST /memory/write to mark a new atom as the correction for one or more old ones. The old atoms are not deleted; they gain a superseded_by pointer and stay fetchable by ID. The read-side filter that hides a replaced revision from default search results is deployed but not yet switched on — the callout under POST /memory/write tracks the current state and the date it was checked — so for now supersedes records the correction rather than hiding the original. Either way the chain itself is readable from any revision in it through GET /memory/atoms/{atom_id}/chain.

One cost to plan for: a superseded revision is still a stored row. total_atoms in GET /memory/stats counts every revision, replaced ones included, and that same total is what your plan's atom limit is measured against — so a long correction history consumes quota even after read-side filtering starts hiding it from search.

Permanently delete one atom. Idempotent: deleting an already-gone atom returns 200 with {"deleted": 0, "atom_id": "..."} rather than 404 — so retry loops are safe.

Atoms written through the Vault (secrets) are immutable via this endpoint: deleting a secret atom returns 403 FORBIDDEN. Manage secrets through the Vault surface instead.

Deleting an atom removes only that atom's row: it does not touch the tenant's learned Hebbian associations. Earlier builds ran an undocumented post-delete sweep across the tenant's whole edge set: any edge without a remaining atom carrying both its endpoint concepts together was deleted, a rule that preserved within-atom pairs (re-derivable from atom content) but destroyed cross-atom learned associations regardless of whether they involved the deleted atom at all. That sweep has been removed. As of this fix, this endpoint deletes the requested atom and nothing else.

The response always carries two more fields, empty and false when the deleted atom was not in a chain:

FieldTypeDescription
superseded_predecessors_left_hiddenUUID[]Predecessors this atom superseded that now have no live successor, so they stay hidden from default reads once read-side filtering is enabled, though always fetchable by ID and through GET /memory/atoms/{atom_id}/chain. Empty unless this atom had its own supersedes chain
history_destroyedbooleantrue when the deleted atom sat in a chain in either direction — it replaced something, or something replaced it. It can be true with the list above empty: deleting the oldest revision of a chain destroys a link without stranding any predecessor

DELETE /memory/domain/

Destructive administrative operation

Same guidance as above, at domain scope: this is a bulk destructive operation, not a routine cleanup step. Prefer writing corrected memories, or using the /memory/feedback endpoint to down-rank stale entries, over wiping a domain.

Wipe every atom in a domain (e.g. project:old-client). Returns {"deleted": <int>, "domain": "...", "skipped_secrets": <int>, "orphan_edges_removed": 0}.

FieldTypeDescription
deletedintNumber of non-secret atoms deleted
domainstringThe domain that was wiped
skipped_secretsintVault secret atoms in the domain that were not deleted (secrets are immune to this endpoint, same as DELETE /memory/atoms/{atom_id})
orphan_edges_removedintDeprecated, always 0. Earlier builds ran the same tenant-wide sweep after every domain wipe: any Hebbian edge without a remaining atom carrying both its endpoint concepts was deleted, wiping out cross-atom learned associations across the tenant regardless of whether they involved the wiped domain's atoms at all. That sweep has been removed. The field is kept for response-shape compatibility only.
superseded_predecessors_left_hidden_countintCount of predecessors left permanently hidden from default reads because their successor lived in this domain and was wiped
history_destroyedbooleantrue when the count above is non-zero

Note: the REST route has no confirm-style interlock of its own — there is no MCP wrapper around it either, so no schema-level safety gate exists anywhere in the stack. Treat this endpoint as destructive on the client side.


DELETE /memory/reset

Single-tenant / self-hosted only, not for multi-tenant deployments

This is a global TRUNCATE across all tenants on the deployment, not a per-tenant wipe. It is disabled by default and refused at boot on any deployment running in a multi-tenant posture. It exists for single-tenant, self-hosted, experimental setups; never enable it on a shared deployment.

Gated server-side; returns 403 FORBIDDEN unless explicitly enabled on the deployment. On success, returns {"status": "cleared"}; there is no deleted-count field in the response.


GET /memory/domains

List active domain names in your tenant. Same data as the domains field of /memory/stats, but cheaper if you only need the list.


POST /memory/atoms/by-external-ref

Look up a single atom by its external_ref (the optional client-supplied identifier set during write). Useful when the client wants to recover an atom ID after restart. Request body: { "external_ref": "<string, 1-255 chars>" }. Response: a single atom envelope (same shape as GET /memory/atoms/{atom_id}). Returns 404 NOT_FOUND if no atom matches.

Like GET /memory/atoms/{atom_id}, this lookup is never filtered: it resolves a superseded revision in full whatever the read-side setting does, and the envelope carries supersedes / superseded_by.


GET /memory/export

Stream every atom in the calling tenant as JSONL (one atom JSON per line). Always tenant-scoped — there is no cross-tenant export and no domain filter; if you need a specific domain, post-filter the stream client-side. For backup / migration.

The stream carries every atom the tenant holds, superseded revisions included, whatever the read-side setting does, and each line carries supersedes and superseded_by — always, unlike read items: an unversioned atom exports as "supersedes": [] and "superseded_by": null rather than omitting the keys. An export of a versioned corpus therefore records which revision replaced which, instead of arriving as a bag of disconnected revisions. Note that there is no import endpoint, and a re-written export gets fresh atom IDs, so a chain has to be re-asserted write by write.


Shared rooms (Beta)

A room is a memory pool shared across accounts. Addressed as a domain of the form xroom:<room_id>, so every memory endpoint that takes a domain works against a room you belong to — reads and writes go through a database-enforced membership check, not an application-layer one.

POST /memory/rooms

Create a room. The caller becomes its owner.

GET /memory/rooms

List the rooms you belong to — how a fresh session re-finds them.

POST /memory/rooms/{room_id}/invites

Mint an invite code for a room you own. Codes are single-use and expire.

GET /memory/rooms/{room_id}/invites

List the outstanding invites for a room you own.

POST /memory/rooms/join

Redeem an invite code and join the room it belongs to.


Secrets

Values are sealed and never returned by any listing. These endpoints exist to discover which secrets are stored, by alias.

GET /vault/secrets

List stored secret aliases and their metadata. Never returns a value.

POST /vault/secrets

Store a secret under an alias.

GET /vault/secrets/by-alias/

Fetch one secret's metadata by alias. Never returns the value.


Health Endpoints

Two monitoring probes for load balancers, uptime services, and post-deploy smokes. Both are public — no authentication required — and are the only non-authed routes under /api/v1/. Hit them as often as your monitoring allows; cost is a single DB SELECT 1 on /health/ready, nothing on /health.

GET /health

Liveness probe. Returns 200 as long as the process is serving. Does NOT check the database, embedding model, or any downstream — a 200 here only means "uvicorn is accepting connections".

Response:

FieldTypeDescription
statusstringAlways "ok" when the process is up.
databasebooleanAlways false — this endpoint intentionally skips the DB round-trip. Use /health/ready for DB status.
versionstringService version (matches the deployed container).

Example:

bash
curl https://core.mnemoverse.com/api/v1/health
# {"status":"ok","database":false,"version":"1.0.0"}

When to use: load-balancer liveness probes, uptime monitors that only care "is the process alive?". Safe at high frequency.


GET /health/ready

Readiness probe. Verifies the service can actually handle traffic — database reachable, memory engine initialised, embedding backend ready. This is what the post-deploy smoke hits to confirm a Railway deploy rolled out successfully.

Response:

FieldTypeDescription
readybooleantrue only when every sub-check passes.
checks.databasebooleanDB pool is connected and SELECT 1 succeeds.
checks.enginebooleanMemoryEngine initialised and wired to storage.
checks.embeddingbooleanLocal model loaded OR API backend responded to warm-up.
versionstringService version.

Example:

bash
curl https://core.mnemoverse.com/api/v1/health/ready
# {"ready":true,"checks":{"database":true,"engine":true,"embedding":true},"version":"1.0.0"}

When to use:

  • Kubernetes / Railway readiness probe — route traffic only after this returns 200 with every sub-check true.
  • Post-deploy smoke tests. Reference retry shape: poll up to 8 × 15 s (2 min cap, enough for a typical Railway rollout), assert HTTP 200 AND ready === true AND every checks.* field is true. Any false = deploy regression; fail the smoke.
  • Your own monitoring: alert when ready=false persists for longer than a deploy window (~2 min).

Status code: always 200 while the process is up. Sub-check values live in the body — ready=false is still a 200 response. This keeps the health surface distinct from errors the retry-wrapping proxies would otherwise treat as transient.


Error Responses

All errors follow a consistent format:

json
{
  "code": "UNAUTHORIZED",
  "message": "Invalid or missing API key",
  "requestId": "req_abc123",
  "retryable": false,
  "details": null
}
HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid request body / schema constraints violated (returned by core's MnemoError(VALIDATION_ERROR, ..., 400) path). Also covers a malformed or duplicate supersedes ID, a supersedes list over 32 items, or a non-empty supersedes on an xroom:* write (see POST /memory/write)
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENAPI key lacks permission for the requested action (NOT raised on cross-tenant reads — those return an empty result set)
404NOT_FOUNDReferenced atom / domain does not exist. A supersedes target that belongs to another organization returns the same 404 as one that does not exist at all: deliberate, so a caller cannot use the response to probe for other tenants' atom IDs
409CONFLICTThe write conflicts with existing state. Under supersedes: a target is already superseded, is a secret atom, or was just claimed by a concurrent write racing yours. Also covers an external_ref collision on POST /memory/write: this returned an unhandled 500 before this release, and now returns 409 and names external_ref in the message
422VALIDATION_ERRORFastAPI/Pydantic-level validation failure (raised before reaching the route handler). Also returned by both batch-write routes when an item carries supersedes, which they refuse outright (see POST /memory/write-batch)
429RATE_LIMITEDToo many requests. Check Retry-After and X-RateLimit-Reset headers
500INTERNALServer-side error (may be retryable)

The codes above are the exact strings the API returns in the code field of the error body — match them with === / == in agent code, not by substring. There is no AUTH_INVALID or INTERNAL_ERROR code; older docs that mentioned them are wrong.

Rate Limits

Rate limits depend on your plan:

PlanRequests/minQueries/dayAtoms
Free601,00010,000
Pro60050,000500,000
Team3,000500,0005,000,000

Rate limit headers are included in every response:

text
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1712345678