Documents
A document is any prose you want the Brain to know: a call note, a support thread, a wiki page, a commit message. Send it and it becomes searchable and citable immediately.
curl -X POST "$API/v1/brains/tenant:acme/documents" \ -H "authorization: Bearer $NICIA_KEY" \ -H "content-type: application/json" \ -d '{ "id": "zendesk:ticket:8821", "title": "SSO login loop after IdP change", "text": "Customer reports a redirect loop after switching to Okta…", "metadata": { "source": "zendesk", "status": "resolved", "priority": "p1" }, "occurredAt": "2026-08-02T14:20:00Z" }'{ "data": { "document": { "id": "zendesk:ticket:8821", "status": "queued" } }}Only text is required. Everything else is optional and useful.
The Brain takes text, not bytes. The write’s body is a JSON text field and
there is no file upload, so a PDF, a slide deck, or an audio recording is yours
to convert first. Conversion is your step; keep the original filename and the
converter you used in metadata. Keep the document’s headings as markdown
headings when you convert: a standing answer cites a document by section
(§ <heading>), and text with no headings can be cited only by line range.
Your id, not ours
Section titled “Your id, not ours”id is a string you choose, and it is the document’s permanent handle. Re-send
the same id and you replace that document.
Send one. A document with your id is an upsert, which makes it naturally
idempotent — a retry after a timeout is one document, not two. Omit it and Nicia
mints an id, but then a retry needs an Idempotency-Key or you get duplicates.
See Idempotency.
Use the id from the system the content came from — zendesk:ticket:8821,
notion:page:abc. Then a nightly sync is a loop of POSTs with no
bookkeeping, no diffing, and no “have I sent this already” table.
# Same id, new content — replaces version 1curl -X POST "$API/v1/brains/tenant:acme/documents" \ -H "authorization: Bearer $NICIA_KEY" \ -H "content-type: application/json" \ -d '{ "id": "zendesk:ticket:8821", "text": "…updated transcript…" }'Nicia keeps prior versions. A receipt issued last month still names the version it was served, and the key that made that read can still fetch the text that version held.
Metadata is free-form, and nothing drops it
Section titled “Metadata is free-form, and nothing drops it”metadata takes any flat JSON object. Nothing needs declaring and nothing is
discarded — every key you send comes back on the document when you read it.
The one refusal: keys beginning nicia: are reserved — that is where the
server keeps the document’s own title and occurredAt — so writing one is a
400, never a key that is silently dropped.
curl "$API/v1/brains/tenant:acme/documents/zendesk:ticket:8821" \ -H "authorization: Bearer $NICIA_KEY"What metadata is not is an index. No read filters on it: /context and
/search take no filter at all, and /query’s where index-resolves
exact slug equality only — free-form document metadata is never a predicate.
Metadata is how a value survives the trip; it is not how you find it again.
So send it freely — source system, document type, author, team, status, language, confidentiality. Wanting to govern one of them is the signal that it belongs declared as a field on the record kind it describes (returned and governed; non-slug filtering still waits on a property index). Resolve by slug today:
curl -X POST "$API/v1/brains/tenant:acme/query" \ -H "authorization: Bearer $NICIA_KEY" \ -H "content-type: application/json" \ -d '{ "kind": "ticket", "where": { "slug": "zendesk-ticket-8821" } }'A declared field governs and returns everything already written, because nothing
you sent was thrown away. The declaring itself happens when the Brain is created
— see when a schema is
chosen. A declared field is
also filterable in /query; an undeclared one is returned but not queryable.
Checking on a document
Section titled “Checking on a document”A document can take a moment to index. The write answers queued — the bytes
are stored and the document is already addressable by your id — and a read
reports processing until body search can reach it, then indexed. A document
the Brain refused is rejected, with the reason on it, which in a batch is how
one bad document reports itself while the rest land. Read it back to check:
curl "$API/v1/brains/tenant:acme/documents/doc_91ab" \ -H "authorization: Bearer $NICIA_KEY"{ "data": { "document": { "id": "doc_91ab", "status": "indexed", "version": 1, "createdAt": "2026-08-14T09:41:02Z", "updatedAt": "2026-08-14T09:41:05Z" } }}Listing what you sent
Section titled “Listing what you sent”GET /v1/brains/{handle}/documents pages through every document you submitted
to the Brain over the API, newest version of each, keyset paged by document id.
It is a listing of submissions, not an inventory of everything the Brain can
answer from. Content that arrived another way — a synced folder or URL source,
say — has no submission behind it and no document id to address, so it is not
here, but /context, /query, and /search all read it. Use the listing to
reconcile your own pipeline against what landed; use
/context to see what the Brain will actually answer with.
Batches
Section titled “Batches”Send up to 100 documents in one request. Each is indexed independently, so one bad document does not fail the rest:
curl -X POST "$API/v1/brains/tenant:acme/documents" \ -H "authorization: Bearer $NICIA_KEY" \ -H "content-type: application/json" \ -d '{ "documents": [ { "id": "zendesk:ticket:8821", "text": "…" }, { "id": "zendesk:ticket:8822", "text": "…" } ] }'{ "data": { "documents": [ { "id": "zendesk:ticket:8821", "status": "queued" }, { "id": "zendesk:ticket:8822", "status": "rejected", "error": "text is empty" } ] }}Per-item isolation is a batch’s promise and only a batch’s. A single-document
write that the Brain refuses fails with the refusal’s own status — never a
201 whose body reports a document that was not stored.
What happens next
Section titled “What happens next”An indexed document is immediately available to
/context and /search. That is the whole
requirement — you can stop here and have a working retrieval system.
Separately, Nicia can turn what it recognises in prose into proposed structured
changes, each anchored to the exact passage it came from. In open mode those
land directly; in reviewed mode they queue for approval. Either way the
document itself was already useful.
# See what was extracted from a documentcurl "$API/v1/brains/tenant:acme/documents/zendesk:ticket:8821/records" \ -H "authorization: Bearer $NICIA_KEY"That answers the record addresses this document produced and what kind each
one is — not their values. Every read that returns values returns them under a
receipt, so read the records themselves through
/context or
the record read.
Turn extraction off per document when you only want the text searchable:
{ "id": "raw:log:5512", "text": "…", "extract": false }Superseding
Section titled “Superseding”Documents are addressed, so the way to replace one is to write it again under
the same id. The new text becomes the current version, the old version is
kept, and a receipt that cited the old version still names it — its text stays
readable through that receipt.
There is no document delete, and a document written here is never retired: it is served until you write it again. Removing its influence means retiring every record extracted from it and marking every receipt that cited it — that is correction work over the accepted state, not a delete, and Nicia would rather have no operation than one that quietly leaves derived records standing.
When the content you send is revised as a whole set — one customer’s documents,
say — send it as a source instead. A complete
snapshot that leaves a document out proposes retiring
it, through the Brain’s review policy, together with the record values only
that document supported. A retired document stops being served; its versions
are kept, and the receipts that pinned them still resolve.