API Reference
LongMemory is a memory layer for AI. Your app saves what its users say, and before the AI answers, one search returns everything relevant: memories, preferences, past events and document passages, as one block of text ready for the prompt.
localhost:8080.Most apps need only three calls: save, search and upload a document. To capture memories from a chat, add the piggyback kit to the AI call your app already makes. All bodies are JSON; unknown fields are rejected with a 400; max body size is 1 MB (25 MB for document uploads).
Quickstart
Start LongMemory from its folder:
./longmemory # dashboard + API on http://127.0.0.1:8080 ./longmemory --no-browser # API only
1. Save what you learn about a user:
curl -s localhost:8080/v1/memories -d '{
"user_id": "u1",
"memories": ["Allergic to shellfish, carries an EpiPen."],
"preferences": [{"who": "wife", "topic": "diet", "value": "vegetarian", "type": "must", "until": ""}]
}'2. Upload a document (optional):
curl -s localhost:8080/v1/documents -F knowledge_base="Menus" -F file=@menu.pdf
3. Search before your AI answers:
curl -s localhost:8080/v1/search -d '{"user_id": "u1", "query": "order dinner for me and my wife"}'Put the contextfield of the response into your AI model's system prompt. That is the whole integration.
Authentication
Two modes, chosen by whether LM_ADMIN_KEY is set.
| Mode | When | Your app calls with | Admin routes |
|---|---|---|---|
| Open single-tenant | LM_ADMIN_KEY not set | No auth; one implicit tenant | Not available (501) |
| Multi-tenant | LM_ADMIN_KEY set | Authorization: Bearer <key> | X-Admin-Key |
LM_ADMIN_KEY for anything reachable from the internet, behind TLS.Errors are {"error":{"code":"...","message":"..."}} with status 400/401/404/429/500/501. Rate limiting is per tenant (rate_rps, burst 2×) and returns 429 with Retry-After.
Core concepts
user_id per end user of your app (a customer or employee ID). Everything about a user is kept apart from every other user. The earlier name owner_id is still accepted.who is a name, a relation such as wife, or self). A newer statement replaces an older one, temporary ones (until a date) expire on their own, and rules always come first.user_id is searched for that user only.POST /v1/search returns: preferences in force now, the relevant memories, events and document passages, ready for your AI model's prompt.Save
/v1/memoriesOne call takes any combination of memories, preferences and events, up to 100 of each. No AI is needed to save.
{
"user_id": "u_42",
"memories": ["Allergic to shellfish; carries an EpiPen.",
{"text": "Works at Google as a software engineer.", "topic": "work"}],
"preferences": [
{"who": "self", "topic": "cuisine", "value": "biryani", "type": "like", "until": ""},
{"who": "self", "topic": "sugar", "value": "none", "type": "must", "until": "2026-12-31"}
],
"events": [{"kind": "order", "text": "Ordered a vegetable biryani", "payload": {"item": "veg biryani"}}]
}| Part | Format | Notes |
|---|---|---|
memories | A string, or {"text", "topic"} | Filed under the topic (default notes); same text, same memory |
preferences | {"who", "topic", "value", "type", "until"} | type: like | dislike | must (a rule) | fact | forget (removes that topic for that person); until: a date for a temporary one, else "" |
events | {"kind", "text", "payload", "occurred_at"} | Repeated patterns become inferred preferences automatically |
Response
{"stored":[{"path":"/memories/notes/9f2a41c83d10.md"}, …], "preferences":[…], "events":[…]}A preference that cannot be applied is reported as skipped with the reason; the rest of the save still goes through. Full preference operations (see Advanced) are accepted here too.
Search
/v1/searchOne call returns, together, the user's preferences in force now, their most relevant memories and events, and the most relevant document passages, both as ranked results and as context: one block of text ready for the AI model's prompt. No AI call.
{"user_id":"u_42","query":"what should we cook tonight?"}| Field | Default | Meaning |
|---|---|---|
limit | 5 | Max memories and max document passages (up to 50); memories / documents set them separately |
accuracy | standard | "high": up to 50 memories, plus neighbours and dampening (below) |
filter | true | Leave out weakly related results |
search_in | everywhere | Or conversations (memories, preferences, events) or documents |
knowledge_base | all | Search only this knowledge base |
at | now | RFC 3339 time; some preferences depend on the time of day |
High accuracy mode
Off by default. Send "accuracy": "high" on a request, or start the server with --accuracy high (LM_ACCURACY=high) to make it the default; a request can still ask for "standard". It returns up to 50 memories unless memories or limit says otherwise, and adds two steps, still with no AI:
- Neighbours: for the top 3 memories, the 2 saved just before and just after each one (within 30 minutes of it) are added right behind it, since a reply often answers a question without repeating its words. Only when 20 or more memories are asked for.
- Dampening:question words found in more than 20% of the user's memories (a name on every line of a chat) are left out of the search, for users with 100 or more memories.
On LoCoMo (all 10 conversations, messages saved as written, no piggyback, 1,536 questions, Claude Sonnet 5.5 answering and grading) it scored 91.5%, against 81.4% for the standard search at 20 memories. It sends more text to the AI (about 2,270 tokens of memories per question) and a search takes about 8 ms typically instead of about 5 ms. Full log
{"user_id":"u_42","query":"what did I say about running?","accuracy":"high"}Response
{"context":"### Memory (as of 2026-11-04 20:08, wed evening)
- wife: [RULE] diet: vegetarian
### Relevant notes
- ...",
"preferences":[…],"memories":[{"path":"…","text":"…","relevance":{…}}],
"events":[…],"documents":[…],"filtered_out":2,"chars":812,"tokens_estimate":205,
"timings_ms":{"preferences":0.2,"memories":0.7,"events":0.1,"documents":6.1,"total":7.3}}Documents
/v1/documentsUpload PDF, Word (.docx), text, Markdown, HTML, CSV or JSON files, up to 25 MB each, as multipart (file, repeatable) or JSON ({"name": "...", "text": "..."}). Searchable immediately; search returns the matching passages with the document name and page.
# shared with every user, in a knowledge base ("General" if omitted)
curl -s localhost:8080/v1/documents -F knowledge_base="HR policies" -F file=@manual.pdf
# one user's own document: searched only for them, deleted with them
curl -s localhost:8080/v1/documents -F user_id=u_42 -F file=@contract.pdfUploading a file with the same name to the same place replaces it. Scanned PDFs contain only images of text and cannot be read without OCR.
Capture from chat: the piggyback kit
/v1/piggybackLongMemory never calls an AI model. Your app already calls one to answer the user; the piggyback kit makes that same call also return what is worth remembering. No extra AI call, no extra bill.
| The kit has | What it is for | |
|---|---|---|
instructions | Text to add to your system prompt (about 120 tokens) | |
reply_schema | A reply format for structured output: {"reply", "memories", "preferences"} in one response | |
tool | The same lists as a save_to_memory tool, for apps that already use tools |
How it works
- Fetch the kit once:
GET /v1/piggyback. - Add
instructionsto your system prompt, with today's date, and ask forreply_schema(or registertool). - Show the user
reply. - Send
memoriesandpreferencesfrom the model's output, as they are, toPOST /v1/memorieswith theuser_id.
// the model's reply, structured by reply_schema
{"reply": "Got it, I'll keep the biryani vegetarian for Priya.",
"memories": ["Priya is the user's wife."],
"preferences": [{"who": "wife", "topic": "diet", "value": "vegetarian", "type": "must", "until": ""}]}Works with any model that supports structured output or tools, including models on your own servers. Check the results with your model before going live: smaller models may miss or mislabel preferences.
User data & GDPR
| Call | What it does | |
|---|---|---|
GET /v1/owners/{user_id}/memory | List a user’s memories | |
GET /v1/owners/{user_id}/export | Export everything about a user | |
DELETE /v1/owners/{user_id} | Delete a user and everything about them, including their documents (right to erasure) | |
GET /v1/owners/{user_id}/persons | The people a user’s preferences are about | |
GET /v1/owners/{user_id}/profile?person=wife&history=1 | One person’s preferences; history adds replaced and expired ones | |
DELETE /v1/owners/{user_id}/persons/{id} | Delete one person and everything about them | |
GET /v1/documents?knowledge_base=… or ?user_id=… | List documents | |
DELETE /v1/documents/{id} | Delete a document | |
GET /v1/knowledge-bases | List shared knowledge bases (DELETE /v1/knowledge-bases/{name} deletes one) |
Multi-tenant admin
Available when LM_ADMIN_KEY is set. A tenant is the trust boundary — give each customer their own tenant; never share one key across mutually untrusted users.
| Endpoint | Description | |
|---|---|---|
POST /v1/admin/tenants | Create a tenant → returns api_key once (only its SHA-256 is stored) | |
GET /v1/admin/tenants | List tenants | |
DELETE /v1/admin/tenants/{id} | Disable — revokes access, keeps data |
LM_RATE_RPS as instance self-protection; keep per-user quotas in your own gateway.Configuration
| Env / flag | Default | Meaning |
|---|---|---|
LM_ADDR | :8080 | Listen address (also --addr) |
LM_DB | ./longmemory.db | Data file (also --db). Back up by copying it |
LM_ACCURACY | standard | Default for POST /v1/search: standard or high (also --accuracy) |
LM_ADMIN_KEY | unset | Set: multi-tenant mode with API keys |
LM_RATE_RPS | 0 (off) | Per-tenant rate limit, burst 2× |
LM_EMBED_MODEL | the model in the release | Local meaning-search model file; without one, search uses keywords only |
LM_NO_BROWSER | unset | 1: do not open the dashboard (also --no-browser) |
LM_ALLOW_PUBLIC_OPEN | unset | Allow open mode on a public address (not recommended) |
LM_LOG | info | debug logs every request with its time |
LM_ANTHROPIC_KEY | unset | Advanced: enables POST /v1/extract |
Also: GET /healthz (no auth) and GET /v1/stats for counts.
Several instances: failover and scaling
Instances do not sync writes with each other. Each LongMemory instance is one process and one SQLite data file, and it is the only writer of that file. There is no built-in replication, no consensus protocol (no Raft) and no shared state between instances. Durability across zones comes from streaming that file's changes to object storage; scale comes from giving each instance its own share of tenants or users.
Never point two instances at the same data file, and do not keep it on a network file system shared between machines (EFS, NFS, SMB): SQLite's WAL mode relies on shared memory on one machine, so locks are not reliable across hosts and the file can be corrupted. Use a local or block volume (EBS, a persistent disk, a local SSD).
| Goal | How | What to know |
|---|---|---|
| Failover across Availability Zones | One active instance. Litestream streams every committed change of its data file to S3. On failure, a standby in another zone runs litestream restore, then starts LongMemory. | Changes after Litestream’s last sync (about a second by default) can be lost. LongMemory starts in ~0.1 s on an existing file; restore time grows with file size. Your orchestration decides the failover and must stop the old writer first. |
| More capacity | Shard by tenant or by user: N instances, each the only owner of its share; your gateway routes every request for a tenant or user to its instance (e.g. a consistent hash of the ID). Each shard gets its own Litestream copy. | Every request is for one tenant and one user, so no request needs two instances. Shared knowledge-base documents belong to a tenant: when sharding by user, upload them to every instance holding that tenant’s users. |
| Read replicas | Not supported today. | Live replicas (such as LiteFS) are untested, and LongMemory writes its own activity record to the data file even when it only searches, so it cannot run on a read-only copy. |
Measured: 1 to 96 share-nothing instances on one AWS c5.24xlarge (96 vCPUs), each sent 1,000 searches at the same instant: throughput rose from 249 to 5,248 searches per second, with all 96,000 answered and 0 errors at 96 instances. Run log
Performance
| What | Measured | Where |
|---|---|---|
| Save one note | 0.51 ms typical, about 1,100 notes per second | Apple M2 laptop |
| Search, standard (20 results) | 5.2 ms typical, 8.9 ms for the slowest 1 in 100 | Apple M2, 1,536 LoCoMo questions |
| Search, high accuracy (up to 50) | about 8 ms typical | Apple M2, 1,536 LoCoMo questions |
| 1M tokens of history, one user | saved in 1.9 s; search 12.2 ms typical, 18.9 ms for the slowest 1 in 100 | Apple M2 laptop |
| Burst | 96,000 searches at the same instant, 0 errors, 5,248 per second | 96 instances on one AWS c5.24xlarge |
| Restart | about 0.1 s, every memory kept | Apple M2 laptop |
Search cost follows the size of one user's data, not the whole database. Logs and scripts: AWS run · 1M tokens · search times
Advanced: customization
owner_id (the same value as user_id).Preference operations
/v1/opsFull control over structured preferences: a batch of operations applied atomically (all or nothing, up to 200). Use it when you need more than the short form of the save call: people with aliases, merging two people, context such as time of day or place, exact validity windows, or confidence.
{
"owner_id": "u_42",
"ops": [
{"op":"person_upsert","name":"Priya","relation":"wife","aliases":["wife"]},
{"op":"assert","person":"wife","domain":"food","key":"diet",
"value":"vegetarian","polarity":"rule"},
{"op":"assert","person":"self","domain":"food","key":"diet","value":"no sugar",
"polarity":"rule","valid_until":"2027-01-31T23:59:59Z"},
{"op":"retract","person":"self","domain":"food","key":"spice_tolerance"}
]
}Operations
| op | Required | Notes |
|---|---|---|
assert | person, domain, key, value | Creates, reinforces on repeat, or supersedes conflicting facts |
reinforce | assertion_id or person+key | Bumps observed_count; inferred confidence +0.1 |
retract | assertion_id or person+key | Marks matching active assertions retracted (audit kept) |
person_upsert | name | Matches existing person by name/alias; sets relation, aliases |
person_alias | person, aliases | Adds aliases to an existing person |
person_merge | into, from | Reassigns all data, tombstones the merged-from person |
forget_person | person | Hard-deletes a person and everything about them |
assert fields
| Field | Type | Description |
|---|---|---|
person | string | "self", a p_… id, or a name/alias/relation. Auto-creates if unknown. |
domain | string | food, dining, grocery, ride, place, schedule, general… |
key | string | snake_case: diet, allergy, cuisine, budget_per_meal, home_address… |
value | any | Usually a short string. |
polarity | enum | rule | like | dislike | fact (default fact) |
context | object | time_of_day[], days[], location, occasion — only when genuinely conditional |
valid_from | RFC3339 | For preferences that start in the future |
valid_until | RFC3339 | Makes it temporary: shadows, then auto-reverts |
provenance | enum | stated (default) | inferred | imported |
confidence | float | 0–1; overrides the provenance default |
cardinality | enum | single | multi — whether values replace or accumulate |
relation | string | Sets the relation when auto-creating the person |
Response
{"results":[
{"index":0,"action":"person_created","person_id":"p_01hv…"},
{"index":1,"action":"created","assertion_id":"a_01hv…","superseded":["a_01hu…"]}
]}Actions: created, reinforced, retracted, person_created, person_updated, merged, forgotten, not_found (selector matched nothing — the batch still succeeds).
The emit_memory_ops tool
GET /v1/schema/ops returns the JSON Schema of an emit_memory_opstool that emits these full operations. Register it on your agent's AI call and send its output here. For most apps the piggyback kit is simpler: it uses the short form and also captures memories.
Recall
/v1/recallPreferences only: everything known about the people named, valid now (or at at), matching the moment's context, ranked, plus a rendered context block. No AI, about 0.8 ms. POST /v1/search already includes these preferences; use recall when you want them alone, for named people or domains.
{
"owner_id": "u_42",
"persons": ["self", "wife"], // default ["self"]; "*" = everyone
"domains": ["food", "dining"], // default all; "general" always included
"at": "2026-07-08T20:00:00+05:30", // default server now
"location": "home", // optional
"occasion": "date_night", // optional
"max_per_person": 12, // default 12
"token_budget": 800 // context_block budget, default 800
}Response
{
"persons": [
{"ref":"wife",
"person":{"id":"p_…","name":"Priya","relation":"wife"},
"assertions":[
{"id":"a_…","key":"diet","value":"vegetarian","polarity":"rule",
"score":1.1,"effective_confidence":1.0,"provenance":"stated"}
]}
],
"unresolved": [
{"ref":"rohit","candidates":[{"id":"p_…","name":"Rohit Sharma"},
{"id":"p_…","name":"Rohit Verma"}]}
],
"context_block": "### Memory (as of 2026-07-08 20:00, wed evening)\n- You: …",
"at":"2026-07-08T14:30:00Z", "time_of_day":"evening", "day":"wed",
"elapsed_ms":0.8
}unresolvedwith candidates, so your agent can ask “which Rohit?” instead of remembering the wrong person's allergy.Ranking
Rules sort first and survive token-budget trimming. Everything else ranks by provenance weight × effective confidence, plus a context-specificity bonus and a recency term. Inferred facts decay on a 180-day half-life and drop out below 0.35 confidence. Identical inputs always produce identical output.
Events
/v1/episodesLog events on their own, up to 500 per batch (the save call takes the same events, up to 100). Recognized payload fields become behaviour signals; three observations of the same pattern within 90 days become an inferred preference, scoped to a time of day when at least 80% of them agree. No AI involved. Search events alone with POST /v1/events/search.
{
"owner_id": "u_42",
"episodes": [{
"kind": "order_completed",
"domain": "food",
"persons": ["self"],
"text": "Ordered Chicken Biryani from Meghana Foods",
"payload": {"restaurant":"Meghana Foods",
"items":[{"name":"Chicken Biryani","cuisine":"biryani"}],
"total":540},
"occurred_at": "2026-07-07T20:05:00+05:30"
}]
}Recognized payload fields
| Payload field | Becomes key | |
|---|---|---|
restaurant | place | venue | store | frequent_place | |
destination | frequent_destination | |
ride_type | ride_type_affinity | |
items[].cuisine | cuisine_affinity | |
items[].name | dish_affinity |
Anything else in the payload is stored but ignored for inference. Only text is full-text indexed.
Claude memory tool
LongMemory also implements Anthropic's client-side memory_20250818 tool contract, so Claude can manage its own notes and have them be durable, multi-user and searchable. Forward each memory tool_use block's input verbatim, plus an owner_id.
POST /v1/memory
{"owner_id":"u1","command":"create",
"path":"/memories/user.md","file_text":"name: AJ\n"}
→ {"content":"File created successfully at: /memories/user.md",
"is_error":false}Commands: view, create, str_replace, insert, delete, rename. Paths are jailed under /memories. Command-level failures return HTTP 200 with is_error: true — relay that straight back as the tool result.
| Endpoint | Purpose | |
|---|---|---|
GET /v1/memory/tool | Tool descriptor + wiring instructions (no auth) | |
POST /v1/memory/search | Rank a user’s memory files by keywords and meaning | |
GET /v1/owners/{owner}/memory | List the whole /memories tree |
Extraction (optional)
/v1/extractThe one call that uses an AI model, and only if you configure it: text in, preference operations out, using your own Anthropic key (LM_ANTHROPIC_KEY; model LM_ANTHROPIC_MODEL, default claude-haiku-4-5-20251001). Returns 501 when not configured — by design.
{"owner_id":"u_42",
"text":"my wife Priya is vegetarian and I'm off sugar this month",
"at":"2026-07-07","apply":true}Response: {"ops":[...], "results":[...]} — results only with apply:true. The piggyback kit is the recommended way and needs no extra AI call.