Skip to content
Beta. This surface may change before GA; breaking changes are dated in the changelog.

Documents

A document is any prose you want the Brain to know: a call note, a support thread, a PDF, a wiki page, a commit message. Send it and it becomes searchable and citable immediately.

Terminal window
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.

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, s3://bucket/key. Then a nightly sync is a loop of POSTs with no bookkeeping, no diffing, and no “have I sent this already” table.

Terminal window
# Same id, new content — replaces version 1
curl -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, so a receipt issued last month still resolves to the text that was actually shown. See Receipts.

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:

Terminal window
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 matches declared fields, which free-form document metadata is not. 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. When you find yourself wanting to slice by one of them, that is the signal to declare it as a field on the record kind it describes, and query it exactly:

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/query" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-d '{ "kind": "ticket", "where": { "source": "zendesk", "priority": "p1" } }'

Declaring is retroactive, because nothing you already sent was thrown away — see Schemas.

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:

Terminal window
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"
}
}
}

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.

Send up to 100 documents in one request. Each is indexed independently, so one bad document does not fail the rest:

Terminal window
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.

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.

Terminal window
# See what was extracted from a document
curl "$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 }

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 stays resolvable, and any receipt that cited the old version keeps resolving to what it actually showed.

There is no document delete. Removing a source’s 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.