Build long-term memory into your agents
FishMem is a mem0-compatible memory API. Use API keys from the FishMem dashboard to add memories from conversations, run hybrid graph + vector search, and manage scoped memory stores from your backend.
Get started
Quickstart
Two calls cover most agents: write conversation turns withPOST /v1/memories, then recall relevant facts withPOST /v1/memories/searchbefore each model call. Every memory lives under a scope key (user, agent, or run) so retrieval stays per-user.
const API_KEY = process.env.FISHMEM_API_KEY;
const BASE_API_URL = "https://fishmem.com";
async function addMemories() {
const response = await fetch(`${BASE_API_URL}/v1/memories`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
messages: [
{ role: "user", content: "I'm vegetarian and I'm allergic to nuts." },
{ role: "assistant", content: "Got it — vegetarian, no nuts." }
],
user_id: "alice"
})
});
if (!response.ok) {
throw new Error(`add failed: ${response.status} ${await response.text()}`);
}
return response.json(); // { results: [{ id, memory, event }] }
}async function searchMemories(query: string) {
const response = await fetch(`https://fishmem.com/v1/memories/search`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FISHMEM_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ query, user_id: "alice", top_k: 5 })
});
const { results } = await response.json();
// Feed the memories into your prompt before calling the model.
return results.map((r) => r.memory).join("\n");
}Security
Authentication
Create a backend key
Open API Keys in the dashboard, create a named key, and copy the secret immediately. Keys start withrm_and are shown once.
Send a Bearer token
Include the API key in theAuthorizationheader for every server-side request.
Authorization: Bearer rm_your_api_keyKeep API keys in a secret manager or backend environment variable. Do not embed them in browsers, mobile apps, public repos, support tickets, or analytics logs. Every key is bound to one workspace and only reads that workspace's memories.
Write
Add memories
/v1/memoriesAdd memories from a conversation
Withinfer: true(the default) an LLM extracts dated atomic facts from the input and reconciles them against the scope's existing memories, deciding per fact whether to ADD, UPDATE, INVALIDATE, or DELETE. An INVALIDATE keeps the superseded fact with a closed validity interval, so "used to live in Berlin" stays searchable after the user moves. Withinfer: falsethe input is stored verbatim as a single memory — cheaper, and useful for notes or pre-extracted facts.
Request body
messagesRequired · array | string
Conversation turns as [{role, content}] objects, or a plain string. One of messages or content is required.
contentOptional · string
Alternative to messages: a single string to remember. Ignored when messages is present.
user_idOptional · string
Scope key identifying the end user the memory belongs to. At least one of user_id, agent_id, run_id is required.
agent_idOptional · string
Scope key identifying the agent. Useful for memories that belong to an assistant rather than a person.
run_idOptional · string
Scope key identifying a single session or run. Use it for short-lived working memory.
metadataOptional · object
Arbitrary JSON stored with each extracted memory and returned on reads. Useful for tags, sources, or app-side ids.
inferOptional · boolean
When true, an LLM extracts dated atomic facts and reconciles them against existing memories. When false, the input is stored verbatim as a single memory.
Values: true (default), false
Add response
resultsRequired · array
One entry per memory the engine touched while processing the request.
results[].idOptional · string
Memory id. Store it to fetch, update, or delete the memory later.
results[].memoryOptional · string
The extracted (or verbatim) memory text that was written.
results[].eventOptional · enum
What the engine did: ADD created a new fact, UPDATE rewrote an existing one, INVALIDATE closed a superseded fact's validity interval (it stays searchable), DELETE removed a contradicted fact.
Values: ADD, UPDATE, INVALIDATE, DELETE
curl --request POST "https://fishmem.com/v1/memories" \
--header "Authorization: Bearer $FISHMEM_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"messages": [
{ "role": "user", "content": "I moved to Lisbon last month." },
{ "role": "assistant", "content": "Nice, how is Lisbon treating you?" }
],
"user_id": "alice",
"metadata": { "source": "support-chat" }
}'{
"results": [
{
"id": "mem_8f2c1a",
"memory": "Moved to Lisbon in May 2026",
"event": "ADD"
},
{
"id": "mem_31bd07",
"memory": "Lives in Berlin",
"event": "INVALIDATE"
}
]
}// Store a fact verbatim, skipping LLM extraction (1 credit instead of 2).
await fetch("https://fishmem.com/v1/memories", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FISHMEM_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
content: "Prefers TypeScript examples over Python",
user_id: "alice",
infer: false
})
});Recall
Search memories
/v1/memories/searchHybrid retrieval over a scope
Search runs four retrievers in parallel — vector similarity, keyword match, a temporal stream of recent facts, and Personalized-PageRank diffusion over the memory graph — then fuses them with weighted reciprocal-rank fusion into a single ranked list. Typical p50 latency is ~400–600ms, so call it once per agent turn rather than per token.
Request body
queryRequired · string
Natural-language query. Phrase it the way your agent would ask, e.g. "what does the user eat?".
user_idOptional · string
Scope key to search within. At least one of user_id, agent_id, run_id is required.
agent_idOptional · string
Scope key to search agent-owned memories.
run_idOptional · string
Scope key to search a single session's memories.
top_kOptional · number
Maximum number of results to return after fusion and re-ranking.
Values: 1–50, default 10
Search result (memory object)
idRequired · string
Memory id.
memoryRequired · string
The memory text.
memory_typeOptional · string
Engine classification of the memory, e.g. semantic or episodic.
importanceOptional · number
Engine-assigned importance weight used during retrieval.
user_id / agent_id / run_idOptional · string | null
Scope keys the memory was written under.
metadataOptional · object | null
The metadata object supplied at write time, if any.
created_at / updated_atOptional · ISO-8601 string
Server timestamps for creation and the latest change.
event_dateOptional · ISO-8601 string | null
When the fact happened (extracted by the LLM), as opposed to when it was stored.
valid_from / valid_toOptional · ISO-8601 string | null
Validity interval. An INVALIDATE event sets valid_to on the superseded fact so history stays queryable.
scoreOptional · number
Fused relevance score. Present on search results only; higher is more relevant.
curl --request POST "https://fishmem.com/v1/memories/search" \
--header "Authorization: Bearer $FISHMEM_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"query": "where does the user live?",
"user_id": "alice",
"top_k": 5
}'{
"results": [
{
"id": "mem_8f2c1a",
"memory": "Moved to Lisbon in May 2026",
"memory_type": "semantic",
"importance": 0.8,
"user_id": "alice",
"agent_id": null,
"run_id": null,
"metadata": { "source": "support-chat" },
"created_at": "2026-06-01T09:12:00.000Z",
"updated_at": "2026-06-01T09:12:00.000Z",
"event_date": "2026-05-15T00:00:00.000Z",
"valid_from": "2026-05-15T00:00:00.000Z",
"valid_to": null,
"score": 0.91
}
]
}const response = await fetch("https://fishmem.com/v1/memories/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FISHMEM_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
query: "dietary restrictions",
user_id: "alice",
top_k: 10
})
});
const { results } = await response.json();
const context = results
.map((r) => `- ${r.memory} (score ${r.score.toFixed(2)})`)
.join("\n");CRUD
Manage memories
/v1/memoriesList every memory in a scope without ranking. Returns{ results: [...] }of memory objects (same shape as search results, withoutscore).
Query parameters
user_id / agent_id / run_idRequired · string
Scope to list. At least one scope key is required as a query parameter.
limitOptional · number
Maximum number of memories to return.
Values: 1–500, default 100
offsetOptional · number
Number of memories to skip, for pagination.
Values: default 0
curl --request GET "https://fishmem.com/v1/memories?user_id=alice&limit=50&offset=0" \
--header "Authorization: Bearer $FISHMEM_API_KEY"/v1/memories/{id}Fetch a single memory object by id. Returns404 MEMORY_NOT_FOUNDif the id does not exist in your workspace.
curl --request GET "https://fishmem.com/v1/memories/mem_8f2c1a" \
--header "Authorization: Bearer $FISHMEM_API_KEY"/v1/memories/{id}Overwrite a memory's text and/or metadata. The change is recorded in the memory's history. Returns{ id, memory, event: "UPDATE" }.
Request body
memoryOptional · string
New memory text. text is accepted as an alias. At least one updatable field is required.
metadataOptional · object
Replacement metadata object stored with the memory.
curl --request PUT "https://fishmem.com/v1/memories/mem_8f2c1a" \
--header "Authorization: Bearer $FISHMEM_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"memory": "Moved to Lisbon in May 2026; works remotely",
"metadata": { "source": "support-chat", "verified": true }
}'/v1/memories/{id}Permanently delete one memory. Returns{ id, deleted: true }.
curl --request DELETE "https://fishmem.com/v1/memories/mem_8f2c1a" \
--header "Authorization: Bearer $FISHMEM_API_KEY"/v1/memories?user_id=...Delete every memory in a scope — for example when a user exercises a data-deletion request. At least one ofuser_id,agent_id,run_idis required. Returns{ deleted: <count> }.
curl --request DELETE "https://fishmem.com/v1/memories?user_id=alice" \
--header "Authorization: Bearer $FISHMEM_API_KEY"/v1/memories/{id}/historyRead the full change log of a memory: every ADD, UPDATE, INVALIDATE, and DELETE event with the value before and after.
History response
resultsRequired · array
Change-log entries for the memory, oldest first.
results[].idOptional · string
History entry id.
results[].memory_idOptional · string
The memory this entry belongs to.
results[].eventOptional · enum
What changed at this point in the memory's life.
Values: ADD, UPDATE, INVALIDATE, DELETE
results[].previous_valueOptional · string | null
Memory text before the change. Null for ADD events.
results[].new_valueOptional · string | null
Memory text after the change. Null for DELETE events.
results[].created_atOptional · ISO-8601 string
When the change happened.
{
"results": [
{
"id": "hist_01",
"memory_id": "mem_8f2c1a",
"event": "ADD",
"previous_value": null,
"new_value": "Moved to Lisbon in May 2026",
"created_at": "2026-06-01T09:12:00.000Z"
},
{
"id": "hist_02",
"memory_id": "mem_8f2c1a",
"event": "UPDATE",
"previous_value": "Moved to Lisbon in May 2026",
"new_value": "Moved to Lisbon in May 2026; works remotely",
"created_at": "2026-06-03T14:40:00.000Z"
}
]
}Billing
Credits
Writes and searches consume workspace credits; reads are free. Inferred adds cost more than raw adds because they run LLM extraction and reconciliation. Track your balance, plan, and included monthly credits in the dashboard under Billing.
| POST /v1/memories (infer: true) | 2 credits per request |
| POST /v1/memories (infer: false) | 1 credit per request |
| POST /v1/memories/search | 1 credit per request |
| GET /v1/memories, GET /v1/memories/{id}, history | Free |
An inferred add is charged once per request regardless of how many facts the engine extracts. List, get, and history calls are never metered.
Errors
Errors
Use the HTTP status to decide whether to retry, correct the request, or surface an account action. Error responses include a machine-readableerrorcode and a sanitizedmessage.
| 401 | INVALID_API_KEY | API key missing, malformed, or revoked |
| 400 | SCOPE_REQUIRED | None of user_id, agent_id, run_id was provided |
| 400 | INVALID_QUERY | Search query missing or empty |
| 400 | INVALID_MESSAGES | messages array or content string missing or empty |
| 400 | INVALID_BODY | Request body is not valid JSON or has nothing to apply |
| 404 | MEMORY_NOT_FOUND | Memory id does not exist in your workspace |
| 500 | MEMORY_ENGINE_ERROR | The memory engine failed; safe to retry |
Error response
codeRequired · number
HTTP status code repeated in the body for log pipelines.
messageRequired · string
Safe human-readable explanation that can be shown to operators.
errorRequired · string
Machine-readable error code for branching and retries.
{
"code": 400,
"message": "One of user_id, agent_id, run_id is required",
"error": "SCOPE_REQUIRED"
}