Migrate from Supermemory
If you built on Supermemory, the Korely API will feel familiar. The verbs
match: add a memory, search memories, list them, delete them. The main
difference is scoping. Supermemory scopes everything with one free-form
containerTag; Korely names the dimensions:
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
with Supermemory's list endpoint, import them with
POST /v1/batch, then run a search against the migrated
corpus. Entity extraction, graph edges, and typed facts are built
automatically in the background after the import, with nothing to
enable and nothing to configure. Once GET /v1/events
reports processing: 0, they are ready to read.
Concept mapping
Before touching code, map the mental model. Supermemory has one scoping dimension and one document type. Korely names three dimensions and adds a typed fact layer on top of raw memories.
| Supermemory concept | Korely equivalent | Notes |
|---|---|---|
containerTag | user_id | Free-form string of 1 to 255 characters with no / or control characters and not . or .. (replace a / before importing). End users are unlimited on every plan. |
Compound tag e.g. supportbot_customer-4812 | agent_id="supportbot" + user_id="customer-4812" | Split once at migration time; filters are additive after that. |
customId (dedup key) | metadata.original_id | Carry it in metadata; no first-class dedup key in Korely. |
| Document (raw text) | Memory | Same unit. POST /v1/memories or korely.add(). |
Extracted fact (search_mode="memories") | Typed fact, GET /v1/facts | Built automatically after the memory is stored. Subject + predicate + object triple, bi-temporal. |
| Project / workspace | agent_id | One API key, multiple logical applications via agent_id. |
| Memory history | GET /v1/memories/{id}/history | Also available via korely.history(memory_id). |
Call mapping
Every core Supermemory call has a direct Korely equivalent, in the Python SDK and over REST. The full REST contract is in the API reference.
Supermemory calls are shown as in its Python SDK 3.x; in 5.x the
container tag is the first positional argument:
client.add("u1", content=...),
client.search("u1", query=...),
client.list("u1", "documents").
| Supermemory call | Korely SDK | Korely REST |
|---|---|---|
client.add(content="...", container_tag="u1") | korely.add("...", user_id="u1") | POST /v1/memories |
client.search.memories(q="...", container_tag="u1") | korely.search("...", user_id="u1") | POST /v1/memories/search |
client.documents.list(container_tags=["u1"]) | korely.get_all(user_id="u1") | GET /v1/memories?user_id=u1 |
GET /v3/documents/:id | korely.get(memory_id) | GET /v1/memories/:id |
DELETE /v3/documents/: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) |
search_mode="memories" (extracted facts) | korely.get_facts(user_id="u1") | GET /v1/facts?user_id=u1 |
To clear one end user entirely, korely.delete_all(user_id="u1")
maps to DELETE /v1/users/:user_id/memories: delete every
memory for a user with one call.
On scoping: a containerTag that holds an end-user identifier
becomes user_id, free-form, and end users are unlimited on
every tier. If your tags are compound, for example
supportbot_customer-4812, split them: the application part
becomes agent_id, the user part becomes user_id,
and a session identifier becomes run_id. Filters are
additive, so you keep the same isolation with named dimensions instead of
string conventions. One Supermemory detail to remember while exporting:
memory search takes containerTag (one tag) or
containerTags (an array); the document list endpoint still
accepts containerTags but marks it deprecated (its
filters object is the documented way to narrow the list).
If you used customId for deduplication, carry it over inside
metadata so every imported memory keeps a pointer to its
original record.
In code, the switch looks like this:
# before
from supermemory import Supermemory
client = Supermemory(api_key="...")
client.add(content="Prefers invoices as PDF", container_tag="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")
The Python and Node SDKs are live on PyPI and npm as korely-memory.
See the SDK reference for the full method list.
Step 1: export from Supermemory
Supermemory's list endpoint, POST /v3/documents/list, returns
your documents page by page with their containerTags and
metadata; pass includeContent: true and each one
carries its text in the same call. Write everything to one JSON file:
import json, requests
SM = "https://api.supermemory.ai"headers = {"Authorization": "Bearer sm_..."}
export, page = [], 1while True: r = requests.post( f"{SM}/v3/documents/list", headers=headers, json={"containerTags": ["customer-4812"], "limit": 200, "page": page, "includeContent": True}, ) batch = r.json().get("memories", []) for doc in batch: export.append( { "content": doc["content"], "containerTags": doc.get("containerTags", []), "metadata": doc.get("metadata") or {}, "created_at": doc.get("createdAt"), # the upload day: Supermemory's list does not return the documentDate you may have set on upload } ) if len(batch) < 200: break page += 1
json.dump(export, open("supermemory_export.json", "w"))print(len(export), "memories exported")
Run it once per container tag, or drop the containerTags
filter to export the whole project in one pass.
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("supermemory_export.json"))
def to_scope(tags): # one containerTag per end user: the tag is the user_id return {"user_id": tags[0]} if tags else {}
def to_korely(item): memory = { "content": item["content"], "metadata": item.get("metadata", {}), **to_scope(item.get("containerTags", [])), } 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), 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 == "rate_limit_exceeded": # wait as long as the server says time.sleep(int(r.headers.get("Retry-After", "60"))) else: # quota_exceeded or agent_cap_exceeded: stop raise SystemExit(r.json()["message"])
# POST /v1/batch takes up to 500 memories per requestfor i in range(0, len(memories), 500): print(send(memories[i : i + 500])) # {"id": "job_9b21f4", "status": "pending", "received": 500}
If your tags are compound, replace to_scope with a split:
agent_id from the application part, user_id from
the user part. Poll the job until it reports completed:
curl https://api.korely.ai/v1/batch/job_9b21f4 \ -H "Authorization: Bearer kor_live_..."
# 200 OK{"id": "job_9b21f4", "status": "completed", "received": 500, "imported": 500, "failed": 0, "errors": []} Keep the import inspectable. Tag migrated memories in
metadata, for example
{"source": "supermemory_export"}, so you can filter them
with the metadata filter of
POST /v1/memories/search and audit exactly what came over.
Step 3: verify recall
The primary recall path on Korely is GET /v1/context: it
assembles a compact answer from the typed facts and relevant memories it
holds about one end user, with the source ids attached. This is the call
you put in front of your agent's model, it returns the active facts first,
not a raw list to re-rank. Point it at a query you know your old corpus can
answer:
curl "https://api.korely.ai/v1/context?query=invoice%20preferences&user_id=customer-4812" \ -H "Authorization: Bearer kor_live_..."
# 200 OK (shortened here: the notes in the block, and the stable, volatile and stable_hash fields, are left out){ "context": "_The facts below are a compact profile of the user; the memories are the verbatim source of truth. Ground your answer in this context. [...] do not abstain when it is present._\n\n## Known facts\n- The user prefers PDF invoices (since 2026-06-15)\n- The user replies_fastest_before 10am CET (since 2026-06-15)\n\n## Relevant memories\n_A time in square brackets [...] is the day it was said._\n- [2026-06-15] Prefers invoices as PDF, replies fastest before 10am CET.", "tokens": 309, "sources": ["fct_b91e", "fct_c30a", "mem_7c20de"], "degraded": false, "degraded_parts": []}
The context string is fact-assembled from the bi-temporal
layer, and sources mixes the fact ids
(fct_) and memory ids (mem_) that backed it. 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 what Korely returns.
If you want the raw matches instead, the closest Supermemory analogue,
POST /v1/memories/search returns semantic vector matches
(cosine similarity over the stored embeddings) scoped to one end user. Use
it as a secondary path when you want the underlying memories rather than the
assembled context:
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_7c20de", "score": 0.91, "snippet": "Prefers invoices as PDF, replies fastest before 10am CET.", "user_id": "customer-4812", "agent_id": null, "metadata": {"source": "supermemory_export"}} ]}
If you relied on search_mode="memories" for extracted facts,
the equivalent check is GET /v1/facts?user_id=customer-4812:
extraction runs after the memories are stored, so the typed layer fills in
after the batch job completes. Wait until GET /v1/events
reports processing: 0, then check
GET /v1/facts?user_id=customer-4812.
Gotchas
A few things that catch migrating teams off guard:
- Keep the original dates with
timestamp. Supermemory lets you back-date a memory at write time, and so does Korely:POST /v1/memories, the SDK'sadd()and everyPOST /v1/batchitem take an optionaltimestamp(ISO 8601), stored as the facts'valid_from. Without it, every migrated fact reads as true since the day of the import. The import above sends it. If you backdated documents withdocumentDate, keep those dates in your own records and pass them astimestamp, because Supermemory's read endpoints do not return them. -
search()has notime_filterorinclude_history. Korely search takesquery,user_id,agent_id,run_id,metadata,limit,min_scoreandexclude_sessions; none of them is a date filter. For temporal queries useGET /v1/facts?as_of=2025-01-01on the typed-fact layer. - Agent cap is plan-limited; end users are not.
If you register more than your plan's
agent_idlimit you receive a429 agent_cap_exceededresponse (withlimitandused). End users (user_id) are always unlimited. Keep application identifiers inagent_idand per-customer identifiers inuser_id. - Write quota is enforced; reads are generous.
Each plan has a monthly write quota (memories added or updated, and facts written or corrected). When you
hit it the API returns
429with body{"code": "quota_exceeded", "message": "..."}, the write-quota 429 carries noRetry-Afterheader; you clear it with a top-up of the month on a paid plan, an upgrade or the monthly reset, not by retrying. (Only the separate per-minute, per-hour or per-day rate-limit 429 carries aRetry-Afterheader, in integer seconds.) Queries get a hundred times or more the writes. A batch counts the writes of each of its memories (one per 6,000 characters of text), and one that does not fit in the month's writes left is refused whole, with nothing stored. Quotas per plan are on the pricing page. - Optimistic concurrency on
update().PATCH /v1/memories/{id}accepts an optionalexpected_updated_atfield: theupdated_atyou read, sent back exactly as the API returned it. If the memory was modified between your read and your write, the API returns409 stale_write. Re-fetch and retry. Without the field the last write wins. This is a guard against clobber in concurrent agent environments. - Batch jobs are asynchronous.
POST /v1/batchreturns a job id immediately with statuspending(thenprocessing, thencompleted). When the job iscompleted, every memory is stored and searchable. Typed facts are extracted in the background after that. PollGET /v1/batch/{id}untilcompleted, then pollGET /v1/eventsuntil itsprocessingcount is 0 before you checkGET /v1/factsorGET /v1/context.
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 whose relation is learned in each end user's vocabulary (each new relation
gets its family once) in 10 families (preferences, people, places, work,
ownership, health, financial, events, identity, other). The graph is traversable via the SDK and REST: fetch all memories
for a user, or query facts directly with korely.get_facts() /
GET /v1/facts. Graph reads are pure SQL and graph lookups
with zero AI calls and no embeddings.
# retrieve typed facts for a user: flat list, the graph edge layer
facts = korely.get_facts(user_id="customer-4812")
# each fact keeps the phrase as extracted in predicate_raw next to predicate, the relation it resolved to (usually the same words in snake_case): here both are "prefers"
# facts[0].subject "customer-4812", facts[0].predicate "prefers", facts[0].object "PDF invoices"
# or via REST → JSON {"facts": [...], "total": N}
# GET /v1/facts?user_id=customer-4812
# list all memories: GET /v1/memories?user_id=customer-4812 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. The temporal
engine runs on every tier; GET /v1/facts and
korely.get_facts() work on every plan, including the free
hobby tier. 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 surfaces: reads are retrieval, not generation. The intelligence runs once, at write time, which is why read quotas are a hundred times or more the 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 info@korely.ai. We read every message.