Skip to main content

Store a memory

user_id is a real field, not metadata. Putting a user id in metadata stores it but does not isolate anything: metadata is returned with results, never filtered on.
Response 201 Created
A 201 does not mean something was stored. One item usually extracts to several facts, but it can extract to none, and the write is still accepted because the extraction itself did not fail — there was simply nothing durable in the content. Measured against production, roughly 44% of short, context-free statements land there.Check status, not the HTTP code:
The fix is context, and the effect is large: "Falcon retry count is 3." survived about 5 times in 9, while "Dunlin, the webhook sender, has a connection pool of 25." survived 10 out of 10. Name what the thing is, or why it matters, in the same sentence.This is charged either way — the extraction model ran. message is null on every successful write, so a caller may surface it whenever it is present.
A synchronous write is slow, and async is several times faster. A memory is analyzed by a language model and its facts extracted before the call returns, so record waits for that work. Measured against production with short memories:How long a synchronous write takes depends mostly on how much you send: longer content means more for the model to extract, and very long content is extracted in several passes, so expect the top of that range or beyond for anything past a few paragraphs.Anything a person is waiting on (a chat turn, a form submit, a page transition) should use async mode below. Reach for a synchronous write only when the very next thing you do is search for what you just stored.

Store a memory asynchronously

Set "async": true and the call returns in about a second with a job_id and status: "processing" in place of the stored memory_id. The memory is extracted and indexed in the background; poll the job to learn when it is searchable. This is the recommended way to write. The extraction work is identical and costs the same; you simply stop waiting for it. The one thing that changes is timing: the memory is not searchable the instant the call returns, so a flow that writes and then immediately retrieves needs to wait for the job (or use a synchronous write).
The SDKs spell the flag background; on the wire it is async.
Pair async writes with webhooks and you can drop polling entirely: Anona posts to your endpoint when the memory is stored.

Let Anona choose the space

Send route: "auto" in place of space_id and Anona files the memory into the space it belongs in. Exactly one of the two must be present: sending both, or neither, is 422 route_conflict.
routed_to is present only when route was used, and is null on a write that named its space. Each entry reports where the memory went and how that was decided.
A reason of router_disabled means automatic routing is not enabled on the deployment you are calling: route: "auto" is accepted and the write succeeds, but the memory goes to your fallback space. The routing guide lists every reason value and what each one means.
max_targets accepts only 1 today. 2 or 3 is refused with 422 validation_error rather than accepted and quietly capped.
The same field means something wider on a read. A memory is filed in one space, so max_targets on a write is 1. A question can legitimately be answered out of several, so on POST /v1/retrieve the same field accepts 1 to 20.
Error codes are lowercase snake_case and arrive in the standard {"error": {"code", "message"}} envelope. Which spaces can be chosen, how to give a space a description, and the rules that answer before any model does are covered in Automatic space routing. Content sent to the third-party decision model is disclosed in Subprocessors. A search can be routed the same way: POST /v1/retrieve takes route: "auto" in place of space_id and answers with searched. It behaves differently in one respect, so read that section rather than assuming symmetry: a routed read that fits no space returns no results rather than falling back to your default space, because on a read the default space is the least likely place to hold a specific answer.

Bulk ingest

Queue many memories in a single call, which is the efficient way to backfill history. Bulk ingest is always asynchronous: the batch is accepted and a job_id returned immediately.
Response 202 Accepted
accepted is the number of items queued. Poll job_id for progress. A large batch may be split into several operations, all listed under job_ids.
A routed batch returns these same four fields and nothing more: there is no routed_to on a batch response. The routing decision is made from the content of up to the first 20 items. If you need to know which space a batch landed in, route a single record call instead, or set the space explicitly.
To send more than 100 items, chunk them into multiple calls. Each item is charged a base cost when queued and reconciled to its real cost once processing finishes, exactly as a synchronous write is, so queuing does not make ingestion cheaper.

Job status

Checks a job returned by an async record or by record/batch. This read is free and consumes no credits.
memory_count and memory_ids are set once the job completes — they are how you address the memories an async write produced. While a batch is running, progress carries {stage, processed, total, parts_done, parts_total, memories_stored, updated_at}; processed/total count chunks in the parts that have started, so parts_done / parts_total and status are the completion signals, not processed == total. items_submitted is how many content items the job was given, so memory_count can be read against something. One number alone says nothing: 15 memories is a good outcome for 12 items and a destroyed corpus for 35. It is null for a job that has no item count, such as a single record or a document upload. zero_yield_items is how many of those items produced no memory at all. Extraction retries a chunk once if it comes back with nothing before accepting the zero, so this is not ordinary model flakiness — it is content that came back empty twice. It is 0, not null, once measured; distinguish the two before treating a missing field as “nothing was lost.” Unlike the ratio above, a non-zero zero_yield_items is worth re-submitting that specific content, since nothing was written for it at all. retry_count and next_retry_at explain a job that is still pending. progress describes a job that is running; these describe one that has not started, which otherwise looks the same whether it is queued behind other work, parked until a later time, or has already failed and been requeued: Read retry_count: 0 with next_retry_at: null against created_at: a job that has been waiting far longer than your other jobs take, with no attempts burned and no parked time set, is the one worth raising with support. A job with a future next_retry_at is working as intended and will resume on its own.
A long-pending job is never auto-failed on a deadline. Ingestion is paced by the extraction model and a large batch legitimately queues for tens of minutes, so a timeout would report healthy work as data loss. These two fields are reported so you can tell the cases apart; the verdict is yours. To stop a job you no longer want, DELETE it (below) — that is free.
A job that produced far fewer memories than it was given items still reports completed with error: null, and that is deliberate: the memories it did write are real, and a batch of near-identical items legitimately yields fewer memories than items. Nothing is wrong with the write path. What changes this ratio is usually extraction settings: guidance, custom_prompt, or a label description phrased as extraction policy. Compare the two numbers after any change to them.
A 404 is returned when the job_id does not belong to the space.

Poll until complete

A record/batch job is polled exactly the same way. completed means every item in the batch has been stored and is searchable.

Cancel a job

Stops an ingestion job that has not finished. Cancelling is free, consumes no credits, and is never refused for an empty balance: a runaway import must always be stoppable.
Response 200 OK
A job is split into parts that run independently, and only a part still queued can be stopped. A part already being processed runs to completion. cancelled_parts is what this call stopped; running_parts is what it could not, so poll the job afterwards to see those finish.
Cancelling does not undo work already done. Memories written by parts that ran stay stored and searchable, and the base cost charged when the job was queued is not refunded — but the parts that never run are never charged for the extraction they would have done.

List memories in a space

Response 200 OK
Every row also carries the scope it was written under and, when a connector wrote it, where it came from. These are fields rather than tags because the values are stored as reserved anona: tags, which never appear in tags: A row a connector captured carries the object instead of null:
author and merged_by are GitHub’s numeric user ids, not logins, and they are usually different people. See Where a memory came from for what each tag means and for the one caveat on captures written before October 2026.
Anona stores knowledge in two layers: the raw facts you record, and the note it synthesizes from them once they settle. Both describe the same thing, so listing them side by side reads as duplicates, so by default this endpoint returns the note and hides the facts folded into it. source_ids names those facts, and prefer_observations=false lists them as rows of their own. total follows the same rule, so paging stays consistent either way.

Filtering the listing by tags

tag_groups takes the same boolean expression /v1/retrieve does. Because this is a GET there is no body to carry a nested structure, so it is JSON-encoded into the query string:
URL-encode it as your HTTP client requires. Groups are AND-ed, and each is a leaf {"tags": [...], "match": ...} or one of {"and": [...]}, {"or": [...]}, {"not": {...}}. There is no separate tags parameter on this endpoint, and there does not need to be: a flat tag list is a single leaf, [{"tags": ["project:alpha"]}]. total counts the filtered set, not the unfiltered one, so pagination over a filtered listing behaves the same as over an unfiltered one.
Scope still applies on top. user_id, agent_id, session_id and member_id are AND-ed onto your filter, so a scoped listing stays scoped.
List results use a few different field names from the rest of the API. Map them before reusing the objects elsewhere.Each item carries the memory body twice, as content and as text, always with the same value. Use content: it is the name record writes and retrieve returns. text is deprecated and kept only so existing clients keep working. It will not be removed without notice, but new code should not read it.entities also differs in shape: the list returns a single comma-separated string, while retrieve returns an array of strings. tags does not: it is an array of strings on both surfaces, carrying the tags written with the memory plus any a label taxonomy stamped. Scope stays out of it and arrives as user_id and friends instead.

Edit or retire a memory

Correct what was stored, or reversibly retire it. Every field is optional; only what you send changes.
The response is the updated memory in the same shape the list returns, including its state, so an invalidate or restore can be confirmed from the reply. It is read back after the edit is applied, so every field in it is current. Editing text does not re-extract entities. The memory keeps the entity set it had, and the response shows that set, not one derived from the new text. Nothing recomputes it later either. If the edit changes who or what the memory is about, send entities in the same request to replace the set. Notes (type: "note") cannot be edited — they are synthesized, and the engine rejects the attempt rather than letting a synthesis drift from its evidence.

Memory history

Memories are living records. As new information arrives, Anona updates what it knows and keeps the previous version. Fetch a memory’s history to see what it said before and when it changed.
Entries are written both by Anona’s own consolidation and by your edits. An empty history array means the memory has never changed.

Delete a memory

Response 204 No Content. The body is empty. The deletion is permanent and it cascades: the memory, its links, its entity associations and every observation consolidated from it are removed, and the other memories that fed those observations are re-consolidated without it. There is no undo. To take a memory out of recall while keeping it for audit, PATCH it to state: "invalidated" instead — that is reversible. A memory id the space does not hold is 404 memory_not_found, so deleting the same memory twice reports the second attempt honestly rather than as a success. There is no endpoint for fetching a single memory by id — read one through listing or retrieve. Updating one is PATCH, above.

Error responses

See the full error reference.

Next steps

Retrieve API

Query memories semantically.

Reason API

Synthesize an answer across memories.

Documents

Ingest files into the same space.

Webhooks

Get told when ingestion finishes.