Korely

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 Korely
korely = 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) # ready
print(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 a user_id does 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 as customer-4812, may leave user_id out; 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 write pipeline of add(). The steps after the dotted arrow run after the reply, so the first reply carries no facts. Embed memory turns the text into numbers for search.

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).

ParamTypeNotes
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 Optional but recommended. 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. Without it the memory is shared (see Terms; id rules under Details).
agent_id string Optional. The app or agent that writes (e.g. "infra-bot", see agent_id); same rules as user_id. Each new name counts toward your plan's agent limit, for the whole 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 time
from 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_8f2c1a4d9b3e47a6b05e2c7d1f3a9e84
print(result.status) # processing: the facts are extracted after the reply
# Check the status every 2 seconds, for at most one minute
status = result.status
for _ in range(30):
if status != "processing":
break
time.sleep(2)
status = korely.get(result.id).status
print(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

FieldTypeDescription
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.

StatusCode stringWhen 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 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_count 2), 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 out user_id or agent_id on a read does not filter by it.
  • Id rules. user_id and agent_id: 1 to 255 characters; not empty, not . or .., no / or control characters, not starting with erased:, which Korely reserves for the erased end users in its records.
  • client. From the X-Korely-Client request header. null with cURL and no header; the SDKs always send it.
  • valid_from. Korely takes the first of these that applies:
    1. the request's timestamp;
    2. a date the text states with its year, never a future one: "since March 2026" gives 2026-03-01;
    3. a day the text says it happened ("yesterday") gives that day;
    4. the time of the write.
  • 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 of preferences, people, places, work, ownership, health, financial, events, identity, other; get_facts can 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. Earlier facts of the same end user in the project, from any agent, that match the new fact in one of these ways:
    1. same subject and relation;
    2. same subject, compatible object, another relation;
    3. the same two entities, reversed;
    4. the same relation, about any subject;
    5. the closest in meaning.
    Since 1.0.12, a fact written by hand (Write a fact) is a candidate in 4 and 5 only when it has the same subject.
  • Memories in error. List them with GET /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