Korely

Migrate from Mem0

If you built on Mem0, the Korely API will feel familiar. The verbs are the same: add a memory, search memories, list them, delete them. The scoping is the same: user_id identifies your end user, agent_id identifies your application, run_id identifies a session. Switching is mechanical: map the calls, replay the corpus through the batch endpoint, verify with a search.

The short version: three steps. Export your memories from Mem0 as JSON, import them with POST /v1/batch, then run a search against the migrated corpus. Entity extraction, graph edges, and typed facts build automatically on ingest. There is nothing to enable and nothing to configure.

Concept mapping

Mem0 and Korely share the same scoping vocabulary. The table below maps each Mem0 concept to its Korely equivalent so you know what to keep, what to rename, and what is new.

Mem0 conceptKorely equivalentNotes
Memory Memory (mem_*) Same unit. Stored text plus metadata, scoped by user/agent/run.
user_id user_id Identical. Free-form string. End users are unlimited on every plan.
agent_id agent_id Identical. Namespaces your application. Count is plan-gated (hobby: 2, developer: 10, team: 100, scale: 500).
run_id run_id Identical. Sub-scopes one session or conversation.
metadata metadata Identical. Arbitrary JSON object, stored and returned verbatim.
Graph memory Entity graph + typed facts (fct_*) Korely extracts entities and typed (subject, predicate, object) triples from every memory at write time. No separate enable flag. Available on all plans, free hobby tier included. Mem0 gates its graph memory behind the $249 Pro tier, and has no bi-temporal fact layer on any plan.
Categories / custom categories Predicate families Korely groups predicates into canonical families (preferences, people, places, work, ownership, health, financial, events) plus an other bucket for everything outside those groups. No custom categories; the graph self-organises from the text.
History History + bi-temporal validity Korely keeps a supersede chain for every memory (GET /v1/memories/{id}/history) and a separate bi-temporal record for each fact. Contradicted facts are invalidated, not deleted.
Export job / import job Batch job (job_*) POST /v1/batch accepts up to 500 memories per request, processed asynchronously. Poll with GET /v1/batch/{id}.
Organization API key (kor_live_*) One API key = one tenant. Multiple agents live under the same key, each with its own agent_id.

Call mapping

Every core Mem0 call has a direct Korely equivalent, in the Python SDK and over REST. The full REST contract is in the API reference.

Mem0 callKorely SDKKorely REST
m.add("...", user_id="u1") korely.add("...", user_id="u1") POST /v1/memories
m.search("...", user_id="u1") korely.search("...", user_id="u1") POST /v1/memories/search
m.get_all(user_id="u1") korely.get_all(user_id="u1") GET /v1/memories?user_id=u1
m.get(memory_id) korely.get(memory_id) GET /v1/memories/:id
m.update(memory_id, data="...") korely.update(memory_id, content="...") PATCH /v1/memories/:id
m.delete(memory_id) korely.delete(memory_id) DELETE /v1/memories/:id (drops it from reads and keeps its facts as history; to erase a person's data, delete_all)
m.delete_all(user_id="u1") korely.delete_all(user_id="u1") DELETE /v1/users/:user_id/memories
m.history(memory_id) korely.history(memory_id) GET /v1/memories/:id/history

The scoping parameters carry over with the same names and the same semantics. user_id is your end user's identifier, free-form, and end users are unlimited on every tier. agent_id namespaces your application. run_id sub-scopes one session. Filters are additive.

In code, the switch looks like this:

korely_memory.py python
# before
from mem0 import MemoryClient
m = MemoryClient(api_key="...")
m.add("Prefers invoices as PDF", user_id="customer-4812")

# after
from korely_memory import Korely
korely = Korely(api_key="kor_live_...")
korely.add("Prefers invoices as PDF", user_id="customer-4812")

results = korely.search("invoice preferences", user_id="customer-4812")

Python and Node.js today.

Gotchas

The surface is familiar, but a handful of parameter names and behaviours differ. Check these before running your migration.

Mem0KorelyWhat to do
m.update(id, data="...") korely.update(id, content="...") Rename the keyword argument from data to content. Shape is otherwise identical.
m.add(..., timestamp=...) korely.add(..., timestamp=...) Same name. add(), POST /v1/memories and every POST /v1/batch item take an optional timestamp (ISO 8601), stored as the facts' valid_from, so a backdated memory anchors temporal validity to the original event instead of the import day.
categories / custom_categories Predicate families (automatic) Korely self-organises facts into canonical predicate families (with an other catch-all); there are no caller-defined categories. Filter facts with get_facts(predicate_family=...), or by entity when a predicate falls in other.
output_format / version Not applicable Korely returns one stable JSON shape. Drop these parameters.

Watch for 403 agent_cap_exceeded. Each plan caps the number of distinct agent_id values (hobby: 2, developer: 10, team: 100, scale: 500). End users under each agent are unlimited. If you fan out a separate agent_id per customer, switch to a per-user namespace with user_id instead and a single shared agent_id.

409 stale_write on update. PATCH /v1/memories/{id} accepts an optional expected_updated_at field for optimistic concurrency. If you pass it and the memory has changed since you last read it, the server returns 409 stale_write. Either omit the field (last-write-wins) or re-fetch before retrying.

Step 1: export from Mem0

Use the Mem0 platform export API: create an export job, then fetch the JSON payload when it completes. The export contains one object per memory with the text under memory, plus the user_id and metadata you stored. That shape maps onto the Korely batch payload almost field for field.

Step 2: import with the batch endpoint

POST /v1/batch accepts up to 500 memory objects per request, same shape as POST /v1/memories, processed asynchronously. You get a job id back and poll it until it completes. Each imported item runs the full write pipeline: embeddings, entity extraction, typed-fact extraction with contradiction checking and bi-temporal validity. About a tenth of a cent of intelligence per memory, all included in your plan.

import json, time, requests
export = json.load(open("mem0_export.json"))
def to_korely(item):
memory = {
"content": item["memory"],
"user_id": item.get("user_id"),
"metadata": item.get("metadata", {}),
}
if item.get("created_at"):
# when it was true: the facts take it as valid_from, not the import day
memory["timestamp"] = item["created_at"]
return memory
memories = sorted((to_korely(i) for i in export["results"]), key=lambda m: m.get("timestamp", ""))
def send(chunk):
while True:
r = requests.post(
"https://api.korely.ai/v1/batch",
headers={"Authorization": "Bearer kor_live_..."},
json={"memories": chunk},
)
if r.status_code != 429:
r.raise_for_status()
return r.json()
code = r.json().get("code")
if code == "too_many_batches": # three imports still running
time.sleep(30)
elif code == "rate_limit_exceeded": # wait as long as the server says
time.sleep(int(r.headers.get("Retry-After", "60")))
else: # quota_exceeded: the month is full, stop
raise SystemExit(r.json()["message"])
# POST /v1/batch takes up to 500 memories per request
for i in range(0, len(memories), 500):
print(send(memories[i : i + 500])) # {"id": "job_4e1aa0", "status": "processing", "received": 500}

Poll the job until it reports completed:

Terminal window
curl https://api.korely.ai/v1/batch/job_4e1aa0 \
-H "Authorization: Bearer kor_live_..."
# 200 OK
{"id": "job_4e1aa0", "status": "completed", "imported": 500, "failed": 0}

Keep the import inspectable. Tag migrated memories in metadata, for example {"source": "mem0_export"} , so you can filter them later with GET /v1/memories or verify the import scope with a targeted search.

Step 3: verify with search

Pick a query you know your old corpus can answer and run it scoped to one end user. Memory search is semantic vector retrieval, cosine similarity over embeddings. The only model call on the read path is the query embedding, a fraction of a hundredth of a cent. No generative model composes the output. Your agent's own model does the reasoning over the results. For recall assembled from the typed-fact layer rather than raw snippets, reach for GET /v1/context, see below.

Terminal window
curl -X POST https://api.korely.ai/v1/memories/search \
-H "Authorization: Bearer kor_live_..." \
-H "Content-Type: application/json" \
-d '{"query": "invoice preferences", "user_id": "customer-4812", "limit": 5}'
# 200 OK
{
"results": [
{"id": "mem_3fa1c9", "score": 0.91,
"snippet": "Prefers invoices as PDF, replies fastest before 10am CET.",
"user_id": "customer-4812", "agent_id": null,
"metadata": {"source": "mem0_export"}}
]
}

Extraction runs on ingest, so the graph and the fact store fill in as the batch completes. A GET /v1/facts call against a migrated entity confirms the typed layer is populated.

The recall path your agent should default to is GET /v1/context. It returns a ready-to-inject block assembled from the active typed facts plus the most relevant memories, the differentiator over a raw snippet list. Call it with query, user_id (optional agent_id, token_budget, default 800) and you get back {"context": "_The facts below are...## Known facts...## Relevant memories...", "tokens": 233, "sources": [...]}, where sources mixes the fct_ and mem_ ids that grounded the answer. Raw POST /v1/memories/search stays available when you want the individual snippets and scores.

What your agents gain

A typed knowledge graph

Every imported memory goes through entity extraction. People, companies, products, places, and concepts become nodes; relations between them become typed edges, drawn from canonical predicates grouped into families (preferences, people, places, work, ownership, health, financial, events, and an other bucket for predicates outside the named groups). The graph is queryable: GET /v1/facts?entity=... returns every typed fact touching an entity, its neighbourhood in the graph, so you can walk from any subject to what it is connected to. These reads are deterministic SQL with zero AI calls, reusing stored vectors rather than computing new embeddings.

korely_sdk.py python
# list all memories for a user (SDK)
memories = korely.get_all(user_id="customer-4812")

# or over REST
# GET /v1/memories?user_id=customer-4812&limit=20
# check the graph layer with GET /v1/facts?user_id=customer-4812
# {"facts": [{"subject": "customer-4812", "predicate": "likes", "predicate_raw": "prefers",
#             "object": "PDF invoices", "predicate_family": "preferences", "invalid_at": null, ...}], "total": 1}

See the graph for a full explanation of how entities connect memories and how to traverse those connections over the REST API.

Bi-temporal facts

Each memory also yields typed (subject, predicate, object) triples, and every triple carries valid_from and invalid_at. When new information contradicts an existing fact, a two-stage contradiction check at write time invalidates the old fact and keeps it as history. Default reads return only what is true now; pass include_invalidated=true for the full supersede chain, or as_of on GET /v1/facts for a point-in-time view. Facts reads are deterministic SQL, with no model call, and return flat JSON, {"facts": [...], "total": N}, with each fact's raw verb preserved in predicate_raw alongside the normalized predicate. The temporal engine runs on every tier, and GET /v1/facts / korely.get_facts() work on every plan, free hobby tier included. The full model is in temporal facts.

Memory your end users can see

Every fact is a readable triple, so your product can show a user what the agent knows (get_profile) and let them fix it: correct_fact supersedes a wrong fact, forget_fact closes one, and delete_all erases everything about the user, for good, with an audit record. The dashboard has Correct and Forget on every fact. If agents carry long-term memory about people, the people get the delete button. Read human in the loop for the full model.

Underneath all three: reads are retrieval, not generation. The intelligence runs once, at write time, which is why read quotas are an order of magnitude more generous than write quotas. Quotas for each plan are on the pricing page.

Next steps

  • Quickstart: wire Korely into your stack and run your first search in five minutes.
  • API reference: the complete REST contract behind every call in the mapping table.
  • Architecture: where the write-time intelligence runs and why the read path stays deterministic.
  • Questions about a large corpus or an unusual export shape? Email [email protected]. We read every message.