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.201 Created
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).
background; on the wire it is async.
Let Anona choose the space
Sendroute: "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 ajob_id returned
immediately.
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 asyncrecord 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
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.200 OK
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
200 OK
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:
{"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.Edit or retire a memory
Correct what was stored, or reversibly retire it. Every field is optional; only what you send changes.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.history array means the memory has never
changed.
Delete a memory
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.