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.
/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, running embeddings, typed-fact extraction, and
contradiction checking, exactly as a single add would. Poll
GET /v1/batch/{job_id} for progress.
Authentication
HTTP header, required: Authorization: Bearer kor_live_.... 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 for the shared namespace. |
agent_id | string · null | Optional | Your application's namespace. Default null. Same rule as user_id. |
run_id | string · null | Optional | One session or conversation. 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. At most 8,192 bytes once serialized, 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, 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 | 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
job = 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 | A body larger than 2 MiB did not arrive whole within 120 seconds. 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, the most this route takes. Refused before it is read; nothing is stored. A body that holds more JSON values than 500 valid items can hold is refused with 413 too, before it is decoded; a valid batch, however dense its metadata, never is. |
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 memories do not fit in this month's writes left (batches already queued count as used). Nothing is stored. No Retry-After. |
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. |
Notes
- It's asynchronous. The POST only enqueues the job and returns immediately (
202). A background worker imports each item later through the same pipeline asPOST /v1/memories,engine.addplus best-effort typed-fact extraction. - 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. While the service's daily model budget is spent, the import continues 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.
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.