Core operations
Add a memory
Store a text or a chat thread about one of your users. Korely saves it at once, then extracts facts from it.
add() saves a memory and
replies with status processing. A language model then reads
the text and extracts facts. For example: subject
Northwind Hosting, predicate costs, object 50 euro
per month. This usually takes about ten seconds.
Quick start
You need Python 3.9 or later, pip install korely-memory (the
Python SDK), and an
API key (kor_live_...).
Get a free API key (Quickstart).
The code writes one memory. After a wait, it reads the memory's status and its facts.
from korely_memory import Korelykorely = Korely(api_key="kor_live_...")memory = korely.add("Northwind Hosting costs 50 euro per month since the June upgrade.", user_id="customer-4812")# In a Python console, run the next two lines about ten seconds later:print(korely.get(memory.id).status) # readyprint(korely.get_facts(entity="Northwind Hosting", user_id="customer-4812")) entity is the subject you ask about
(Glossary). You see ready and a list with one fact: predicate
costs, object 50 euro per month. If the status is
still processing, wait and run the last two lines again. If it
is ready and the list is empty, the memory finished with no
facts. If it is error, see status under
Response.
Call add() after any turn where the user reveals something
worth remembering: a preference, a decision, a fact about themselves.
Terms
The Glossary defines the words used across the docs. This page adds three.
- Shared memory: a memory written with no
user_id. A read that passes auser_iddoes not find it. - Top-up: extra writes that a paid plan buys for the current month.
- Confined key rule: a confined key writes only for its own end users, and names one with
user_id. Exception: a key confined to one end user, such ascustomer-4812, may leaveuser_idout; the write goes to that end user.
Reference
The Quick start is enough for a first try. Read on when you build your integration: parameters, response, errors and limits.
What happens on a write
flowchart LR
A([add content]) --> B[Store memory]
B --> C[Embed memory]
C --> I([Reply: status processing])
C -.-> E[Extract facts]
E --> N[Entities: subjects, named objects]
E --> F{Contradiction?}
F -- yes --> G[Supersede old fact]
F -- no --> H[Record new fact]
The subject of each fact is an entity, and so is an object that names
something (a company, a person, a city) rather than a value such as
50 euro per month. With entities you retrieve everything known
about one person, company or product:
get_facts(entity="Northwind Hosting")
(The graph).
A filter then picks earlier facts that might conflict with each new fact, and a model judges them (contradiction). If the price goes from 50 to 80 euro, the 50 euro fact is superseded (closed).
Each API key belongs to a project.
Each project has a region
(global by default, or eu), chosen on the
Projects page of the dashboard; the extraction quality can differ between
regions, see Regions.
Request
Endpoint: POST /v1/memories, which answers 201 on success. SDK: korely.add(content, *, user_id=None, agent_id=None, run_id=None, metadata=None, timestamp=None, kind=None).
| Param | Type | Notes |
|---|---|---|
content | string | Required. Plain text or Markdown, max 16,000 characters (up to 3 writes, see Notes). The REST body takes a string. The Python and Node SDKs also accept a list of chat messages ([{role, content}]): each message becomes a line role: text, empty messages are dropped, and the lines are sent as one text. |
user_id | string | One of user_id and agent_id is required: with neither, the write is a 422 identity_required (see Every write says whose memory it is). The end user the memory belongs to (e.g. "customer-4812"); the id is sent to the model with the text, so prefer an opaque id to a name or an email address. End users are unlimited on every plan. With an agent_id and no user_id the memory is shared (see Terms; id rules under Details). |
agent_id | string | The app or agent that writes (e.g. "infra-bot", see agent_id); same rules as user_id, and required when there is no user_id. Each name counts toward your plan's agent limit once per project, so the same name in two projects counts as two agents of the account. With no agent_id, nothing counts toward it. A new name past the limit returns 429 agent_cap_exceeded. Since 1.0.12, the model that extracts the facts treats a memory with an agent_id and no user_id as the agent's own memory, not as a person's: see who the memory is about. If it is about one person, pass that person's user_id. |
run_id | string | Optional, max 255 characters. One agent run or chat session (run_id). |
metadata | object | Optional. Your own labels (metadata), returned verbatim on read and ignored by the pipeline. The limit is 8,192 bytes of JSON, counted as the server writes it (with spaces after colons and commas), so a compact payload of exactly 8,192 bytes can be refused. At most 32 levels deep; NaN and Infinity are refused. |
timestamp | string | Optional. ISO 8601 date or datetime when the events happened, for loading old data: the facts take it as valid_from. A value that is not ISO 8601, or falls outside 0001-01-02 to 9999-12-30 UTC, returns 422. An empty value counts as absent; a time later than now is read as now. |
kind | string | Optional (kind). note (the default) for what somebody said or decided, session for the log of a work session (see Work sessions). |
Any other field is refused with 422 invalid_request (for example "userId is not a field here. Did you mean user_id?"): field names are snake_case.
Example
The full example adds metadata and an agent, then waits for the facts. Its
agent_id="infra-bot" takes one of your plan's agents (the free
Hobby plan has 2). Node needs version 18 or later and npm install korely-memory.
import timefrom korely_memory import Korely
korely = Korely(api_key="kor_live_...")
result = korely.add( "Northwind Hosting costs 50 euro per month since the June upgrade.", user_id="customer-4812", agent_id="infra-bot", metadata={"source": "slack", "channel": "#billing"},)print(result.id) # mem_8f2c1a4d9b3e47a6b05e2c7d1f3a9e84print(result.status) # processing: the facts are extracted after the reply
# Check the status every 2 seconds, for at most one minutestatus = result.statusfor _ in range(30): if status != "processing": break time.sleep(2) status = korely.get(result.id).statusprint(status) # ready (or error)
facts = korely.get_facts(entity="Northwind Hosting", user_id="customer-4812")for fact in facts: print(fact.predicate, fact.object) # costs 50 euro per month
Over REST, read the status with GET /v1/memories/{id}.
If the loop ends on processing, read the status again later.
Response
The reply of the cURL example, with HTTP status 201:
{ "id": "mem_8f2c1a4d9b3e47a6b05e2c7d1f3a9e84", "content": "Northwind Hosting costs 50 euro per month since the June upgrade.", "user_id": "customer-4812", "agent_id": "infra-bot", "run_id": null, "client": null, "kind": "note", "metadata": { "source": "slack", "channel": "#billing" }, "status": "processing", "created_at": "2026-06-15T09:14:00.412345+00:00", "updated_at": null, "facts": []}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Unique memory ID (prefix mem_), to retrieve, update, or delete the memory later. |
content | string | The text that was stored, with leading and trailing whitespace removed. |
user_id, agent_id, run_id, kind, metadata | as in the request | As passed. When omitted: null, "note" for kind, an empty object for metadata. |
status | string | processing on the immediate reply; ready once extraction has run. error means extraction failed five times: it is final, Korely stops trying, and the memory has used its writes. To try again, call add() with the same content; that creates a second memory and uses writes again. Remove the failed one with DELETE /v1/memories/{id} to avoid a duplicate. |
created_at, updated_at | ISO 8601 string or null | UTC time the memory was first stored; updated_at is null until the memory is updated, then the time of the last edit. |
client | string or null | The tool that wrote the memory (see Details). |
facts | array | Always empty in this reply on the Cloud (api.korely.ai): extraction runs after it. The rows below describe a fact as you read it back with get_facts. |
facts[].id | string | Unique fact ID (prefix fct_). Appears in the sources of get_context(). |
facts[].subject | string | The entity this fact is about (a product, a person, an organisation). |
facts[].predicate | string | The relation, as a standard verb (e.g. costs, works_at, likes). |
facts[].object | string | The value of the relation (e.g. 50 euro per month), or the entity it points to. |
facts[].valid_from | ISO 8601 string or null | When the fact became true in the world (UTC; valid time). How Korely sets it: Details. |
facts[].tense | string | current, past or planned, as the text said it. |
facts[].invalid_at | ISO 8601 string or null | Set when the fact has ended: because the text states it as finished (a past fact) or because a later fact superseded it. null while the fact is still true. |
facts[].observation_count | integer | How many memories state the fact. Above 1 when a write restated a fact Korely already held. |
Errors
Every error carries code (stable, shown below: branch on it)
and message (for a person). detail is kept for
older clients; on a 422 it lists the fields that failed.
| Status | Code string | When it occurs |
|---|---|---|
401 | invalid_key | The Authorization header is missing, malformed, or the key has been revoked. |
403 | forbidden | The key lacks the memories:write permission, or it is a confined key (rule under Terms) and the write is not for one of its end users. |
422 | identity_required | The write names neither user_id nor agent_id, or only ids made of spaces, invisible characters or combining marks; a run_id alone is not enough. Nothing is stored and nothing counts against your quota. Add one of the two ids: why and how. |
422 | invalid_request | Validation failed: content missing, not a string, blank or over 16,000 chars; user_id or agent_id breaking the id rule; run_id over 255; metadata too large, deeper than 32 levels, or with NaN or Infinity; a NUL character in any field; kind other than note or session; timestamp not ISO 8601 or out of range; malformed JSON; an unknown field. The message names the offending field (e.g. "content: Field required"). |
429 | agent_cap_exceeded | The agent_id is a new name past your plan's agent limit (2 on Hobby, 10 on Developer, 100 on Team, 500 on Scale). Nothing is written or counted. Reuse an existing agent_id or upgrade. detail carries limit and used. No Retry-After. |
429 | quota_exceeded | This write would take your account past its monthly writes plus the 10% margin; nothing is written (see Details). detail carries limit, used and resets_at (00:00 UTC on the 1st). No Retry-After: read resets_at. |
429 | rate_limit_exceeded | Over the rate limit: too many requests in the current minute, hour or day (on Hobby: 60 per minute, 1,000 per hour, 10,000 per day). Carries a Retry-After header in seconds. |
Notes
- Calling twice writes twice. Two identical calls store two memories; their facts are counted once more (
observation_count2), not duplicated. - Write quota. Each
add()counts one write per 6,000 characters of content, rounded up, so at most 3. The quota resets on the 1st of the month, UTC.
Details
- Shared memory.
korely.search("office hours")finds it;korely.search("office hours", user_id="customer-4812")does not. Leaving outuser_idoragent_idon a read does not filter by it. - Id rules.
user_idandagent_id: 1 to 255 characters; not empty, not.or.., no/or control characters, not starting witherased:, which Korely reserves for the erased end users in its records. client. From theX-Korely-Clientrequest header.nullwith cURL and no header; the SDKs always send it.valid_from. Korely takes the first of these that applies:- the request's
timestamp; - a date the text states with its year, never a future one: "since March 2026" gives 2026-03-01;
- a day the text says it happened ("yesterday") gives that day;
- the time of the write.
- the request's
- Reading facts back.
korely.get(id)shows only the facts true now;get_facts(..., include_invalidated=True)also shows the superseded ones.predicate_raw: the verb as the text wrote it.invalidated_by: the fact that closed this one, often set but not always.predicate_family: one ofpreferences,people,places,work,ownership,health,financial,events,identity,other;get_factscan filter on it. - Quota. Korely refuses a write once it would pass the plan's monthly writes plus 10%. A long memory counts up to 3 writes, so it can be refused while one or two writes are still free. Top-ups add writes (see Terms).
- Contradiction candidates. For a memory with a
user_id: earlier facts of the same end user in the project, from any agent, that match the new fact in one of these ways:- same subject and relation;
- same subject, compatible object, another relation;
- the same two entities, reversed;
- the same relation, about any subject;
- the closest in meaning.
user_id(an agent's own memory), the candidates are only the facts of the new fact's own entity: facts with the same subject, facts whose object is the new fact's subject, and facts whose subject is the new fact's object. A fact of another entity is not shown to the judge because it is near in meaning. See Temporal facts for the cost of that. - Memories in
error. List them withGET /v1/events?status=error(korely.events(status="error")).
Self-hosted
A Self-hosted server whose setting WRITE_ASYNC is false
extracts before it answers and returns the facts in the reply, each with
invalidated: the IDs of the earlier facts this write
superseded. If the embedding step (see Search) gives no vector, a background worker
computes it a moment later.
Related
- Get context: current facts plus relevant memories in one block.
- API reference for every
/v1endpoint.