Concepts
Concepts
The primitives in an OpenMemory space - memories, journal, records, actions, KV, events, context, batch, and forget - and how they fit together.
Everything lives in a space. A space has one API key, a timezone, a dream mode, and these stores:
| Primitive | What it holds | Typical use |
|---|---|---|
| Memories | Short facts with full-text recall | Preferences, people, decisions |
| Journal | One entry per day (and per week) | What happened, decisions, open loops |
| Records | Typed JSON in named collections | Commitments, goals, routines, contacts |
| Actions | An approval ledger | Anything with side effects that needs a yes |
| KV | Namespaced JSON values | Settings, sync cursors, locks |
| Events | A capped append-only log | Run history, audit breadcrumbs |
Two calls tie them together: GET /v1/context reads what a run needs in one go, and POST /v1/batch writes several things atomically. Dreaming tidies memory overnight, and forget deletes things for good.
Spaces
Create one with POST /v1/spaces (no auth, rate limited per IP address). The response contains space_id and api_key (om_...). The key is shown once. Only its SHA-256 hash is stored.
Send the key on every other call as Authorization: Bearer om_... or X-API-Key: om_....
GET /v1/spaceshows settings, usage counts, limits, anddelete_after(when the space will be deleted if it stays unused).PATCH /v1/spacechangestimezone(IANA name) ordream_mode(client,server, oroff).GET /v1/exportreturns everything as one JSON document.DELETE /v1/spacedeletes the space and everything in it, permanently.
Spaces are isolated: one key can never read or change another space. Spaces unused for 90 days are deleted.
Memories
A memory is one fact, in one or two sentences.
{
"subject": "Sam",
"text": "Prefers code reviews on Thursdays",
"tier": "core",
"tags": ["work"],
"source_ref": "slack:C123/1712",
"pinned": false,
"origin": "user",
"expires_at": null
}| Field | Meaning |
|---|---|
subject | Who or what it is about (up to 200 characters). Optional. |
text | The fact (1 to 4,000 characters). Required. |
tier | core facts are always in context. long (default) facts are recalled on demand. scratch facts are short-lived. |
tags | Up to 20 tags of up to 50 characters each. |
source_ref | Where it came from (message id, URL). Do not paste message bodies here. |
pinned | Pinned facts are always in context and never changed by dreaming. |
origin | user when the person stated it, agent (default) when the agent inferred it. Dreaming writes dream. |
expires_at | Optional ISO 8601 time after which the fact is hidden and later removed. |
Writing the exact same subject and text again returns the existing fact (200, created: false) instead of storing a duplicate.
Recall (GET /v1/memories, MCP recall) searches with full-text matching that ignores case and accents and works in any script, including Greek (final sigma and tonos are folded). Any word may match, words match as prefixes, and pinned and core facts get a small boost. Without q it lists facts: pinned first, then core, long, scratch, newest first. You can filter by subject (exact, case-insensitive), tier, or tag.
Each recall increments recall_count on the facts it returns (pass touch=false over REST to skip that). Dreaming uses recall counts to decide which facts to promote to core.
Change a fact with PATCH /v1/memories/{id}. Delete one with DELETE /v1/memories/{id}, or many with forget.
Journal
One entry per date and kind. kind is day (default) or week. Dreaming writes week digests.
curl -s -X PUT https://api.openmemory.sh/v1/journal/2026-11-01 \
-H "Authorization: Bearer $OPENMEMORY_KEY" -H 'content-type: application/json' \
-d '{"summary":"Shipped the export endpoint","decisions":["Use JSON, not CSV"],"open_loops":["Ask Sam about retention"],"merge":true}'PUT replaces the entry unless merge is true. With merge, the summary is appended on a new line and the lists are combined without duplicates. Lists hold up to 50 items of up to 500 characters each, and the summary up to 4,000 characters. GET /v1/journal lists entries newest first, filtered by from, to, and kind.
Records
Records are typed JSON objects in named collections (commitments, goals, routines, anything you like). Collection names use letters, digits, _, ., and -, up to 64 characters. Names starting with dream. are written by dreaming.
{ "key": "deck-for-sam", "status": "open", "due_at": "2026-11-05T09:00:00Z", "data": { "what": "Send Sam the deck" } }- Upsert by key.
POST /v1/collections/{collection}/recordswith akeyupdates the record that has that key (and bumps itsversion). Without a key it always creates. - Compare-and-set.
PATCHtakesexpected_version. If the record has moved on, you get409withcurrent_versionin the error details. - Shallow merge.
PATCHmergesdatainto the existing object. Set a field tonullto remove it. - Query by
status(comma-separated),key, ordue_before. Results are ordered by due date (undated last). Page withnext_cursor.
Records whose status is done, dropped, closed, cancelled, archived, or rejected count as closed. Every other status counts as open for context.
Actions
The approval ledger. An agent proposes something with side effects, the owner approves it, and execution is recorded with a receipt.
curl -s -X POST https://api.openmemory.sh/v1/actions \
-H "Authorization: Bearer $OPENMEMORY_KEY" -H 'content-type: application/json' \
-d '{"class":"email_reply","summary":"Reply to Sam: Thursday works","payload":{"to":"sam@example.com","body":"Thursday works"},"idempotency_key":"reply-sam-1712","expires_at":"2026-11-03T18:00:00Z"}'- Each action gets a short id per space:
A1,A2, ... Ids are case-insensitive (a7works). - Idempotency. Reusing an
idempotency_keywith the same payload returns the existing action (200,created: false). Payloads are compared by content, so key order does not matter. The same key with a different payload is a409. - Compare-and-set transitions.
POST /v1/actions/{id}/transitionwith{"from": "approved", "to": "executing"}succeeds only if the action is stillapproved. Two workers cannot both execute A7. - Expiry. Pending or approved actions past
expires_atbecomeexpiredand can no longer be approved or executed. - Receipts. Send
receipt(any JSON object) withto: "done"orto: "failed"to record what happened.
Allowed transitions:
| From | To |
|---|---|
pending | approved, executing, denied, expired, superseded, blocked |
approved | executing, denied, expired, superseded |
executing | done, failed |
blocked | pending, denied, superseded |
failed | pending |
done, denied, expired, superseded | none (final) |
GET /v1/actions lists open actions (pending, approved, executing) by default. Pass status=done,failed for others.
KV
Namespaced JSON values for settings, cursors, and small state. A namespace uses the same name rules as collections. Namespaces starting with _ are reserved. Values can be any JSON up to 64 KB.
PUT /v1/kv/{ns}/{key} with {"value": ..., "expected_version": n}:
- omit
expected_versionto upsert, 0to create only (fails with409if the key exists, which makes a simple lock),nto update only if the value is still at versionn.
expires_at makes a value disappear after a time. GET /v1/kv/{ns} lists a namespace and GET /v1/kv/{ns}/{key} reads one value.
Events
An append-only log of small structured entries: {"kind": "run", "key": "daily-brief", "status": "ok", "detail": {...}}. It is capped per space and the oldest events are dropped first. List newest first with filters kind, since, and before_id (for paging). Keep message bodies and secrets out of detail.
Context
GET /v1/context (MCP get_context) is the one call to make at the start of a run. It returns:
| Field | Contents |
|---|---|
now, timezone, today | The current time and the space's local date. |
profile.text | Pinned and core facts, one per line (- Subject: text [mem_id]), cut to budget_chars. truncated tells you whether some were left out. |
profile.dream_summary | The owner profile written by the last dream, if any. |
due | Open records due within due_within_days. |
open_records | Open records in the collections you name in collections. Empty if you name none. |
actions | Pending, approved, and executing actions. |
journal | Day entries from the last journal_days days. |
weekly | The latest weekly digest. |
kv | The namespaces you list in kv, as {ns: {key: value}}. |
last_dream | Id, status, finish time, and report of the most recent dream. |
Parameters: budget_chars (200 to 20,000, default 4,000), journal_days (0 to 31, default 7), due_within_days (0 to 90, default 7), kv (comma-separated namespaces), and collections (comma-separated, limits due and fills open_records).
Batch
POST /v1/batch applies up to 50 writes in one transaction: all succeed or none do.
{
"ops": [
{ "op": "memory.remember", "input": { "text": "Sam is out Nov 10-14", "origin": "user" } },
{ "op": "journal.put", "date": "2026-11-01", "input": { "summary": "Planned the review", "merge": true } },
{ "op": "record.update", "collection": "commitments", "id": "rec_...", "patch": { "status": "done", "expected_version": 3 } },
{ "op": "action.transition", "id": "A7", "input": { "from": "executing", "to": "done", "receipt": { "message_id": "m1" } } },
{ "op": "kv.set", "ns": "cursors", "key": "gmail", "input": { "value": { "history_id": 4411 } } },
{ "op": "event.append", "input": { "kind": "run", "status": "ok" } }
]
}Ops: memory.remember, memory.update, memory.delete, journal.put, record.put, record.update, record.delete, action.propose, action.transition, kv.set, kv.delete, event.append.
The response is {"results": [...]} with one result per op, in order. A failed version or status check, or a missing row, aborts the whole batch with 409. The error message names the op index. Ops that read first (record.update, a journal merge, action.propose) see the state from before the batch.
Batch is REST-only. Over MCP, each write is its own tool call.
Forget
POST /v1/forget (MCP forget) hard-deletes. See Forget and privacy for the full rules.
{ "query": "Sam Lee", "scopes": ["memories", "records", "journal"], "dry_run": true }idsdeletes those memories (up to 200).querydeletes memories containing all of its words, plus records and journal entries containing the exact phrase (only in the scopes you list; the default scope ismemories).- Journal text is redacted by default (the phrase becomes
[forgotten]). Setjournal_mode: "delete"to delete the entries instead. dry_run: truereturns what would be deleted without deleting anything.
Deleted items leave content-free tombstones (kind, id, and time only).
Errors
Errors have one shape: {"error": {"code", "message", "details"?}}.
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Invalid input. details.issues lists the fields. |
| 401 | unauthorized | Missing or unknown key. |
| 404 | not_found | No such item in this space. |
| 409 | conflict | A version, status, or idempotency check failed. |
| 413 | bad_request | Request body larger than 256 KB. |
| 422 | quota_exceeded | The space is full for that kind of item. See Limits. |
| 429 | rate_limited | Too many requests. Retry in a minute. |