For developers
The client API
Every deployment exposes one versioned HTTP API under /api/v1:
a data plane to push documents into the knowledge base, and a
message plane to ask questions and get cited answers. Both are
authenticated by a Bearer key you create in the admin console.
https://assistant.yourcompany.com/api/v1.
There is no Naxis-hosted API — your data never leaves your instance.
Two keys, two jobs
The API is surfaced as two catalog cards, each with its own key.
Create them under Documents → Add a source → API (the ingest key)
and Chat channels → Add a channel → API (the message key). A key
is a Bearer secret shown once at creation; it starts with nx_sk_.
POST /ingest/documents · /batch · /syncPOST /messages · /messages/stream · /conversations| Card | Key authorises | Answers as |
|---|---|---|
| API source | the /ingest/* endpoints | — |
| API channel | the /messages* and /conversations* endpoints |
a guest at the channel's groups, or a specific user for a per-user key |
Authenticate every request with the header:
Authorization: Bearer nx_sk_…
Ingesting documents
Push a document by a stable external_id of your choosing — your
record id, a file path, anything unique within the source. Re-pushing the same
id replaces the document; the sweep re-indexes it in the background (responses
are 202 Accepted with a job id).
Create or replace one document
POST /api/v1/ingest/documents
Content-Type: application/json
{
"external_id": "crm/deal/8842",
"title": "Acme renewal — terms",
"text": "# Renewal\nThe Acme contract renews on 2026-09-01 …",
"acl": ["grp:sales"],
"metadata": {"source_system": "hubspot"}
}
curl -X POST https://your-assistant/api/v1/ingest/documents \
-H "Authorization: Bearer $NAXIS_INGEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "crm/deal/8842",
"title": "Acme renewal — terms",
"text": "# Renewal\nThe Acme contract renews on 2026-09-01 …",
"acl": ["grp:sales"],
"metadata": {"source_system": "hubspot"}
}'
import os, requests
BASE = "https://your-assistant/api/v1"
KEY = os.environ["NAXIS_INGEST_KEY"]
r = requests.post(
f"{BASE}/ingest/documents",
headers={"Authorization": f"Bearer {KEY}"},
json={
"external_id": "crm/deal/8842",
"title": "Acme renewal — terms",
"text": "# Renewal\nThe Acme contract renews on 2026-09-01 …",
"acl": ["grp:sales"],
"metadata": {"source_system": "hubspot"},
},
)
r.raise_for_status() # 202 Accepted
print(r.json()["job"]) # background indexing job id
const BASE = "https://your-assistant/api/v1";
const KEY = process.env.NAXIS_INGEST_KEY;
const r = await fetch(`${BASE}/ingest/documents`, {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
external_id: "crm/deal/8842",
title: "Acme renewal — terms",
text: "# Renewal\nThe Acme contract renews on 2026-09-01 …",
acl: ["grp:sales"],
metadata: { source_system: "hubspot" },
}),
});
const { job } = await r.json(); // 202 Accepted
console.log(job);
Provide exactly one of text (inline markdown/text/HTML)
or content_base64 (base64 bytes for PDF, DOCX and other binaries, up
to 50 MB). acl is a list of groups (grp:*); omit it and
the document inherits the source's own groups. PUT /ingest/documents/{external_id}
does the same, taking the id from the path.
Batch
POST /api/v1/ingest/documents/batch
{ "documents": [ { "external_id": "…", "text": "…" }, … ] }
curl -X POST https://your-assistant/api/v1/ingest/documents/batch \
-H "Authorization: Bearer $NAXIS_INGEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"documents": [
{ "external_id": "crm/deal/8842", "title": "Acme renewal", "text": "…" },
{ "external_id": "crm/deal/8843", "title": "Globex renewal", "text": "…" }
]
}'
import os, requests
BASE = "https://your-assistant/api/v1"
KEY = os.environ["NAXIS_INGEST_KEY"]
deals = [
{"id": 8842, "name": "Acme renewal", "body": "…"},
{"id": 8843, "name": "Globex renewal", "body": "…"},
]
documents = [
{"external_id": f"crm/deal/{d['id']}", "title": d["name"], "text": d["body"]}
for d in deals # up to 100 documents per call
]
r = requests.post(
f"{BASE}/ingest/documents/batch",
headers={"Authorization": f"Bearer {KEY}"},
json={"documents": documents},
)
r.raise_for_status()
print(r.json()) # {"accepted": 2, "job": "…"}
const BASE = "https://your-assistant/api/v1";
const KEY = process.env.NAXIS_INGEST_KEY;
const deals = [
{ id: 8842, name: "Acme renewal", body: "…" },
{ id: 8843, name: "Globex renewal", body: "…" },
];
const documents = deals.map((d) => ({
external_id: `crm/deal/${d.id}`,
title: d.name,
text: d.body,
}));
const r = await fetch(`${BASE}/ingest/documents/batch`, {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ documents }), // up to 100 per call
});
console.log(await r.json());
Up to 100 documents per call.
Read, delete, sync, status
| Method & path | Does |
|---|---|
GET /ingest/documents | List pushed documents with indexing status (limit, offset, prefix). |
GET /ingest/documents/{external_id} | One document and its status. |
DELETE /ingest/documents/{external_id} | Tombstone it (the sweep removes it). ?purge=true erases immediately. |
POST /ingest/sync | Re-index this source's corpus now. |
GET /ingest/status | Store and indexing counts for this source. |
# List indexed documents (limit / offset / prefix)
curl "https://your-assistant/api/v1/ingest/documents?limit=20&prefix=crm/" \
-H "Authorization: Bearer $NAXIS_INGEST_KEY"
# One document and its status (slashes in the id are percent-encoded)
curl https://your-assistant/api/v1/ingest/documents/crm%2Fdeal%2F8842 \
-H "Authorization: Bearer $NAXIS_INGEST_KEY"
# Tombstone it — add ?purge=true to erase immediately
curl -X DELETE https://your-assistant/api/v1/ingest/documents/crm%2Fdeal%2F8842 \
-H "Authorization: Bearer $NAXIS_INGEST_KEY"
import os, requests
from urllib.parse import quote
BASE = "https://your-assistant/api/v1"
H = {"Authorization": f"Bearer {os.environ['NAXIS_INGEST_KEY']}"}
# List indexed documents
docs = requests.get(f"{BASE}/ingest/documents",
headers=H, params={"limit": 20, "prefix": "crm/"}).json()
# One document and its status — percent-encode the external_id
eid = quote("crm/deal/8842", safe="")
one = requests.get(f"{BASE}/ingest/documents/{eid}", headers=H).json()
# Tombstone it (purge=true erases immediately)
requests.delete(f"{BASE}/ingest/documents/{eid}",
headers=H, params={"purge": "true"})
const BASE = "https://your-assistant/api/v1";
const H = { Authorization: `Bearer ${process.env.NAXIS_INGEST_KEY}` };
// List indexed documents
const docs = await fetch(
`${BASE}/ingest/documents?limit=20&prefix=${encodeURIComponent("crm/")}`,
{ headers: H },
).then((r) => r.json());
// One document and its status — encode the external_id
const eid = encodeURIComponent("crm/deal/8842");
const one = await fetch(`${BASE}/ingest/documents/${eid}`, { headers: H })
.then((r) => r.json());
// Tombstone it (?purge=true erases immediately)
await fetch(`${BASE}/ingest/documents/${eid}?purge=true`, {
method: "DELETE",
headers: H,
});
Asking questions
Send a question to the message plane. The same call in three forms:
curl -X POST https://your-assistant/api/v1/messages \
-H "Authorization: Bearer $NAXIS_MESSAGE_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"When does the Acme contract renew?","end_user":"u-3391"}'
import os, requests
BASE = "https://your-assistant/api/v1"
KEY = os.environ["NAXIS_MESSAGE_KEY"]
r = requests.post(
f"{BASE}/messages",
headers={"Authorization": f"Bearer {KEY}"},
json={"text": "When does the Acme contract renew?", "end_user": "u-3391"},
)
answer = r.json()
print(answer["text"])
for c in answer["citations"]:
print(c["n"], c["breadcrumb"])
# Continue the thread: pass answer["conversation_id"] on the next turn
const BASE = "https://your-assistant/api/v1";
const KEY = process.env.NAXIS_MESSAGE_KEY;
const r = await fetch(`${BASE}/messages`, {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "When does the Acme contract renew?",
end_user: "u-3391",
}),
});
const answer = await r.json();
console.log(answer.text);
answer.citations.forEach((c) => console.log(c.n, c.breadcrumb));
// Continue the thread: pass answer.conversation_id on the next turn
The answer is grounded in the documents the caller is allowed to see, and it cites the exact passages — or abstains when the knowledge base doesn't contain the answer, rather than guessing.
{
"text": "The Acme contract renews on 1 September 2026 [1].",
"abstained": false,
"error": false,
"conversation_id": "6f1c…",
"citations": [
{ "n": 1, "passage_id": "…", "breadcrumb": "Acme renewal — terms › Renewal" }
]
}
end_user scopes conversation memory within the channel, so two of your
users don't share a thread. Pass the returned conversation_id back to
continue a multi-turn conversation. POST /messages/stream streams the
same result over Server-Sent Events (a terminal final event carries the
authoritative payload). A per-user key answers as its owner with the owner's own
document access; the shared channel key answers as a guest at the channel's groups.
Streaming the answer
The same call over Server-Sent Events: incremental delta events, then
one terminal final event carrying the authoritative payload (the same
object POST /messages returns).
curl -N -X POST https://your-assistant/api/v1/messages/stream \
-H "Authorization: Bearer $NAXIS_MESSAGE_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"text":"When does the Acme contract renew?","end_user":"u-3391"}'
import os, json, requests
BASE = "https://your-assistant/api/v1"
KEY = os.environ["NAXIS_MESSAGE_KEY"]
with requests.post(
f"{BASE}/messages/stream",
headers={"Authorization": f"Bearer {KEY}", "Accept": "text/event-stream"},
json={"text": "When does the Acme contract renew?", "end_user": "u-3391"},
stream=True,
) as r:
event = None
for line in r.iter_lines(decode_unicode=True):
if not line: # blank line ends one event
event = None
elif line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data = json.loads(line[5:])
if event == "delta":
print(data["text"], end="", flush=True)
elif event == "final":
print("\n", data["citations"])
const BASE = "https://your-assistant/api/v1";
const KEY = process.env.NAXIS_MESSAGE_KEY;
const r = await fetch(`${BASE}/messages/stream`, {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
body: JSON.stringify({
text: "When does the Acme contract renew?",
end_user: "u-3391",
}),
});
const reader = r.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const events = buffer.split("\n\n");
buffer = events.pop(); // keep the incomplete tail
for (const chunk of events) {
const ev = /event:\s*(.*)/.exec(chunk)?.[1];
const data = /data:\s*(.*)/.exec(chunk)?.[1];
if (!data) continue;
const payload = JSON.parse(data);
if (ev === "delta") process.stdout.write(payload.text);
else if (ev === "final") console.log("\n", payload.citations);
}
}
The wire format:
event: delta
data: {"text": "The Acme contract renews on "}
event: delta
data: {"text": "1 September 2026 [1]."}
event: final
data: {"text": "The Acme contract renews on 1 September 2026 [1].", "abstained": false, "conversation_id": "6f1c…", "citations": [{"n": 1, "breadcrumb": "Acme renewal — terms › Renewal"}]}
Conversations
| Method & path | Does |
|---|---|
GET /conversations | The caller's conversations. |
GET /conversations/{id} | One conversation's messages and citations. |
DELETE /conversations/{id} | Erase a conversation. |
CID="6f1c9e2a-…"
# List the caller's conversations
curl https://your-assistant/api/v1/conversations \
-H "Authorization: Bearer $NAXIS_MESSAGE_KEY"
# One conversation's messages and citations
curl "https://your-assistant/api/v1/conversations/$CID" \
-H "Authorization: Bearer $NAXIS_MESSAGE_KEY"
# Erase a conversation
curl -X DELETE "https://your-assistant/api/v1/conversations/$CID" \
-H "Authorization: Bearer $NAXIS_MESSAGE_KEY"
import os, requests
BASE = "https://your-assistant/api/v1"
H = {"Authorization": f"Bearer {os.environ['NAXIS_MESSAGE_KEY']}"}
# List the caller's conversations
conversations = requests.get(f"{BASE}/conversations", headers=H).json()
# One conversation's full transcript with citations
cid = conversations[0]["id"]
thread = requests.get(f"{BASE}/conversations/{cid}", headers=H).json()
# Erase it
requests.delete(f"{BASE}/conversations/{cid}", headers=H)
const BASE = "https://your-assistant/api/v1";
const H = { Authorization: `Bearer ${process.env.NAXIS_MESSAGE_KEY}` };
// List the caller's conversations
const conversations = await fetch(`${BASE}/conversations`, { headers: H })
.then((r) => r.json());
// One conversation's full transcript with citations
const cid = conversations[0].id;
const thread = await fetch(`${BASE}/conversations/${cid}`, { headers: H })
.then((r) => r.json());
// Erase it
await fetch(`${BASE}/conversations/${cid}`, { method: "DELETE", headers: H });
Limits & errors
| Status | Meaning |
|---|---|
202 | Ingest accepted; indexing runs in the background (carries a job id). |
400 | Malformed request (missing external_id, both/neither of text and content, empty question). |
401 | Missing or unknown Bearer key. |
413 | Document over 50 MB, or request over 100 MB. |
423 | The assistant is temporarily unavailable — contact your administrator. |
429 | Rate limit: 300 pushes a minute per document key, 60 questions a minute per messaging key and end user, 120 calls a minute per admin key. Slow down and retry. |
503 | The answering service was briefly unavailable; the turn was not saved — retry. |
/api/v1/openapi.json — point your generator or Postman at it.
Last updated 28 Aug 2026