Docs
Memory API
Give your assistant a memory that stays current. Call it over REST or connect any MCP client.
Build your own assistant on top of MemoryHQ's memory: it remembers what users tell it, keeps facts current when they change, and hands you a ready-made context block for every turn.
Base URL: https://memoryhq.keronlabs.com/api/<workspaceId>
Auth: x-api-key: lore_… or Authorization: Bearer lore_… (create keys in Settings → API Keys). A key acts as the member who created it: user scope means that member's private memories, and workspace scope is shared with the team. read scope is enough for search/context; write is needed to add, correct or forget.
End users
If you're building a product with its own users, use one workspace and one key, and send x-end-user-id: <your user's id> on every call. user scope then means that end user: what you add is private to them, and search, /context, /profile and the memory list only return their memories plus anything you stored with scope: "workspace" (shared knowledge, visible to all of them). One end user can't read, change or delete another's memories; those requests return 404. Shared knowledge is written without the header: a request that carries x-end-user-id can't add, correct or forget workspace memories (403), so one end user's session can't change what all of them see.
- The id is whatever your system uses (user id, UUID, email): 1–128 characters of letters, digits and
. _ : @ | + -. End users don't need a MemoryHQ account, and there's nothing to create first. - Rate limits apply per end user: 120 requests per minute for each key and end user pair.
DELETE /end-users/:iderases everything stored for that end user (memories, history and profile). Call it when your user deletes their account.- Documents and collections stay workspace-wide: every end user's
/contextand/searchdraw on the same documents. - Without the header, a key behaves as before and acts as the member who created it.
Model
- Memory: one atomic fact (
"User prefers aisle seats") with atype(fact, preference, person, org, decision, event, task),aliasesfor keyword recall, and optionalvalidAt/expiresAt. - Never overwritten: when a new fact contradicts an old one, the old one gets
invalidAt+supersededBy. Search returns current facts by default; passincludeHistoryto see what used to be true. - Forgetting:
expiresAtfacts are soft-forgotten automatically, and you can forget manually. Forgotten memories are hidden from search but kept unless hard-deleted. - Profiles: a per-member and a per-workspace one-pager (
staticFacts,dynamicFacts,summary), rebuilt in the background shortly after memories change. - Sources: everything you POST, chat statements in the MemoryHQ app, and every indexed document: uploads (PDF, DOCX, text, transcripts), connector pages, and the backlog from before memory existed. Document memories are workspace-scoped, inherit the collection's access rules, link back via
sourceDocId, and are deleted with the document. Sensitive documents are excluded. New documents are mined about 10 minutes after upload.
Endpoints
| Method | Path | Body / query | Notes |
|---|---|---|---|
| POST | /memories | { content? , messages?: [{role, content}], scope?: "user"|"workspace", infer?: true, expiresAt?: ISO } | Extracts facts, dedupes, supersedes contradictions. Returns { added, updated, skipped: [{content, duplicateOf}] }. infer:false stores content verbatim. Over 12k chars → 202 {queued:true}, processed in background. |
| GET | /memories | ?scope=user|workspace|all&include_history=true&include_forgotten=true&limit=50&offset=0 | Newest first. |
| POST | /memories/search | { query, scope?: "all", limit?: 10, includeHistory?, includeForgotten? } | Hybrid semantic + keyword/alias, ranked by relevance × recency × usage. |
| GET | /memories/:id | ||
| PATCH | /memories/:id | { content } or { forgotten: true|false } | content = explicit correction (new memory supersedes this one). |
| DELETE | /memories/:id | Hard delete. | |
| GET | /profile | ?refresh=true | { user, workspace } profiles; refresh rebuilds now. |
| POST | /context | { query?, limit?: 10, documents?: 0 } | { profiles, memories, documents, prompt }. documents: n also returns the top n permission-filtered document passages (retrieval only, no LLM). prompt is a markdown block to put in your system prompt. |
| POST | /search | { query, history?, collectionIds?, connectorIds? } | Cited answer ([n] = n-th document in sources) over documents, personalised with memory. |
| DELETE | /end-users/:id | Erases one end user's memories and profile. Returns { deletedMemories }. |
Model-backed calls (add, profile refresh, document search) draw from the workspace's AI credits.
MCP
POST /mcp is an MCP server (Streamable HTTP, stateless JSON). Tools: get_profile, search_memories, add_memory, forget_memory, search_documents (passages only, no AI credits) and search_knowledge (written answer with sources, uses AI credits). Point Claude Desktop, Cursor and similar clients at it with your key in the x-api-key header.
A personal assistant in ~40 lines
import Anthropic from '@anthropic-ai/sdk'
const BASE = `https://memoryhq.keronlabs.com/api/${process.env.MEMORYHQ_WORKSPACE}`
const headers = { 'x-api-key': process.env.MEMORYHQ_KEY!, 'Content-Type': 'application/json' }
const claude = new Anthropic()
// userId: your own user's id. Each one gets private memory inside the same workspace.
async function mhq<T>(userId: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(BASE + path, {
method: body ? 'POST' : 'GET',
headers: { ...headers, 'x-end-user-id': userId },
body: body && JSON.stringify(body),
})
if (!res.ok) throw new Error(`${path}: ${res.status} ${await res.text()}`)
return res.json() as Promise<T>
}
export async function chat(userId: string, history: Anthropic.MessageParam[], userMessage: string) {
// 1. Pull profile, relevant memories and document passages for this turn.
const { prompt } = await mhq<{ prompt: string }>(userId, '/context', { query: userMessage, documents: 5 })
// 2. Answer with memory in the system prompt.
const reply = await claude.messages.create({
model: 'claude-sonnet-5-5',
max_tokens: 1024,
system: `You are my personal assistant. Today is ${new Date().toDateString()}.\n\n${prompt}`,
messages: [...history, { role: 'user', content: userMessage }],
})
const text = reply.content.flatMap((b) => (b.type === 'text' ? [b.text] : [])).join('')
// 3. Remember the exchange (fire-and-forget). Extraction keeps only durable facts,
// dedupes against what's known, and supersedes anything that changed.
void mhq(userId, '/memories', {
scope: 'user',
messages: [{ role: 'user', content: userMessage }, { role: 'assistant', content: text }],
}).catch(console.error)
return text
}
For agentic assistants, expose /memories/search, /memories (POST) and PATCH /memories/:id as tools so the model can look things up mid-task and correct or forget facts when the user asks.