Migrating from Mem0
Map Mem0 memories and user ids onto twin memory items and end users, and import an export with a short script.
Mem0 keeps memories per user_id. twin memory keeps items per end user, named by the X-Twin-Account
header. A migration reads your Mem0 memories, turns each into an item, and adds them per user.
The console's Import page does this for you: choose a Mem0 export, check each user's end user id, and import. It maps memories as this page does. The script below does the same over plain HTTP.
How the concepts map
| Mem0 | twin memory |
|---|---|
A memory (memory text) | An item of kind note. For raw chat history you still hold, add the turns as message items instead. |
user_id | The X-Twin-Account header. One end user per Mem0 user. |
agent_id | An about tag, such as agent:support-bot. |
run_id / session_id | session. Forget by session removes them together. |
Memory id | Item id, such as mem0-<id>. The same id again is an edit, so a rerun is safe. |
created_at | at. Once an item is held, its time is fixed. |
metadata | metadata. Stored and returned by read; nothing filters on it. |
categories | about tags, such as category:food. At most 20 per item. |
expiration_date | expires. From then, the item is forgotten. |
| Update a memory | Add again with the same id, only to fix the same statement. When something changed, add a new item. |
get_all / get | items, newest first and paged, and item by id. |
| Memory history | versions of the item id. |
| Delete a memory | forget with items. |
| Delete all of a user's memories | forget with the sessions and items, or delete the end user in the console. |
| Search | search. |
1. Export from Mem0
Mem0's list endpoint (POST /v3/memories/, client.get_all in its Python SDK) returns memories in pages,
filtered by user_id. Page through it and save the memories to one JSON file, either as a list or as the
{"results": [...]} envelope the endpoint returns.
The fields the script reads are id, memory, user_id, agent_id, session_id, run_id,
created_at, metadata, categories and expiration_date. Exports differ between Mem0 versions and
plans, so check these names against your file before you run it.
Checked against docs.mem0.ai/api-reference/memory/get-memories on 2026-10-21.
2. Map user ids to end user ids
An end user id holds letters, digits, ., _ and -, and up to 35 characters always fits, whatever your
project is called. A shorter project slug leaves more room: the console's Import shows the limit for the project
you are in, and End users gives the rule. Mem0 user ids are often
emails, which do not fit. The script keeps an id that fits and replaces one that does not with a
stable digest. Keep the mapping it prints, so your app sends the same header later.
3. Import
The script adds each user's memories in batches of 500 items, the most one add takes. It retries a 503
after a pause. Every item has an id, so running it twice changes nothing.
"""Import a Mem0 export into twin memory. Usage: python import_mem0.py export.json"""
import hashlib, json, os, re, sys, time
from collections import defaultdict
import requests
ADD = "https://api.twinmemory.dev/v1/memory/add"
FITS = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,34}")
def account_for(user_id: str) -> str:
if FITS.fullmatch(user_id):
return user_id
return "u_" + hashlib.sha256(user_id.encode()).hexdigest()[:32]
def item_for(m: dict) -> dict:
item = {"id": f"mem0-{m['id']}", "kind": "note", "text": m["memory"]}
if m.get("created_at"):
item["at"] = m["created_at"]
if m.get("expiration_date"):
item["expires"] = m["expiration_date"]
if m.get("session_id") or m.get("run_id"):
item["session"] = m.get("session_id") or m.get("run_id")
about = [f"category:{c}" for c in m.get("categories") or []]
if m.get("agent_id"):
about.append(f"agent:{m['agent_id']}")
if about:
item["about"] = about[:20]
item["metadata"] = {**(m.get("metadata") or {}), "mem0_id": m["id"]}
return item
def post(account: str, batch: list[dict], key: str) -> None:
for attempt in range(5):
r = requests.post(
ADD,
headers={
"Authorization": f"Bearer {os.environ['TWIN_MEMORY_API_KEY']}",
"X-Twin-Account": account,
"Idempotency-Key": key,
},
json={"items": batch},
timeout=60,
)
if r.status_code != 503:
r.raise_for_status()
return
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
r.raise_for_status()
def main(path: str) -> None:
data = json.load(open(path))
memories = data["results"] if isinstance(data, dict) else data
by_user = defaultdict(list)
for m in memories:
by_user[m["user_id"]].append(item_for(m))
for user_id, items in by_user.items():
account = account_for(user_id)
print(f"{user_id}\t{account}\t{len(items)} items")
for start in range(0, len(items), 500):
post(account, items[start:start + 500], f"mem0-import-{account}-{start}")
if __name__ == "__main__":
main(sys.argv[1])Every item here has an id, so a retry is the same items again. The Idempotency-Key header only names
items sent without an id; it is there in case you change the script to drop them.
A request body is at most 1,000,000 bytes. If your memories are long, use smaller batches.
4. Check
Ask status about a few item ids. landed means searchable. Then run a search for a user
you know well.
What changes
- Search returns more than a list. A search answers with what is known now, dated facts and the passages behind them, each with an address. See Search.
- Reading happens in the background. An add is searchable by its words within seconds. The facts read
from it follow within minutes, and the
knownlines within about half an hour. Records and reading gives the timings, andstatussays how far each item has got. - No memory is overwritten for you. A later value does not replace an earlier one; both are kept with the day each was said. See Values and history.
- Metadata does not filter. Narrow a search with
scope,beforeandafterinstead. - An update is a new item, not an edit. Mem0's
updatereplaces a memory. Here, send the sameidagain only to fix the same statement, such as a typo. When something changes, such as a new address, add a new item: memory keeps both, each with its date. A re-send replaces the text and keeps the item'sabout,metadata,expiresandauthorunless you send them;nullclears one. See Versions and edits. get_allandgetareitemsanditem.itemslists what you added for an end user, newest first, up to 200 a page, with each item's id, text,about,metadataandexpires.itemreturns one by its id, whole. Both take a read key. They return items as you sent them; the facts memory read from them are insearch.
Webhooks
Be told when an item lands or expires, when an erasure completes, and when an end user is deleted, with a signed POST you can verify.
MCP overview
Give an MCP client one end user's memory. One hosted URL; your key and the end user's id as two headers, or a sign-in for claude.ai and ChatGPT; and the same tools as the HTTP API.