Naxis Technologies Docs
Live demo

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.

Base URL The API lives on your own deployment, e.g. https://assistant.yourcompany.com/api/v1. There is no Naxis-hosted API — your data never leaves your instance.

Keys and planes

Two planes, one API. The data plane pushes documents into the knowledge base — POST /ingest/documents · /batch · /sync. The message plane asks questions and gets cited answers — POST /messages · /messages/stream · /conversations. Three kinds of key reach them, each a Bearer secret shown once at creation.

KeyMinted atReachesAnswers as
Ingestion API/MCP
nx_sk_…
Documents → Add a source → Ingestion API/MCP the data plane only
Messaging API/MCP
nx_sk_…
Chat channels → Add a channel → Messaging API/MCP the message plane only a guest at the channel's groups, or a specific user for a per-user key
Admin API/MCP
nx_ak_…
the API & MCP page both planes the administrator who minted it, with their own document access

An Ingestion or Messaging key reaches only its own plane — a different card is a different key, and neither crosses to the other's job. The Admin API/MCP key is the exception: one credential that asks, pushes documents, and — over this deployment's MCP endpoint — runs the console itself, all under the minting administrator's own identity and audit trail. It stops working the moment that admin account is deactivated or demoted. Give an integration only the key its job needs.

The API & MCP console page: a step card that creates the admin key, with the connect steps below it.
Where keys are minted: 1 the console’s API & MCP page is one guided card — 2 its first step creates the admin key, the next connects an MCP client or hands you the plain REST address.

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). An external_id is up to 512 characters of letters, digits and ._:/-, and must start with a letter or digit.

Create or replace one document

POST /api/v1/ingest/documents
Content-Type: application/json

{
  "external_id": "crm/deal/8842",
  "title": "Renewal terms — deal 8842",
  "text": "# Renewal\nThis 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": "Renewal terms — deal 8842",
    "text": "# Renewal\nThis contract renews on 2026-09-01 …",
    "acl": ["grp:sales"],
    "metadata": {"source_system": "hubspot"}
  }'

The response echoes the resolved identity and queues indexing:

{
  "external_id": "crm/deal/8842",
  "uri": "api://3f1c…/crm/deal/8842",
  "created": true,
  "status": "queued",
  "acl": ["grp:sales"],
  "job": 41
}

uri is this document's permanent identity across the whole API — the same value /messages citations point back to. created is true the first time this external_id is seen and false on every later replace.

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.

FieldControls
titleDisplay title. Optional — blank is fine.
text / content_base64 The content — exactly one of the two.
filename Names the format the pipeline reads the bytes as. Falls back to a known extension already on external_id, then to one guessed from content_type.
content_type A MIME type, for when you're not naming a file. Defaults to text/markdown for text, or application/octet-stream for content_base64.
acl Groups (grp:*) allowed to see this document. Omit it to inherit the source's own default groups.
metadata An object of your own key/value pairs, stored and returned as-is.
Name your binary pushes A content_base64 push with no filename, no content_type, and an external_id that doesn't already end in a real extension is read as plain text. Set filename (e.g. "contract.pdf") whenever you push anything other than markdown or plain text.

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": "Renewal terms — deal 8842", "text": "…" },
      { "external_id": "crm/deal/8843", "title": "Renewal terms — deal 8843", "text": "…" }
    ]
  }'

Up to 100 documents per call. The response reports the whole batch:

{
  "documents": [
    { "external_id": "crm/deal/8842", "uri": "api://3f1c…/crm/deal/8842", "created": true },
    { "external_id": "crm/deal/8843", "uri": "api://3f1c…/crm/deal/8843", "created": true }
  ],
  "count": 2,
  "status": "queued",
  "job": 42
}

Read, delete, sync, status

Method & pathDoes
GET /ingest/documentsList 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/syncRe-index this source's corpus now.
GET /ingest/statusStore 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"

Shapes, abbreviated:

GET /ingest/documents
{ "documents": [ { "external_id": "crm/deal/8842", "uri": "api://3f1c…/crm/deal/8842",
    "title": "Renewal terms — deal 8842", "status": "active", "bytes": 812,
    "created_at": "2026-08-20T09:14:00Z", "updated_at": "2026-08-20T09:14:00Z" } ],
  "total": 1, "limit": 20, "offset": 0 }

GET /ingest/documents/crm%2Fdeal%2F8842
{ "external_id": "crm/deal/8842", "uri": "api://3f1c…/crm/deal/8842",
  "title": "Renewal terms — deal 8842", "filename": "8842.md", "content_type": "text/markdown",
  "acl": ["grp:sales"], "metadata": { "source_system": "hubspot" }, "bytes": 812,
  "status": "active", "error": null,
  "created_at": "2026-08-20T09:14:00Z", "updated_at": "2026-08-20T09:14:00Z" }

DELETE /ingest/documents/crm%2Fdeal%2F8842
{ "external_id": "crm/deal/8842", "uri": "api://3f1c…/crm/deal/8842",
  "status": "queued", "job": 44 }

DELETE …?purge=true
{ "external_id": "crm/deal/8842", "uri": "api://3f1c…/crm/deal/8842",
  "erased": { "passages_deleted": 4, "found": true } }

POST /ingest/sync
{ "job": 45, "status": "queued" }

GET /ingest/status
{ "pushed": 128, "indexed": 121, "failed": 2, "deleted": 5,
  "source_status": "ok", "last_error": "", "last_synced_at": "2026-08-30T06:00:00Z" }

A document's status is queued (pushed, not yet processed), active (indexed and answerable), failed (see its error) or deleted (tombstoned, awaiting purge). A source's own source_status on /ingest/status is new, ok, syncing or error.

Asking questions

Send a question to the message plane — the same call in every language:

curl -X POST https://your-assistant/api/v1/messages \
  -H "Authorization: Bearer $NAXIS_MESSAGE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"When does this contract renew?","end_user":"u-3391"}'
FieldControls
textThe question. Required, non-empty.
end_user Scopes conversation memory within a shared channel key, so two of your users never share a thread. Letters, digits and ._:-, up to 128 characters; omitted, it's "default". Ignored for a per-user or Admin API/MCP key, which already answers as one person.
conversation_id Continue a thread — the id a previous call returned. Omit it to start a new one.
force_answer True always returns a composed, cited answer. Optional, default false.

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": "This contract renews on 1 September 2026 [1].",
  "abstained": false,
  "error": false,
  "conversation_id": "6f1c…",
  "citations": [
    { "n": 1, "passage_id": "…", "breadcrumb": "Renewal terms — deal 8842 › Renewal",
      "source_uri": "api://3f1c…/crm/deal/8842" }
  ]
}

Pass the returned conversation_id back to continue a multi-turn conversation. A per-user key — including an Admin API/MCP 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. source_uri is the same uri the ingest endpoints use — match it against a pushed document's own uri if you need to link back to your system.

Streaming the answer

The same call over Server-Sent Events. Every message is a plain data: line carrying JSON — this endpoint never sends an SSE event: field, so tell messages apart by the type inside: incremental delta previews, an occasional reset (discard whatever partial text you've printed so far and keep listening), then one terminal final carrying the exact object POST /messages returns, under that same type key.

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 this contract renew?","end_user":"u-3391"}'

The wire format:

data: {"type": "delta", "text": "This contract renews on "}

data: {"type": "delta", "text": "1 September 2026 [1]."}

data: {"type": "final", "text": "This contract renews on 1 September 2026 [1].", "abstained": false, "error": false, "conversation_id": "6f1c…", "citations": [{"n": 1, "passage_id": "…", "breadcrumb": "Renewal terms — deal 8842 › Renewal", "source_uri": "api://3f1c…/crm/deal/8842"}]}

Conversations

Method & pathDoes
GET /conversationsThe 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"

Shapes, abbreviated:

GET /conversations
[ { "conversation_id": "6f1c…", "end_user": "u-3391",
    "created_at": "2026-08-29T10:00:00Z", "last_active_at": "2026-08-29T10:04:00Z",
    "messages": 4, "first_question": "When does this contract renew?" } ]

GET /conversations/6f1c…
{ "conversation_id": "6f1c…", "messages": [
    { "role": "user", "content": "When does this contract renew?",
      "created_at": "2026-08-29T10:00:00Z", "citations": [] },
    { "role": "assistant", "content": "This contract renews on 1 September 2026 [1].",
      "created_at": "2026-08-29T10:00:03Z",
      "citations": [ { "n": 1, "passage_id": "…", "breadcrumb": "Renewal terms — deal 8842 › Renewal",
        "source_uri": "api://3f1c…/crm/deal/8842", "live": true, "doc_alive": true } ] } ] }

DELETE /conversations/6f1c…
{ "ok": true }

end_user is only present for a shared channel key's conversations (null for a per-user or Admin API/MCP key, which are already scoped to one person). On a history citation, live is false once that exact passage no longer exists — say, after the document changed; doc_alive is false only once the source document itself has left the knowledge base. Either way the citation stays visible; it just can't jump to a live passage any more.

Limits & errors

StatusMeaning
202Ingest accepted; indexing runs in the background (carries a job id).
400Malformed request — missing or duplicate content fields, an invalid external_id or end_user, an unknown ACL group, an empty question, or a batch of zero or over 100 documents.
401Missing or unknown Bearer key.
404No document or conversation matches that id — including one that exists but belongs to a different key.
413Document over 50 MB, or request over 100 MB.
422The request body itself doesn't match the schema (wrong type, missing required field) — a list-shaped {"detail": [...]} body, not the plain string below.
423This deployment is currently locked ({"error": "license_locked", …}) — contact your administrator.
429Rate limit: 300 document pushes a minute per Ingestion API key, or 60 questions a minute per Messaging API key and end_user. Slow down and retry.
503The answering service was briefly unavailable; the turn was not saved — retry. Alone among the errors, this one answers in the answer shape rather than detail: text carries an apology, error is true, citations is empty. Branch on the body shape, not the status alone.

Every other error carries a plain {"detail": "…message…"} body — read detail for what to fix. The one exception above aside, a 422 is the other shape to expect: its detail is a list of field problems, not a sentence.

A separate limit for MCP This table covers /api/v1. An Admin API/MCP key calling this deployment's MCP endpoint has its own cap on management tool calls — 120 a minute — unrelated to the two limits above.
Interactive reference Each deployment serves a machine-readable OpenAPI 3 document for the whole REST API — both planes — at /api/v1/openapi.json: point your generator or Postman at it.
Was this page helpful?
Was this page helpful? Sign in with Google Sign in to tell us — or leave a comment.

Last updated 3 Sep 2026