Korely

Platform

Data & governance

Where your agents' memory is stored, which models process it, and how to erase it.

This page describes Korely Cloud. On every plan, the database is on our server in Helsinki, on an encrypted volume. Nightly backups are encrypted on that server before upload and kept in a Cloudflare R2 bucket under the EU jurisdiction. To write facts, a model reads each memory you write. In the global region, the starting one, that model comes from Google and processes the text outside the EU. The eu region uses Scaleway in Paris and keeps this step in the EU. On Self-hosted, the data stays where you install Korely, and you choose the model provider.

You control the rest. You choose each project's region and give each key only the permissions it needs. You list your end users and erase one end user in one call. Korely does not use your data to train or improve any model.

Quick check

GET /v1/ping shows where a key's memories are processed. Replace kor_live_... with your API key:

Terminal window
curl https://api.korely.ai/v1/ping \
-H "Authorization: Bearer kor_live_..."
# {"ok":true,"tier":"hobby","region":"eu-hel1","processing_region":"global","scopes":["memories:read","memories:write"]}

region is the install's label, eu-hel1 for the Helsinki server of the Cloud. processing_region is the region of the key's project. scopes lists the key's permissions. Your values depend on your account and key.

Terms

The Glossary defines the words used across the docs. These are the ones this page relies on.

  • Sub-processor: a company that processes your data for Korely, such as a model provider. All of them are on the sub-processors page.
  • Namespace: the slice of data a call reads or writes, set by the key's project and the ids you pass (see Namespaces).
  • Receipt: the answer of an erasure, with the counts and an audit_id, the receipt's id. No /v1 route reads it back by id.
  • Pseudonym: the keyed stand-in, starting with erased:, that replaces an erased user_id in the records that stay. It is still personal data, not anonymous.

Reference

Where data is stored and processed

WhatWhere, on the Cloud
Memories, facts, embeddings, graphOur server in Helsinki, encrypted volume
BackupsCloudflare R2 bucket under the EU jurisdiction, encrypted before upload, up to 35 days
Embeddings of memories, facts and queriesEmbeddingGemma, an open model, on our server in Helsinki
Language model, globalA model from Google (the Gemini API), which processes the text outside the EU
Language model, eugpt-oss-120b on Scaleway, Paris, France

The current provider of each region is named on the sub-processors page and in the Privacy Policy. In either region, the model receives text:

  • the memory you write, with the end user's identifier and up to ten earlier memories of the same end user (300 characters each), to extract its facts;
  • the earlier facts a new one is compared with, to check for a contradiction;
  • a relation phrase new to that end user's vocabulary, with a value it was stated with and examples of known relations;
  • the subject, relation and object of a fact you write directly or correct.

What reaches the model: the text of each memory and the id of the end user (the user_id you chose). Use an opaque id such as customer-4812 rather than an email address or a name, and do not put secrets in a memory.

A search or context query never reaches a language model. The embeddings are computed on our server in Helsinki, not by a provider.

Korely does not use your data to train or improve any model, and no setting turns it on. The providers' own terms are summarised on the sub-processors page and in the Privacy Policy. You keep all rights in your content and in what Korely derives from it (see the Terms of Service).

For the global region only: the Korely Cloud sends the text to Google's Gemini API on a paid-services account. Under Google's paid-services terms, Google does not use those prompts and responses to improve its products. The same terms say Google logs them for a limited period, solely to detect and prevent violations of its Prohibited Use Policy. The eu region uses a different provider with its own terms, listed on the sub-processors page.

Regions

Each project has a region. You choose it on the Projects page of the dashboard, when you create a project or later. An account made with POST /v1/agents/init picks the region of its Default project at signup, with korely init --agent --processing-region eu. The account can change it later, once you connect its key to a login.

  • A new project is created in global unless you choose eu, the Default project included. A project created before regions existed may show Service default (no region stored) on the Projects page: set its region there.
  • In eu, when the EU model is unavailable, new memories are stored at once and their facts follow when it is back. The model of the global region never reads them to extract their facts.
  • A change of region applies to the next writes, including memories still waiting for their facts. Facts already written are not redone.
  • After a change, the next write can include as context up to ten earlier memories of the same end user (300 characters each). Some can come from the other region. So text written in eu can reach the global provider after a move.
  • The region says where your memories are processed by a language model, not where they are stored. On Self-hosted, region in GET /v1/ping is the REGION value in .env, which the service does not check.

API keys and permissions

Requests authenticate with a kor_live_ key in the Authorization: Bearer header. Korely stores only a SHA-256 hash of the key. The full key is shown once, at creation, and cannot be retrieved. Keep it in the KORELY_API_KEY environment variable, which the SDK reads, and never commit it to source control.

If a key is lost, revoke it in the dashboard and create a new one (to replace a key that is not lost, rotate it). From the next request, the old key is refused on every surface (REST, SDK, CLI, MCP). Revoking deletes no data, and work already accepted, such as memories waiting for their facts, still completes.

A key reads and writes unless you choose otherwise. On the API Keys page, under What it may do in Create API key, tick only one permission:

PermissionOpensTypical use
memories:readSearch, context, lists and the audit logA frontend or a support tool
memories:writeAdd, change, forget and erase memories and facts, erase an end user, delete an agent namespace, batch importsAn import pipeline that never reads back

The key list then shows a Read only or Write only badge. A call outside the key's permissions reads and writes nothing: 403 forbidden on REST, an error: forbidden, ... message from the MCP tool. A key keeps the permissions it was made with. For different ones, make a new key and revoke the old.

Key lifetime and rotation (since 1.0.23)

How long a key lasts. On the Cloud a key lasts at most 365 days, and none lasts for ever: POST /admin/keys with expires_in_days over 365, or null, answers 422 and names the ceiling, and the dashboard offers no "Never". Keys made before 1.0.23 keep the dates they have. The key that an account made without a login starts with gets 365 days from the moment the account is connected to a dashboard login. An install of your own has a ceiling of 3650 days.

Every answer says when. Each answer to a key that expires carries the header Korely-Key-Expires-At, an ISO 8601 time in UTC to the second. It is on the REST API, on /admin and on the MCP door, and also on a refusal by the route after the key was accepted; a 500 from an unexpected failure does not carry it. A key that never expires, and the key of a connected app, send none. Read it in your code, and rotate before it. The webhooks key.expiring and key.expired tell the same.

Rotate a key in one call. POST /admin/keys/{key_id}/rotate, with an admin key, gives the same key (its id, name, project, permissions and end users) a new secret. The new secret is in that answer only, so save it. The old secret goes on working for old_key_expires_in_hours more (24 by default, at most 168, never past the key's own end; 0 ends it at once), so the agents that hold it have time to take the new one. old_secret_expires_at says until when, in the answer and in GET /admin/keys; it is null once the overlap has ended or the key is revoked. Up to 1.0.22 you rotated a key by making another and revoking the first.

Terminal window
curl -X POST "https://api.korely.ai/admin/keys/<key id>/rotate" \
-H "Authorization: Bearer kor_live_..." \
-H "Content-Type: application/json" \
-d '{"old_key_expires_in_hours": 48}'
  • expires_in_days works as when you make a key. Left out, the new secret keeps the lifetime the current one was given, counted from now, and never more than a key made today may last. On the Cloud, a key with no end, or one longer than 365 days, gets 365.
  • Rotating again ends the earlier overlap. Revoking the key ends both secrets. Two rotations at once take turns.
  • 409 key_not_rotatable for a key that is revoked, expired, or a connected app's. 404 for a key that is not this account's.
  • An account still on trial cannot rotate: 403 account_not_adopted, as for a new key.

Namespaces

Every /v1 call is scoped by your account and the key's project first, then by the parameters you pass:

  • user_id, your end user, the unit of isolation. Choose any opaque string (a database ID, a hashed email, a UUID) of 1 to 255 characters. It cannot hold / or control characters, be . or .., or begin with erased:. A write with another value gets 422 invalid_request. A read with the erased: prefix returns 200 and an empty list.
  • agent_id, one of your apps or agents. Plans cap the distinct agent IDs across all projects of the account (Hobby: 2, Developer: 10, Team: 100, Scale: 500). A write with a new agent_id past the cap returns 429 agent_cap_exceeded and writes nothing.
  • run_id, a single session or conversation. Search and list can filter on it. Facts are not scoped by run.

The project wall is enforced server-side on every /v1 query. The one exception is a key with the admin scope on the /admin API (see Projects). Within a project, pass user_id to keep a read to one end user, or give a frontend a confined key. A read with no user_id covers every end user of the project.

GET /v1/users lists each user_id Korely holds anything of, until you erase it. Pages hold 50 by default, up to 200 with limit; use offset for the next. total is the full count. An end user whose memories are all forgotten is listed too, with zero counts and no last_active, because the text is still stored (since 1.0.23; up to 1.0.22 such an end user was left out). Memories with no user_id are not listed.

Terminal window
# List the end users of the key's project, first page, with memory and fact counts
curl https://api.korely.ai/v1/users \
-H "Authorization: Bearer kor_live_..."
# {"users": [
# {"user_id": "customer-4812", "memories": 18, "facts": 9, "last_active": "2026-06-15T..."},
# {"user_id": "customer-7091", "memories": 4, "facts": 2, "last_active": "2026-06-14T..."}
# ], "total": 2}

Erasure

Erase one end user in the key's project in a single call: every memory and every fact in the live database. Repeat it in each project that holds the end user.

Terminal window
curl -X DELETE "https://api.korely.ai/v1/users/customer-4812/memories" \
-H "Authorization: Bearer kor_live_..."
# 200 OK
# {"user_id":"customer-4812","memories_deleted":18,"facts_deleted":9,
# "erasure":"permanent","audit_id":"aud_..."}

The call returns a receipt: memories_deleted, facts_deleted, erasure: "permanent" and an audit_id. Every memory and every fact about the end user, closed ones included, is physically deleted. No query surface (REST, SDK, CLI or MCP) and no include_invalidated flag can bring it back.

The erasure also deletes the end user's declared names in the key's project, their learned vocabulary and their batch items. It does not delete memories saved without a user_id.

What stays is a record of the erasure: the counts and a pseudonym of the user_id, never the id or the content. The same pseudonym marks the erasure in the audit log. GET /v1/audit?user_id= with the same id finds that line for 400 days, for your own accountability records.

An unknown or already erased end user returns 200 with zero counts. If you know which person each user_id is, this call is the Korely side of an erasure request. It needs no support ticket, and the live data is gone when the call returns.

DELETE /v1/memories/{id} forgets one memory. It is not an erasure: the memory leaves every read, but its row stays. Add ?erase=true to delete the row, its text and vector, and the facts only it stated (see Delete a memory).

What stays, and other deletions

  • Backups: an encrypted copy stays for up to 35 days. For the first 30 days nobody can delete it (see the Privacy Policy). Once a month an operator restores a backup on his own computer in Italy, inside a throwaway container, decrypted in memory, to check that backups work.
  • Records: the audit log keeps its lines for up to 400 days. The erasure record, with the counts and the pseudonym, stays until the account closes (Privacy Policy, section 9).
  • Read marks (since 1.0.15): when an agent gives its name as reader, Korely keeps which facts the read handed over, by id, with the reader's name, the end user the fact is about and the time of the first and last read, never any text. They go 90 days after the last read, with their fact, project or account, and with the end user's erasure. See Agent notices.
  • Model providers: Korely's erasure does not reach the copies they keep, for the periods on the sub-processors page.
  • Keys: revoking a key deletes no data.
  • Projects: deleting a project without ?erase=true moves its data to the Default project (see Projects).
  • Agents: DELETE /v1/agents/{agent_id} deletes an agent namespace (see Delete an agent).
  • Account: close it from the dashboard, or with DELETE /v1/account for an account made with POST /v1/agents/init (see Delete account, which erases everything with an unconfined admin key of that account, since 1.0.5).
  • Export: there is no single export file. Read the data with GET /v1/memories and GET /v1/facts.

Quotas and rate limits

Plans carry monthly write and query quotas (see pricing), each with a 10% grace on top. Past the plan's figure plus the grace, a write or a read returns 429 quota_exceeded and nothing is written. The call is refused, never billed.

On a paid plan you can buy a top-up of writes for the month on the dashboard's Usage page, or upgrade at any time. The quota 429 has no Retry-After header. Its detail gives limit (top-ups included), used and resets_at (00:00 UTC on the 1st). A Hobby account past 250 writes and the grace gets:

{
"detail": {
"code": "quota_exceeded",
"message": "Monthly write limit reached: 250 writes on the Hobby plan this month, plus a 10% grace. It starts again on <first day of next month>. ...",
"limit": 250,
"used": 275,
"resets_at": "<first day of next month>T00:00:00Z"
},
"code": "quota_exceeded",
"message": "<the same text>"
}

A burst past the per-minute, per-hour or per-day rate limit gets a 429 with a Retry-After header, in whole seconds.

Details

  • Errors. Every error carries code, a stable slug to branch on, and message at the top level, next to detail. An invalid or missing key returns 401 invalid_key. The full list is in the API Reference.

Related