SDK
korely-memory is the Python and Node.js
SDK of Korely: a typed client over the
REST API. Each method calls one
endpoint, and the JSON fields in the API reference are the attribute names
here. The CLI uses the same client and
the same key.
Quick start
You need Python 3.9 or later, or Node.js 18 or later, and an
API key (kor_live_...)
from Quickstart. Install the package from
PyPI or
npm:
pip install korely-memory
The script saves two memories for the
end user dana: where
the user lives, then where the user lives now. It waits while Korely
extracts the
facts, then prints the
context block for a question.
In Node, save it as a .mjs file, or set
"type": "module" in package.json: it uses
import and top-level await.
import timefrom korely_memory import Korely
korely = Korely(api_key="kor_live_...")
korely.add("User lives in Lisbon", user_id="dana")korely.add("User now lives in Berlin", user_id="dana")
time.sleep(20) # extraction usually takes about ten seconds
ctx = korely.get_context("where does the user live", user_id="dana")print(ctx.context)# A note for your model, then:# ## Known facts# - The user lives_in Berlin (since 2026-11-01)## ## Relevant memories# ...
Each add() returns at once, and a model then writes the
facts. When it judges that the Berlin fact
contradicts the Lisbon one,
Korely closes the Lisbon fact and keeps it as history, so Known facts lists
only Berlin. The Lisbon memory can still appear under Relevant memories,
marked "(true when said, changed later)". The model picks the relation name, so you may see
another one than lives_in.
If Known facts is missing, extraction has not finished: wait and run
get_context again. If it still lists Lisbon, call
get_facts(user_id="dana", include_invalidated=True) to see
each fact and whether it is closed.
Terms
The Glossary defines the words used across the docs. This page adds seven.
- Client: the
Korelyobject you create with your key. Each method sends one HTTP request;iter_auditsends one per page. - Receipt: what a delete or a forget returns: counts and an
audit_id, never the text. - Page: a list result with a
total, such as theMemoryPageofget_all().totalcounts every match, beforelimitandoffset. - Positional and keyword-only: in Python the first argument (the content, query, id or name) goes by position. A
*in a Python signature means every parameter after it is passed by name, as inuser_id="dana". - Options object: in Node.js, the other parameters go in one object with snake_case keys, as in
{ user_id: "dana" }.getContexttakes its query inside it too. - Invalidated: the word field names use for closed, as in
invalid_at,include_invalidatedandfacts_invalidated. - SDK errors:
KorelyErroris the base class of every error the SDK raises; anAPIErroris one the server answered, with a status and a code. See Errors.
Reference
Client
from korely_memory import Korely
korely = Korely(api_key="kor_live_...")
# Or with no key: KORELY_API_KEY, then the key# that `korely init` saved in ~/.korely/config.jsonkorely = Korely()- Key. With no key found, the constructor raises
KorelyError. It makes no request:ping()checks a key. A key belongs to one project and reads and writes only its memories and facts (Projects). - Address.
base_url(Node:baseUrl), thenKORELY_BASE_URL, then the config file, thenregion. On the Cloud you set none of them. - Timeout and retries. A request times out after 30 seconds (Python
timeout, in seconds; NodetimeoutMs). The SDK never retries. Onrate_limit_exceededor a503, waitretry_afterseconds and call again. Onquota_exceeded, do not retry: wait forresets_ator change plan. - Async. In Python,
AsyncKorelyhas every method as a coroutine (async foroveriter_audit). In Node, every method returns a promise, exceptiterAudit, an async generator you read withfor await.
Three things are called region:
| Name | What to know |
|---|---|
The client's region argument | The API address. Leave it out. |
| The project's region | Which model reads your text to write the facts: global (the default) uses a model from Google, outside the EU; eu uses Scaleway in Paris. Set it on the Projects page of the dashboard. |
region in ping() | Where your data is stored: Korely's servers in Helsinki. |
Methods
Python names and signatures; a ? marks an optional
parameter. Each example stands alone. Node names are camelCase (get_all is
getAll), with the options in one object. In Node, an unknown
option name throws a KorelyError: userId gets
the hint user_id. A method that changes data needs the
memories:write permission,
one that reads needs memories:read; ping needs
neither, and init_agent needs no key.
| Method | REST endpoint | Returns |
|---|---|---|
add(content, *, user_id?, agent_id?, run_id?, metadata?, timestamp?, kind?) | POST /v1/memories | Memory: id, status, kind, facts (empty while processing) |
search(query, *, user_id?, agent_id?, run_id?, metadata?, limit?, min_score?, exclude_sessions?) | POST /v1/memories/search | list of SearchHit: id, score, snippet, user_id, agent_id, metadata |
get_all(*, user_id?, agent_id?, run_id?, limit?, offset?, exclude_sessions?) | GET /v1/memories | MemoryPage: memories, total |
get(memory_id) | GET /v1/memories/:id | Memory, with its facts |
update(memory_id, *, content, expected_updated_at?) | PATCH /v1/memories/:id | Memory, status processing |
delete(memory_id) | DELETE /v1/memories/:id | DeleteReceipt: id, status, facts_invalidated, audit_id |
delete_all(*, user_id) | DELETE /v1/users/:user_id/memories | BulkReceipt: user_id, memories_deleted, facts_deleted, erasure, audit_id |
batch(memories) | POST /v1/batch | BatchJob: id, status, received |
batch_status(job_id) | GET /v1/batch/:id | BatchJob: status, received, imported, failed, errors |
events(*, user_id?, status?, limit?) | GET /v1/events | EventsResponse (a dict): events, processing |
history(memory_id) | GET /v1/memories/:id/history | MemoryHistory: events |
get_facts(*, entity?, subject?, predicate?, predicate_family?, user_id?, agent_id?, as_of?, known_as_of?, include_invalidated?, limit?, offset?, exclude_sessions?) | GET /v1/facts | list of Fact, with .total |
add_fact_triple(subject, predicate, object, *, user_id?, agent_id?, run_id?, subject_type?, object_is_literal?, confidence?, valid_from?, tense?) | POST /v1/facts | Fact, with invalidated |
correct_fact(fact_id, *, subject?, predicate?, object?) | PATCH /v1/facts/:id | Fact, the current one |
forget_fact(fact_id, *, at?) | POST /v1/facts/:id/forget | ForgetReceipt (a dict): id, status, invalid_at, audit_id |
declare_name(name, *, user_id, agent_id?) | POST /v1/users/:user_id/names | DeclaredName: user_id, name, agent_id, since |
get_context(query, *, user_id?, agent_id?, token_budget?, known_as_of?, exclude_sessions?) | GET /v1/context | Context: context, tokens, sources and more |
get_profile(*, user_id, agent_id?, as_of?, known_as_of?, exclude_sessions?) | GET /v1/profile | Profile: user_id, as_of, known_as_of, facts, by_family, total, truncated |
users(*, agent_id?, limit?, offset?) | GET /v1/users | UsersPage: users, total |
list_agents(*, limit?, offset?) | GET /v1/agents | AgentsPage: agents, total, cap, used |
delete_agent(agent_id) | DELETE /v1/agents/:agent_id | AgentDeleteReceipt: agent_id, memories_deleted, facts_deleted, slot_freed, audit_id |
audit(*, user_id?, action?, since?, until?, limit?, offset?) | GET /v1/audit | AuditPage: events, total |
iter_audit(*, user_id?, action?, since?, until?, page_size?, offset?) | GET /v1/audit, page after page | iterator of AuditEvent |
ping() | GET /v1/ping | PingResponse: ok, tier, region, processing_region, scopes |
delete_account(*, confirm) | DELETE /v1/account?confirm=true | AccountDeleteReceipt: deleted, removed |
Korely.init_agent(agent_caller?, *, processing_region?, region?, base_url?, timeout?) | POST /v1/agents/init, no key | AgentInitResult: api_key, tier, region, processing_region, scopes, quotas |
add
Saves a memory and returns it at once, with
status processing and an empty facts list. The facts
follow, usually about ten seconds later.
content: a string, or a list of chat messages (roleandcontent) joined into one text.user_id,agent_id,run_id: any combination (Scoping).metadata: your own labels, returned unchanged.timestamp: the ISO date the events happened; the facts take it asvalid_from.kind:note(the default) orsession(Work sessions).
memory = korely.add( "Northwind Hosting costs 50 euro per month since the June upgrade.", agent_id="infra-bot", user_id="customer-4812", metadata={"source": "slack"},)
print(memory.id) # mem_8f2c1aprint(memory.status) # processing: facts are extracted after the call returnsprint(memory.facts) # []
# About ten seconds laterfacts = korely.get_facts(entity="Northwind Hosting", user_id="customer-4812")print(facts[0].subject, facts[0].predicate, facts[0].object)# Northwind Hosting costs 50 euro per month
Read the facts once get(memory.id).status is
ready. If the list is then empty, extraction wrote no facts;
error means extraction failed (events).
Work sessions: kind and exclude_sessions
Every memory has a kind.
note, the default, is what somebody said or decided.
session is the log of a work session, such as the notes a
coding assistant saves at the end of a session. Both produce facts. A read with
exclude_sessions leaves out the work sessions and the facts
only they state. search, get_all,
get_facts, get_profile and
get_context take it; it is off by default.
# The log of a work session, saved as what it iskorely.add( "Session: moved the billing service to Postgres 16, tests pass, rollback script in ops/.", user_id="dev-team", kind="session",)
# A decision, saved as a note (the default kind)korely.add("We bill in euro only, agreed with finance.", user_id="dev-team")
# Reads that leave the work sessions outctx = korely.get_context("how do we bill customers", user_id="dev-team", exclude_sessions=True)hits = korely.search("billing currency", user_id="dev-team", exclude_sessions=True)
# Each memory says which kind it is (newest first)page = korely.get_all(user_id="dev-team")print([m.kind for m in page.memories]) # ['note', 'session']search
A search finds the memories closest in meaning to the query, by their
embeddings. Each
hit has a
score. For recall,
get_context is usually the better call: it returns the
current facts and the memories as one block.
agent_id,user_id,run_id,exclude_sessions(Work sessions).metadata: every key must match. A value matches as text, and a number also as a number.limit: 15 by default, 50 at most.min_score: 0 to 1, no floor by default.
All filters combine with AND.
results = korely.search( "northwind pricing", user_id="customer-4812", limit=5,)
for hit in results: print(hit.id, hit.score, hit.snippet) # mem_8f2c1a 0.91 Northwind Hosting costs 50 euro per month...get_all
Lists the memories of a scope, newest first, with no query and no model
call. limit is 50 by default and 200 at most; page with
offset. A listed memory has an empty facts list
even when it is ready: read facts with get or
get_facts.
page = korely.get_all( user_id="customer-4812", limit=50, offset=0,)
print(page.total) # 218for memory in page: print(memory.id, memory.created_at) # mem_8f2c1a 2026-06-07T09:14:00.412345+00:00get
One memory: its content, metadata and the facts from it that are true now. Closed facts are in its history.
memory = korely.get("mem_8f2c1a")
print(memory.content) # Northwind Hosting costs 50 euro per month...print(memory.metadata) # {"source": "slack"}print(memory.created_at) # 2026-06-07T09:14:00.412345+00:00update
Replaces the content and runs extraction again. As with add,
it answers processing with an empty facts list.
An edit counts writes like an add: one
per 6,000 characters of the new text.
An update can close facts. A new value closes the old fact even if another memory still states it: the latest edit wins. If extraction finds no facts in the new text, the memory's old facts are closed.
expected_updated_at refuses an update when another writer
got there first: the call raises StaleWriteError (409
stale_write). Copy the value, never type it: pass the
updated_at the API returned, or created_at
before the first edit. The server compares it to the microsecond, so a
rounded timestamp is refused.
current = korely.get("mem_8f2c1a")memory = korely.update( "mem_8f2c1a", content="Northwind Hosting costs 55 euro per month after the storage add-on.", # the exact value the API returned, never one typed by hand expected_updated_at=current.updated_at or current.created_at,)
print(memory.status) # processingprint(memory.facts) # [], the new facts arrive about ten seconds later
# Read them afterwards (newest first)facts = korely.get_facts(entity="Northwind Hosting", user_id="customer-4812")print(facts[0].object) # 55 euro per monthdelete
Hides one memory: it leaves every read, the facts only it stated are
closed and kept as history, and a fact another memory also states stays
open. The audit_id is a receipt for your logs. To
erase the memory for good, call
DELETE /v1/memories/:id?erase=true through the REST API: the
SDK has no method for it.
The receipt's status is forgotten, which means
hidden. If this memory's fact had closed an older fact, the delete can
make the older fact true again, as a new open fact
(Delete a memory).
The service counts these in facts_reopened. Only Node
returns that count today, without a type; Python 0.1.28 leaves it out.
receipt = korely.delete("mem_8f2c1a")
print(receipt.status) # forgottenprint(receipt.facts_invalidated) # 1print(receipt.audit_id) # aud_3d0fdelete_all
Erases for good every memory and fact of one end user in the key's
project, closed facts included, in one call with one audit record.
Memories saved without a user_id stay. erasure
is always "permanent".
See Forget a user.
receipt = korely.delete_all(user_id="customer-4812")
print(receipt.memories_deleted) # 218print(receipt.facts_deleted) # 64print(receipt.audit_id) # aud_91xbbatch and batch_status
Imports up to 500 memories in one call, for a migration. Each item is
shaped like an add: content, and optionally
user_id, agent_id, run_id,
metadata, timestamp, kind. An
unknown key refuses the whole batch with a 422. Each item counts writes as
on add, one per 6,000
characters.
The job runs in the background: its status goes from
pending to processing, then
completed or failed. Poll
batch_status until it ends.
job = korely.batch([ {"content": "Prefers async standups over meetings.", "user_id": "customer-0001"}, {"content": "Renewal date moved to October 1st.", "user_id": "customer-0002"},])
print(job.status) # pending
# Laterjob = korely.batch_status(job.id)print(job.status) # completedprint(job.imported) # 2events
Which writes have finished extraction. Each event has a
memory_id and a status: processing,
ready or error. The processing
count is the number of writes still being extracted, so a script can wait
on one number. limit is 50 by default, 200 at most. The memories
of a batch() appear here only once the job has stored them:
check batch_status() first.
result = korely.events(user_id="customer-4812")print(result["processing"]) # 0for e in result["events"]: print(e["memory_id"], e["status"]) # mem_8f2c1a readyhistory
The lifecycle of one memory: the events created,
updated, fact_extracted,
fact_invalidated and deleted, each with its
time. It answers for a hidden memory too. To follow one fact's chain of
versions, use get_facts with
include_invalidated=True.
h = korely.history("mem_8f2c1a")for event in h.events: print(event.event, event.at) # created ... / fact_extracted ...get_facts
Typed facts (subject, predicate, object) with their two clocks
(valid time and known time), as a
flat list. A database read, no model call, on every plan. Pass
as_of for what was true on
a date, and known_as_of for what Korely knew on a date.
entity(an entity) matches either side of the triple.predicateis the relation name Korely normalizes, such aslives_in;predicate_rawis the wording as extracted.predicate_familygroups relations into ten families (preferences, people, places, work, ownership, health, financial, events, identity, other). Korely picks the family, so filter byentitywhen you are not sure of it.include_invalidated=Trueadds the closed facts.limitis 50 by default and 200 at most;.totalis the count before paging.
# Current state: only open factsfacts = korely.get_facts(entity="Northwind Hosting")print(facts[0].object) # 50 euro per monthprint(facts[0].invalid_at) # None, open
# Point-in-time: what was true on June 1?facts = korely.get_facts(entity="Northwind Hosting", as_of="2026-06-01")print(facts[0].object) # 40 euro per monthprint(facts[0].invalid_at) # 2026-06-07T09:14:00+00:00, closed since
# What Korely knew on June 1. It learned of the change on June 7,# so on June 1 the 40 euro fact was still open.facts = korely.get_facts(entity="Northwind Hosting", known_as_of="2026-06-01")print(facts[0].object) # 40 euro per monthprint(facts[0].closed_known_at) # None, open as of June 1
Each fact has the fields of Get facts.
Besides the triple, the scope and the two clocks
(valid_from, invalid_at, created_at,
closed_known_at):
subject_type,object_is_literal,confidence,tense: as in add_fact_triple.subject_canonical,object_canonical: the names after declared names apply, such asmariafor Maria Rossi;subjectandobjectkeep the words as written.source_memory_id: the memory it was extracted from.source_memory_ids: every memory that stated it.observation_count: how many memories stated it;last_confirmed_at: when one last stated it again.invalidated_by: the id of the fact that closed it, when there is one.
See Temporal facts for how facts close.
add_fact_triple
Writes a fact directly, without extraction, when your agent already has
the subject, predicate and object. The contradiction check still runs: it
calls a model, and the call counts as one write. Pass
valid_from for a fact that began in the past. The fact it
returns carries only the fields the write knows; read it again with
get_facts for the rest. The optional fields:
subject_type: what the subject is, such aspersonororganization;unknownby default.object_is_literal:Truewhen the object is a value (a date, an amount),False(the default) when it names an entity.confidence: a number from 0 to 1, 0.9 by default. You can ignore it at first.tense:current(the default),past(the fact is over, and it closes the open fact it restates) orplanned(an intention).
fact = korely.add_fact_triple( "Marco", "works_at", "Acme GmbH", user_id="customer-4812", subject_type="person", valid_from="2026-06-01",)
print(fact.invalidated) # ids of any facts this one closedcorrect_fact
Replaces a fact with a corrected one: pass at least one of
subject, predicate, object. The old
fact is closed, not edited, so as_of before the correction
still returns what you believed then. It counts as one write. A correction
that states the fact as it already is returns the same fact, with
invalidated empty.
fact = korely.correct_fact("fct_a1", object="Beacon Labs")print(fact.object, fact.invalid_at) # Beacon Labs Noneforget_fact
Closes a fact: it leaves default reads and stays in history
(include_invalidated=True still shows it). Pass
at (ISO date) for when it stopped being true; the default is
now, and a date before the fact's valid_from is a 422. A
second call is safe: it answers already_forgotten.
receipt = korely.forget_fact("fct_9c1e", at="2026-06-30")print(receipt["status"]) # forgottendeclare_name
Tells Korely another name of
an end user: the id maria is Maria Rossi. Korely never merges
two names by itself, because two people can share a name. After the
declaration, the facts about Maria Rossi in that end user's memory, old
and new, read as facts about maria; their words stay as
written.
declared = korely.declare_name("Maria Rossi", user_id="maria")print(declared.user_id, declared.name) # maria Maria Rossiprint(declared.since) # 2026-10-18T21:04:11.412345Z
# The facts about "Maria Rossi" now come back under mariafacts = korely.get_facts(user_id="maria", subject="maria")
Call it once per name, 1 to 200 characters. Declaring the same name again
answers the same. A name already declared for somebody else in that memory
raises ConflictError (409 conflict). Declaring
the user_id itself as a name is a 422. Pass agent_id to declare the name
in one agent's memory of that end user. The CLI does the same with
korely init --name.
get_context
Returns a context block ready for a prompt: the known facts most relevant to the query, then the relevant memories, within a token budget. It is retrieval and formatting, not generation.
query: the current user turn or topic. In Node it goes inside the options object.token_budget: 800 by default, from 50 to 8,000.known_as_of: a date, such as"2026-06-01". The block then holds only what Korely had learned by that day, so you can check what an agent could have known when it decided.exclude_sessions=True: a block without the work sessions.
Besides context, tokens and sources (the ids of the facts and memories used), Context has:
stable,volatile,stable_hash:contextsplit in two.stableis the part that does not depend on the question, the same on every call;volatileis the rest;stable_hashchanges only whenstablechanges. Use them to cache a fixed prompt start; otherwise usecontext.degraded: true when part of the block could not be retrieved.degraded_partssays which:facts,memoriesor both.
Under heavy load the call waits up to 20 seconds, then can return a
partial block with degraded true; it still counts as a read.
Check degraded: when it is true, call again later for the
full block.
ctx = korely.get_context( "plan infra budget", user_id="customer-4812", token_budget=800,)
print(ctx.tokens) # 642print(ctx.sources) # ["fct_b91e", "mem_8f2c1a"]
messages = [ {"role": "system", "content": f"You are a helpful assistant.\n\n{ctx.context}"}, {"role": "user", "content": user_message},]get_profile
The open facts about one end user, the end user's own facts first, grouped
by family. Pass as_of for the profile on a past date,
known_as_of for the profile as Korely knew it then, and
exclude_sessions=True to leave out the facts only work
sessions state.
profile = korely.get_profile(user_id="customer-4812")print(profile.total) # 7print(list(profile.by_family)) # ["places", "work", "preferences"]
# The profile as it stood on March 1stpast = korely.get_profile(user_id="customer-4812", as_of="2026-03-01")users
The end users you have stored data for, each with counts of memories and
open facts. Page with limit and offset.
page = korely.users()print(page.total) # 1for u in page: print(u.user_id, u.memories, u.facts)list_agents
The agent_id values you have written under, each with counts
and last activity, plus your plan's agent cap and how many
are used. Call it when a write fails with
agent_cap_exceeded, to reuse an existing id.
page = korely.list_agents()print(page.used, "of", page.cap) # 2 of 2for a in page: print(a.agent_id, a.memories, a.facts)delete_agent
Deletes for good every memory and fact written under one
agent_id in the key's project. Its slot in your plan's agent
cap is freed (slot_freed) unless another project of the
account still uses the same name.
receipt = korely.delete_agent("staging-bot")print(receipt.memories_deleted, receipt.facts_deleted, receipt.audit_id)Audit, ping and account
audit: the audit log of the key's project, newest first.sinceanduntil: ISO text,datetimeordate.limit1 to 1,000, 100 by default. It counts against no monthly quota.iter_audit: every page ofaudit, to read the whole log. Withoutuntil, it pinsuntilwhen it starts.ping: checks the key. No permission and no monthly quota, but it counts toward the rate limit.delete_account: Cloud only, for an account made byinit_agent()orkorely init --agent. Deletes it with every key, memory and fact;confirm=Trueis required. An account that has a dashboard login raisesConflictError(account_has_login). On an account without a login, any key that can write closes the whole account: read the warning before you give such a key to code you do not trust.Korely.init_agent: Cloud only, a class method that needs no key. It opens an account on the free Hobby plan and returns its key, shown once (save it).agent_calleris an optional label, such as your app's name.processing_region,"eu"or"global", sets the new project's region,globalif left out. Node:Korely.initAgent(agentCaller, { processingRegion }).
Projects and keys (admin key)
The Python client also manages projects and keys:
projects, create_project,
update_project, archive_project,
unarchive_project, delete_project,
keys, create_key, revoke_key,
move_key. They need an
admin key: use it by hand, never in
a tool your agent can call. Projects and keys are managed from the
dashboard or with an admin key. The Node client does not have them.
Scoping
agent_id names your
app, user_id the end
user, and run_id one
session. A typical setup is one agent_id per product surface
and one user_id per customer. The number of
agent_id values is capped by your
plan; the number of end users is not.
Always pass user_id on reads in multi-tenant
products. A read without user_id covers every end
user the key can see in the project (narrowed to one
agent_id if you pass it). That suits an internal ops agent,
not a customer-facing chat.
Errors
Every error response carries {"code": "...", "message": "..."},
and the SDK raises it as an APIError with
.status, .code, .message and
.retry_after (Node: .retryAfter), the server's
Retry-After in seconds when it sends one. Python also keeps
the server's answer in .body. A KorelyError that
is not an APIError never got an answer: no key, a timeout, a
connection error. Subclasses by status:
AuthenticationError: 401.NotFoundError: 404.NamespaceForbiddenError: every 403 (theforbiddenrows below).ConflictError: 409, with the subclassStaleWriteErrorforstale_write.QuotaExceededError: 429, withlimit,usedandresets_at(Node:resetsAt) onquota_exceeded. Its subclassTooManyBatchesErroristoo_many_batches.
Branch on err.code:
| Status | code | When |
|---|---|---|
401 | invalid_key | Missing, malformed or revoked API key. Message: "Invalid or missing API key: ...", then what is wrong. |
403 | forbidden | The key lacks the permission the call needs. |
403 | forbidden | A confined key names an end user it is not confined to. |
403 | forbidden | A confined key writes with no user_id, and it is confined to more than one end user. |
403 | forbidden | A confined key calls delete_agent, which spans every end user. |
404 | not_found | The memory does not exist, or was hidden. Message: "Memory not found". |
408 | request_timeout | The body did not arrive whole in time: 30 seconds, or 120 seconds for a batch() body over 2,097,152 bytes. Nothing was done; send it again, or in smaller batches. |
409 | stale_write | update with an expected_updated_at that is not the current version (StaleWriteError). |
409 | fact_not_current | correct_fact on a closed fact. current_fact_id (Node: currentFactId) is the fact to correct instead, or None/undefined when nothing took its place. |
409 | account_has_login | delete_account on an account that has a dashboard login. |
409 | conflict | declare_name with a name already declared for somebody else in that memory. |
413 | payload_too_large | The body is over 2,097,152 bytes (a batch: 78,245,152). Nothing was done. On the Cloud a body over 1 MB (80 MB for a batch) gets a bare 413 first, without this code. |
422 | invalid_request | Validation failure, such as "content: Field required". |
429 | quota_exceeded | The monthly write quota (for writes) or query quota (for reads) is used up, including the 10% extra. No Retry-After. |
429 | rate_limit_exceeded | Too many requests in the current minute, hour or day. Retry-After in whole seconds. |
429 | agent_cap_exceeded | A new agent_id would pass your plan's agent cap. Carries limit and used. |
429 | too_many_batches | Cloud: three batch() imports of the account are still running. Nothing was stored; send it again when one finishes. It is not a monthly quota. |
500 | internal_error | The request failed on our side. |
503 | search_unavailable, model_unavailable, service_paused, unavailable | Search could not run, a model gave no usable answer, the service has paused model calls (service_paused, Retry-After: 3600), or the database was too slow (unavailable). Nothing was written; retry. |
from korely_memory import Korely, APIError
korely = Korely(api_key="kor_live_...")
try: memory = korely.get("mem_8f2c1a")except APIError as err: if err.code == "invalid_key": # 401: check the key, or create a new one in the dashboard raise elif err.code == "not_found": # 404: the memory does not exist, or an end user forgot it memory = None elif err.code == "quota_exceeded": # 429: monthly query quota reached (get() is a read), upgrade or wait for the reset memory = None else: raise
There is no overage billing: a plan stops at its limit. At 80% of the
month's writes you get a quota.warning webhook (none for queries). Past 100%
you can still write 10% more, so a busy day does not break your agent.
Past that, writes return 429 quota_exceeded
until the month rolls over at resets_at, you buy extra
writes for the month on a paid plan (a
top-up), or you upgrade. Your bill is the plan price plus any top-up.
End-to-end example
A support bot that remembers each customer across sessions. On every turn
it reads a context block, calls the model, then stores what the user said:
get_context, the model call, add.
The model here is Google's, so install its client next to Korely's, and
set two keys: KORELY_API_KEY (your Korely key from
Quickstart) and GOOGLE_API_KEY
(a key from Google). The Node version is TypeScript and, like the Quick
start, needs "type": "module" in package.json.
pip install korely-memory google-genai"""Support bot with persistent per-customer memory.Uses korely-memory for context recall and fact storage."""import osimport timefrom google import genaifrom korely_memory import Korely, APIError
korely = Korely() # reads KORELY_API_KEYclient = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
AGENT = "support-bot"
def chat(user_id: str, user_message: str) -> str: # 1. Assemble memory context for this user + query ctx = korely.get_context( user_message, agent_id=AGENT, user_id=user_id, token_budget=600, )
system = ( "You are a concise support assistant. " "Use the memory context below to personalise your reply.\n\n" + ctx.context )
# 2. Call the LLM reply = client.models.generate_content( model="gemini-3.6-flash", contents=system + "\n\nUser: " + user_message, ).text
# 3. Persist what the user said (extracts facts, detects contradictions) try: korely.add( user_message, agent_id=AGENT, user_id=user_id, metadata={"turn": "user"}, ) except APIError as err: if err.code != "quota_exceeded": raise pass # over the write quota, log and continue, the reply was already generated
return reply
# --- run a two-turn session ---uid = "customer-4812"
print(chat(uid, "Hi, I am on the Developer plan and I prefer async updates."))# context is empty on first turn, the bot greets + confirms
time.sleep(20) # let extraction write the facts of turn 1
print(chat(uid, "What plan am I on again?"))# the context block now contains the plan + preference facts from turn 1
On turn 1, add extracts facts such as the customer's plan and
preference. On turn 2, get_context puts them in the system
prompt before the model sees the question.
Details
- Extraction time. Usually about ten seconds; a longer text takes longer. A memory can end
readywith no facts. Polling has a cost: eachget_factsoreventscall is a read, counted as a query and toward the rate limit. - Changes in the present tense. Write a change as the new state, such as "now lives in Berlin": Korely does not always recognize a change told as a past event.
- No model on reads. Your agent's model does the reasoning. The only model call on a read is the embedding of a search query, on our own server. Models run on writes, so read quotas are a hundred times or more the write quotas.
- ping() and regions. The client's only
regioniseu,https://api.korely.ai.ping()returnsregion, a label such aseu-hel1, andprocessing_region, the project's region. A new project isglobal(Regions);init_agent(processing_region=...)sets it at signup. - Versions. Check yours with
pip show korely-memoryornpm ls korely-memory.kind,exclude_sessionsanddeclare_name: Python 0.1.26, Node 0.1.16.Memory.kindandkindon a batch item: Python 0.1.27, Node 0.1.17.known_as_of: Python 0.1.22, Node 0.1.14.degradedand the otherContextfields aftersources: Python 0.1.18.limit,used,resets_aton a quota error: Python 0.1.20, Node 0.1.12. - Self-hosted. An install older than Korely 0.1.42 refuses
kindwith a422and sends nokindback, soMemory.kindis empty (Nonein Python, absent in Node). Embeddings, extraction and contradiction checks run on the server (ours on the Cloud, yours on Self-hosted), so the SDK is a thin HTTP client with no heavy dependencies. - Migrating? See the migration guide.
Related
| Topic | Where to go |
|---|---|
| Full REST contract | API reference, the source of truth. |
| CLI | CLI reference: the same memory from the terminal. |
| MCP | MCP reference: connect Claude, Cursor or any MCP client. |
| Facts over time | Temporal facts: how contradictions close facts and what as_of returns. |
| Chatbot | Cookbook: chatbot that remembers, a fuller version of the end-to-end example. |
| Bulk import | Batch import (API reference): progress tracking and per-item errors. |
| Surfaces | When to use SDK, CLI or REST. |
| Pricing and quotas | Pricing: hobby (free), developer (€19/mo + VAT), team (€79/mo + VAT), scale (€249/mo + VAT). Writes and queries count; end users are always unlimited. |