Batch & status
Batch import
Bulk-import up to 500 memories in one async call. Korely enqueues the job and returns a job id immediately; each item is imported later through the same pipeline as a single add. On the Hobby plan a batch can hold fewer items than that (see Notes).
/v1/batch
Use this when you're backfilling history or seeding a namespace and don't
want to fire hundreds of individual POST /v1/memories calls. The
request returns 202 Accepted with a job id; a background worker
imports each item, computing its embedding, extracting its facts (extraction)
and checking them for contradictions, exactly as a single add would. Poll
GET /v1/batch/{job_id} for progress.
Authentication
HTTP header, required: Authorization: Bearer kor_live_..., your API key. The key must carry the memories:write scope.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
memories | array<object> | Required | The memory objects to import. 1-500 items. Each element is a BatchMemory (see fields below). |
Each BatchMemory element has these fields:
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Required | The memory text. 1-16,000 characters. |
user_id | string · null | Optional | The end-user namespace this memory belongs to. Default null. 1 to 255 characters, no /, no control characters, not . or .., not starting with erased:; an empty string is refused, omit the field if the memory belongs to no particular end user. |
agent_id | string · null | Optional | Your application's namespace (glossary). Default null. Same rule as user_id. |
run_id | string · null | Optional | One session or conversation (glossary). Default null. 1 to 255 characters; an empty string is refused, omit the field instead. |
metadata | object · null | Optional | Arbitrary JSON stored alongside the memory and echoed back on reads (glossary). At most 8,192 bytes of UTF-8, counted on the JSON as the server writes it, spaces included, with finite numbers only. An item over the bound, or holding NaN or Infinity, refuses the whole batch with 422 before anything is queued. Default null. |
timestamp | string · null | Optional | ISO 8601 date or datetime when the events happened. The facts extracted from the item take it as valid_from (valid time), so a migration keeps its real dates (a timestamp later than now is read as now: for a fact that begins later, write it with POST /v1/facts and its own valid_from); without it the item is dated when it is imported. A value that is not ISO 8601 refuses the whole batch with 422, naming the item (e.g. memories[3].timestamp). Default null. |
kind | string | Optional | Set per item (glossary): note (the default), what somebody said or decided, or session, the log of a work session. Reads with exclude_sessions leave the sessions out, as for Add a memory. The SDKs send it on a batch item from Python 0.1.27 and Node 0.1.17 (see Work sessions in a batch). |
Example request
curl -X POST https://api.korely.ai/v1/batch \ -H "Authorization: Bearer kor_live_..." \ -H "Content-Type: application/json" \ -d '{ "memories": [ { "content": "User prefers email follow-ups, not phone", "user_id": "customer-giulia-4812", "agent_id": "support-bot" }, { "content": "Account is on the EU data region", "user_id": "customer-giulia-4812", "agent_id": "support-bot" }, { "content": "Renewal date is 2026-09-01", "user_id": "customer-giulia-4812", "agent_id": "support-bot", "metadata": { "source": "crm" } } ] }'Work sessions in a batch
from korely_memory import Korely
korely = Korely() # reads KORELY_API_KEYjob = korely.batch([ {"content": "Session: added retries to the invoice sender.", "user_id": "dev-team", "kind": "session"}, {"content": "Invoices go out on the first working day.", "user_id": "dev-team"},])print(job.received) # 2
An item without kind is a note. Each item is imported with
its own kind, so a migration can bring session logs and decisions in one
batch. Python 0.1.27 and Node 0.1.17 or later.
Response
202 Accepted. The job has been enqueued; nothing is imported
yet. Poll GET /v1/batch/{job_id} to track progress.
{ "id": "job_8f2c1aab4d7e4f0c9a1b2c3d4e5f6a7b", "status": "pending", "received": 3}| Field | Type | Description |
|---|---|---|
id | string | Opaque batch job id, prefix job_ followed by the UUID hex, e.g. job_8f2c1aab4d7e4f0c9a1b2c3d4e5f6a7b. Use it to poll GET /v1/batch/{job_id}. |
status | string | The job's stored status: pending when the POST answers (queued, no worker on it yet), then processing, completed or failed, as GET /v1/batch/{job_id} reports. |
received | integer | Number of memory objects accepted into the job, equals the length of the memories array you sent. |
Errors
| Status | Code | Cause |
|---|---|---|
401 | invalid_key | Missing or invalid API key. No Bearer credential was supplied, or the kor_live_ key does not resolve to a live key. |
403 | forbidden | The API key lacks the memories:write scope, or it is confined to some end users and a user_id is outside its list (or no user_id is given and the key does not name exactly one end user to default to). One such row refuses the whole batch. |
408 | request_timeout | The body did not arrive whole within 30 seconds, or within 120 seconds when it is larger than 2 MiB. Nothing is stored; send it again, or in smaller batches. An account sends one body larger than 2 MiB at a time: a second one waits for the first. |
413 | payload_too_large | The body is larger than 78,245,152 bytes (about 75 MiB), the most this route takes from a key that may write; from any other caller the limit is 2,097,152 bytes. Refused before it is read; nothing is stored. A body that holds more data than 500 valid items could (far more objects, strings or characters) is refused with 413 too; a valid batch never is. |
400 | bad_request | The body is not well formed, for example JSON that is not UTF-8. Nothing is stored. |
422 | invalid_request | Request validation failed: memories is empty or has more than 500 items, a content is empty or over 16,000 chars, a user_id or agent_id breaks the id rule, a run_id is empty or exceeds 255 chars, a timestamp cannot be read, a kind is other than note or session, or a field is unknown (e.g. userId). One bad item refuses the whole batch. |
429 | rate_limit_exceeded | Per-minute, per-hour or per-day request limit. Comes back with a Retry-After header. |
429 | quota_exceeded | The batch's writes do not fit in this month's writes left, with a 10% allowance on top of the plan's number (batches already queued count as used). Nothing is stored. No Retry-After. On Hobby this happens to any batch that needs more than 275 writes, even in a fresh month. |
429 | agent_cap_exceeded | The batch's new agent_id names would take the account past its plan's named agents. Nothing is stored. detail carries limit and used. |
500 | internal_error | The server failed on this request; the message holds no detail. |
503 | unavailable | The database did not answer, or the request waited more than 5 seconds for another change to the same records, or the server is reading as many large bodies as it can at once. Nothing is stored; retry after the Retry-After header. |
Notes
- It's asynchronous. The POST only enqueues the job and returns immediately (
202). A background worker imports each item later through the same steps asPOST /v1/memories: the memory is stored, then its facts are extracted. - Each item counts its writes. One write per 6,000 characters of its text, as an add: an item up to 6,000 characters is one write.
- Counted once, at POST time. The batch's writes and its new agent names are counted when the job is queued; a batch that does not fit the writes left this month, or would pass the agent cap, is refused whole (
429) and nothing is stored. The worker does not count the items again. - Paused, not lost. When Korely has stopped calling language models for the day to cap its spending, the import still stores the memories and the facts are extracted after the pause.
- Webhooks. The worker emits a
memory.createdwebhook for each successful import. Aquota.warningwebhook fires when the POST's admission crosses 80% of the month's writes. - Crash-safe and idempotent. The worker claims jobs atomically (
SELECT ... FOR UPDATE SKIP LOCKED), uses bounded attempts with exponential backoff, and resumes from theimported + failedoffset after a crash, so a restart never double-imports. - Hard ceiling. A single batch accepts at most 500 items.
- Hobby fits fewer. A batch is counted whole against the month's writes. Hobby has 250 writes a month, 275 with the 10% allowance, so a batch that needs more than 275 writes is always refused there, and 500 items never fit. From Developer upward a 500-item batch fits when the month has room. (Read from the code; not tried on a Hobby account.)
Related
- Batch status, poll
GET /v1/batch/{job_id}for progress and per-item errors. - Add a memory, the single-item write each batch item runs through.
- SDK, the typed client surface.