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.
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.
| Key | Minted at | Reaches | Answers as |
|---|---|---|---|
Ingestion API/MCPnx_sk_… |
Documents → Add a source → Ingestion API/MCP | the data plane only | — |
Messaging API/MCPnx_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/MCPnx_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.
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"}
}'
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": "Renewal terms — deal 8842",
"text": "# Renewal\nThis 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: "Renewal terms — deal 8842",
text: "# Renewal\nThis contract renews on 2026-09-01 …",
acl: ["grp:sales"],
metadata: { source_system: "hubspot" },
}),
});
const { job } = await r.json(); // 202 Accepted
console.log(job);
<?php
$BASE = "https://your-assistant/api/v1";
$KEY = getenv("NAXIS_INGEST_KEY");
$payload = [
"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"],
];
$ch = curl_init("$BASE/ingest/documents");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 202) {
throw new RuntimeException("push failed ($status): $body");
}
echo json_decode($body, true)["job"]; // background indexing job id
import java.net.URI;
import java.net.http.*;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
var base = "https://your-assistant/api/v1";
var key = System.getenv("NAXIS_INGEST_KEY");
var json = new ObjectMapper();
var payload = Map.of(
"external_id", "crm/deal/8842",
"title", "Renewal terms — deal 8842",
"text", "# Renewal\nThis contract renews on 2026-09-01 …",
"acl", java.util.List.of("grp:sales"),
"metadata", Map.of("source_system", "hubspot"));
var request = HttpRequest.newBuilder(URI.create(base + "/ingest/documents"))
.header("Authorization", "Bearer " + key)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(payload)))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 202) {
throw new IllegalStateException("push failed: " + res.body());
}
System.out.println(json.readTree(res.body()).get("job").asInt());
using System.Net.Http.Json;
using System.Text.Json;
var baseUrl = "https://your-assistant/api/v1";
var key = Environment.GetEnvironmentVariable("NAXIS_INGEST_KEY");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new("Bearer", key);
var res = await http.PostAsJsonAsync($"{baseUrl}/ingest/documents", new
{
external_id = "crm/deal/8842",
title = "Renewal terms — deal 8842",
text = "# Renewal\nThis contract renews on 2026-09-01 …",
acl = new[] { "grp:sales" },
metadata = new { source_system = "hubspot" },
});
if (!res.IsSuccessStatusCode)
throw new Exception($"push failed ({(int)res.StatusCode}): {await res.Content.ReadAsStringAsync()}");
var body = await res.Content.ReadFromJsonAsync<JsonElement>(); // 202 Accepted
Console.WriteLine(body.GetProperty("job").GetInt32());
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
base := "https://your-assistant/api/v1"
key := os.Getenv("NAXIS_INGEST_KEY")
payload, _ := json.Marshal(map[string]any{
"external_id": "crm/deal/8842",
"title": "Renewal terms — deal 8842",
"text": "# Renewal\nThis contract renews on 2026-09-01 …",
"acl": []string{"grp:sales"},
"metadata": map[string]string{"source_system": "hubspot"},
})
req, _ := http.NewRequest("POST", base+"/ingest/documents", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var out struct {
Job int `json:"job"`
URI string `json:"uri"`
Created bool `json:"created"`
}
json.NewDecoder(res.Body).Decode(&out)
if res.StatusCode != http.StatusAccepted {
panic(fmt.Sprintf("push failed: %d", res.StatusCode))
}
fmt.Println(out.Job, out.URI)
}
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base = "https://your-assistant/api/v1";
let key = std::env::var("NAXIS_INGEST_KEY")?;
let res = reqwest::Client::new()
.post(format!("{base}/ingest/documents"))
.bearer_auth(&key)
.json(&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" }
}))
.send()
.await?;
if res.status() != reqwest::StatusCode::ACCEPTED {
return Err(format!("push failed: {}", res.text().await?).into());
}
let body: serde_json::Value = res.json().await?; // 202 Accepted
println!("{}", body["job"]);
Ok(())
}
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.
| Field | Controls |
|---|---|
title | Display 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. |
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": "…" }
]
}'
import os, requests
BASE = "https://your-assistant/api/v1"
KEY = os.environ["NAXIS_INGEST_KEY"]
deals = [
{"id": 8842, "name": "Renewal terms — deal 8842", "body": "…"},
{"id": 8843, "name": "Renewal terms — deal 8843", "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()
res = r.json()
print(res["count"], "documents queued as job", res["job"])
const BASE = "https://your-assistant/api/v1";
const KEY = process.env.NAXIS_INGEST_KEY;
const deals = [
{ id: 8842, name: "Renewal terms — deal 8842", body: "…" },
{ id: 8843, name: "Renewal terms — deal 8843", 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());
<?php
$BASE = "https://your-assistant/api/v1";
$KEY = getenv("NAXIS_INGEST_KEY");
$deals = [
["id" => 8842, "name" => "Renewal terms — deal 8842", "body" => "…"],
["id" => 8843, "name" => "Renewal terms — deal 8843", "body" => "…"],
];
$documents = array_map(fn($d) => [
"external_id" => "crm/deal/{$d['id']}",
"title" => $d["name"],
"text" => $d["body"],
], $deals); // up to 100 per call
$ch = curl_init("$BASE/ingest/documents/batch");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["documents" => $documents]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
printf("%d documents queued as job %d\n", $res["count"], $res["job"]);
import java.net.URI;
import java.net.http.*;
import java.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;
var base = "https://your-assistant/api/v1";
var key = System.getenv("NAXIS_INGEST_KEY");
var json = new ObjectMapper();
record Deal(int id, String name, String body) {}
var deals = List.of(new Deal(8842, "Renewal terms — deal 8842", "…"),
new Deal(8843, "Renewal terms — deal 8843", "…"));
var documents = deals.stream()
.map(d -> Map.of("external_id", "crm/deal/" + d.id(),
"title", d.name(),
"text", d.body()))
.toList(); // up to 100 per call
var request = HttpRequest.newBuilder(URI.create(base + "/ingest/documents/batch"))
.header("Authorization", "Bearer " + key)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
json.writeValueAsString(Map.of("documents", documents))))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
using System.Net.Http.Json;
using System.Text.Json;
var baseUrl = "https://your-assistant/api/v1";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("NAXIS_INGEST_KEY"));
var deals = new[]
{
new { id = 8842, name = "Renewal terms — deal 8842", body = "…" },
new { id = 8843, name = "Renewal terms — deal 8843", body = "…" },
};
var documents = deals.Select(d => new
{
external_id = $"crm/deal/{d.id}",
title = d.name,
text = d.body,
}); // up to 100 per call
var res = await http.PostAsJsonAsync($"{baseUrl}/ingest/documents/batch",
new { documents });
var body = await res.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{body.GetProperty("count")} queued as job {body.GetProperty("job")}");
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
type doc struct {
ExternalID string `json:"external_id"`
Title string `json:"title"`
Text string `json:"text"`
}
func main() {
base := "https://your-assistant/api/v1"
deals := []struct {
ID int
Name, Body string
}{
{8842, "Renewal terms — deal 8842", "…"},
{8843, "Renewal terms — deal 8843", "…"},
}
documents := make([]doc, 0, len(deals)) // up to 100 per call
for _, d := range deals {
documents = append(documents, doc{
ExternalID: fmt.Sprintf("crm/deal/%d", d.ID),
Title: d.Name,
Text: d.Body,
})
}
payload, _ := json.Marshal(map[string]any{"documents": documents})
req, _ := http.NewRequest("POST", base+"/ingest/documents/batch", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("NAXIS_INGEST_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var out struct {
Count int `json:"count"`
Job int `json:"job"`
}
json.NewDecoder(res.Body).Decode(&out)
fmt.Printf("%d documents queued as job %d\n", out.Count, out.Job)
}
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base = "https://your-assistant/api/v1";
let key = std::env::var("NAXIS_INGEST_KEY")?;
let deals = [(8842, "Renewal terms — deal 8842", "…"),
(8843, "Renewal terms — deal 8843", "…")];
let documents: Vec<_> = deals // up to 100 per call
.iter()
.map(|(id, title, body)| json!({
"external_id": format!("crm/deal/{id}"),
"title": title,
"text": body
}))
.collect();
let body: serde_json::Value = reqwest::Client::new()
.post(format!("{base}/ingest/documents/batch"))
.bearer_auth(&key)
.json(&json!({ "documents": documents }))
.send()
.await?
.json()
.await?;
println!("{} documents queued as job {}", body["count"], body["job"]);
Ok(())
}
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 & 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,
});
<?php
$BASE = "https://your-assistant/api/v1";
$H = ["Authorization: Bearer " . getenv("NAXIS_INGEST_KEY")];
function call(string $url, array $h, string $method = "GET"): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $h,
CURLOPT_CUSTOMREQUEST => $method,
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true);
}
// List indexed documents
$docs = call("$BASE/ingest/documents?" . http_build_query([
"limit" => 20, "prefix" => "crm/",
]), $H);
// One document and its status — percent-encode the external_id
$eid = rawurlencode("crm/deal/8842");
$one = call("$BASE/ingest/documents/$eid", $H);
// Tombstone it (purge=true erases immediately)
call("$BASE/ingest/documents/$eid?purge=true", $H, "DELETE");
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
var base = "https://your-assistant/api/v1";
var key = System.getenv("NAXIS_INGEST_KEY");
var http = HttpClient.newHttpClient();
java.util.function.BiFunction<String, String, String> call = (path, method) -> {
try {
var req = HttpRequest.newBuilder(URI.create(base + path))
.header("Authorization", "Bearer " + key)
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
return http.send(req, HttpResponse.BodyHandlers.ofString()).body();
} catch (Exception e) { throw new RuntimeException(e); }
};
// List indexed documents
var docs = call.apply("/ingest/documents?limit=20&prefix=crm%2F", "GET");
// One document and its status — percent-encode the external_id
var eid = URLEncoder.encode("crm/deal/8842", StandardCharsets.UTF_8);
var one = call.apply("/ingest/documents/" + eid, "GET");
// Tombstone it (purge=true erases immediately)
call.apply("/ingest/documents/" + eid + "?purge=true", "DELETE");
using System.Net;
var baseUrl = "https://your-assistant/api/v1";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("NAXIS_INGEST_KEY"));
// List indexed documents
var docs = await http.GetStringAsync($"{baseUrl}/ingest/documents?limit=20&prefix=crm%2F");
// One document and its status — percent-encode the external_id
var eid = WebUtility.UrlEncode("crm/deal/8842");
var one = await http.GetStringAsync($"{baseUrl}/ingest/documents/{eid}");
// Tombstone it (purge=true erases immediately)
await http.DeleteAsync($"{baseUrl}/ingest/documents/{eid}?purge=true");
package main
import (
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-assistant/api/v1"
key := os.Getenv("NAXIS_INGEST_KEY")
call := func(method, path string) string {
req, _ := http.NewRequest(method, base+path, nil)
req.Header.Set("Authorization", "Bearer "+key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
return string(body)
}
// List indexed documents
docs := call("GET", "/ingest/documents?limit=20&prefix="+url.QueryEscape("crm/"))
// One document and its status — percent-encode the external_id
eid := url.PathEscape("crm/deal/8842")
one := call("GET", "/ingest/documents/"+eid)
// Tombstone it (purge=true erases immediately)
call("DELETE", "/ingest/documents/"+eid+"?purge=true")
_, _ = docs, one
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base = "https://your-assistant/api/v1";
let key = std::env::var("NAXIS_INGEST_KEY")?;
let http = reqwest::Client::new();
// List indexed documents
let docs: serde_json::Value = http
.get(format!("{base}/ingest/documents"))
.query(&[("limit", "20"), ("prefix", "crm/")])
.bearer_auth(&key)
.send()
.await?
.json()
.await?;
// One document and its status — percent-encode the external_id
let eid = urlencoding::encode("crm/deal/8842").into_owned();
let one: serde_json::Value = http
.get(format!("{base}/ingest/documents/{eid}"))
.bearer_auth(&key)
.send()
.await?
.json()
.await?;
// Tombstone it (purge=true erases immediately)
http.delete(format!("{base}/ingest/documents/{eid}?purge=true"))
.bearer_auth(&key)
.send()
.await?;
let _ = (docs, one);
Ok(())
}
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"}'
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 this 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 this 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
<?php
$BASE = "https://your-assistant/api/v1";
$KEY = getenv("NAXIS_MESSAGE_KEY");
$ch = curl_init("$BASE/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"text" => "When does this contract renew?",
"end_user" => "u-3391",
]),
]);
$answer = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $answer["text"], "\n";
foreach ($answer["citations"] as $c) {
echo $c["n"], " ", $c["breadcrumb"], "\n";
}
// Continue the thread: pass $answer["conversation_id"] on the next turn
import java.net.URI;
import java.net.http.*;
import java.util.Map;
import com.fasterxml.jackson.databind.*;
var base = "https://your-assistant/api/v1";
var key = System.getenv("NAXIS_MESSAGE_KEY");
var json = new ObjectMapper();
var request = HttpRequest.newBuilder(URI.create(base + "/messages"))
.header("Authorization", "Bearer " + key)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(
Map.of("text", "When does this contract renew?", "end_user", "u-3391"))))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
JsonNode answer = json.readTree(res.body());
System.out.println(answer.get("text").asText());
for (JsonNode c : answer.get("citations")) {
System.out.println(c.get("n") + " " + c.get("breadcrumb").asText());
}
// Continue the thread: pass answer.get("conversation_id") on the next turn
using System.Net.Http.Json;
using System.Text.Json;
var baseUrl = "https://your-assistant/api/v1";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("NAXIS_MESSAGE_KEY"));
var res = await http.PostAsJsonAsync($"{baseUrl}/messages", new
{
text = "When does this contract renew?",
end_user = "u-3391",
});
var answer = await res.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(answer.GetProperty("text").GetString());
foreach (var c in answer.GetProperty("citations").EnumerateArray())
Console.WriteLine($"{c.GetProperty("n")} {c.GetProperty("breadcrumb")}");
// Continue the thread: pass answer.GetProperty("conversation_id") on the next turn
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
type citation struct {
N int `json:"n"`
Breadcrumb string `json:"breadcrumb"`
SourceURI string `json:"source_uri"`
}
func main() {
base := "https://your-assistant/api/v1"
payload, _ := json.Marshal(map[string]string{
"text": "When does this contract renew?",
"end_user": "u-3391",
})
req, _ := http.NewRequest("POST", base+"/messages", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("NAXIS_MESSAGE_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var answer struct {
Text string `json:"text"`
Abstained bool `json:"abstained"`
ConversationID string `json:"conversation_id"`
Citations []citation `json:"citations"`
}
json.NewDecoder(res.Body).Decode(&answer)
fmt.Println(answer.Text)
for _, c := range answer.Citations {
fmt.Println(c.N, c.Breadcrumb)
}
// Continue the thread: pass answer.ConversationID on the next turn
}
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base = "https://your-assistant/api/v1";
let key = std::env::var("NAXIS_MESSAGE_KEY")?;
let answer: serde_json::Value = reqwest::Client::new()
.post(format!("{base}/messages"))
.bearer_auth(&key)
.json(&json!({
"text": "When does this contract renew?",
"end_user": "u-3391"
}))
.send()
.await?
.json()
.await?;
println!("{}", answer["text"].as_str().unwrap_or_default());
for c in answer["citations"].as_array().into_iter().flatten() {
println!("{} {}", c["n"], c["breadcrumb"]);
}
// Continue the thread: pass answer["conversation_id"] on the next turn
Ok(())
}
| Field | Controls |
|---|---|
text | The 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"}'
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 this contract renew?", "end_user": "u-3391"},
stream=True,
) as r:
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue # blank line between messages
msg = json.loads(line[5:])
if msg["type"] == "delta":
print(msg["text"], end="", flush=True)
elif msg["type"] == "reset":
print("\r", end="") # discard what you've printed so far
elif msg["type"] == "final":
print("\n", msg["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 this 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 messages = buffer.split("\n\n");
buffer = messages.pop(); // keep the incomplete tail
for (const line of messages) {
if (!line.startsWith("data:")) continue;
const msg = JSON.parse(line.slice(5));
if (msg.type === "delta") process.stdout.write(msg.text);
else if (msg.type === "final") console.log("\n", msg.citations);
}
}
<?php
$BASE = "https://your-assistant/api/v1";
$KEY = getenv("NAXIS_MESSAGE_KEY");
$ch = curl_init("$BASE/messages/stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $KEY",
"Content-Type: application/json",
"Accept: text/event-stream",
],
CURLOPT_POSTFIELDS => json_encode([
"text" => "When does this contract renew?",
"end_user" => "u-3391",
]),
// called as each chunk arrives; keep the incomplete tail between calls
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$buffer) {
$buffer .= $chunk;
while (($nl = strpos($buffer, "\n")) !== false) {
$line = substr($buffer, 0, $nl);
$buffer = substr($buffer, $nl + 1);
if (strncmp($line, "data:", 5) !== 0) {
continue; // blank line between messages
}
$msg = json_decode(substr($line, 5), true);
if ($msg["type"] === "delta") {
echo $msg["text"];
} elseif ($msg["type"] === "reset") {
echo "\r"; // discard what you've printed
} elseif ($msg["type"] === "final") {
echo "\n", json_encode($msg["citations"]), "\n";
}
}
return strlen($chunk);
},
]);
$buffer = "";
curl_exec($ch);
curl_close($ch);
import java.net.URI;
import java.net.http.*;
import java.util.Map;
import com.fasterxml.jackson.databind.*;
var base = "https://your-assistant/api/v1";
var key = System.getenv("NAXIS_MESSAGE_KEY");
var json = new ObjectMapper();
var request = HttpRequest.newBuilder(URI.create(base + "/messages/stream"))
.header("Authorization", "Bearer " + key)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(
Map.of("text", "When does this contract renew?", "end_user", "u-3391"))))
.build();
HttpResponse<java.util.stream.Stream<String>> res = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofLines());
res.body().forEach(line -> {
if (!line.startsWith("data:")) return; // blank line between messages
try {
JsonNode msg = json.readTree(line.substring(5));
switch (msg.get("type").asText()) {
case "delta" -> System.out.print(msg.get("text").asText());
case "reset" -> System.out.print("\r"); // discard partial text
case "final" -> System.out.println("\n" + msg.get("citations"));
default -> { }
}
} catch (Exception e) { throw new RuntimeException(e); }
});
using System.Net.Http.Json;
using System.Text.Json;
var baseUrl = "https://your-assistant/api/v1";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("NAXIS_MESSAGE_KEY"));
var req = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/messages/stream")
{
Content = JsonContent.Create(new
{
text = "When does this contract renew?",
end_user = "u-3391",
}),
};
req.Headers.Accept.Add(new("text/event-stream"));
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
while (await reader.ReadLineAsync() is string line)
{
if (!line.StartsWith("data:")) continue; // blank line between messages
var msg = JsonSerializer.Deserialize<JsonElement>(line[5..]);
switch (msg.GetProperty("type").GetString())
{
case "delta": Console.Write(msg.GetProperty("text").GetString()); break;
case "reset": Console.Write("\r"); break; // discard partial text
case "final": Console.WriteLine("\n" + msg.GetProperty("citations")); break;
}
}
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
)
func main() {
base := "https://your-assistant/api/v1"
payload, _ := json.Marshal(map[string]string{
"text": "When does this contract renew?",
"end_user": "u-3391",
})
req, _ := http.NewRequest("POST", base+"/messages/stream", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("NAXIS_MESSAGE_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
scanner := bufio.NewScanner(res.Body)
scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "data:") {
continue // blank line between messages
}
var msg struct {
Type string `json:"type"`
Text string `json:"text"`
Citations json.RawMessage `json:"citations"`
}
json.Unmarshal([]byte(line[5:]), &msg)
switch msg.Type {
case "delta":
fmt.Print(msg.Text)
case "reset":
fmt.Print("\r") // discard what you've printed so far
case "final":
fmt.Printf("\n%s\n", msg.Citations)
}
}
}
use futures_util::StreamExt;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base = "https://your-assistant/api/v1";
let key = std::env::var("NAXIS_MESSAGE_KEY")?;
let mut stream = reqwest::Client::new()
.post(format!("{base}/messages/stream"))
.bearer_auth(&key)
.header("Accept", "text/event-stream")
.json(&json!({
"text": "When does this contract renew?",
"end_user": "u-3391"
}))
.send()
.await?
.bytes_stream();
let mut buffer = String::new();
while let Some(chunk) = stream.next().await {
buffer.push_str(&String::from_utf8_lossy(&chunk?));
while let Some(nl) = buffer.find('\n') {
let line: String = buffer.drain(..=nl).collect();
let Some(payload) = line.trim_end().strip_prefix("data:") else {
continue; // blank line between messages
};
let msg: serde_json::Value = serde_json::from_str(payload)?;
match msg["type"].as_str() {
Some("delta") => print!("{}", msg["text"].as_str().unwrap_or_default()),
Some("reset") => print!("\r"), // discard partial text
Some("final") => println!("\n{}", msg["citations"]),
_ => {}
}
}
}
Ok(())
}
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 & 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]["conversation_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].conversation_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 });
<?php
$BASE = "https://your-assistant/api/v1";
$H = ["Authorization: Bearer " . getenv("NAXIS_MESSAGE_KEY")];
function call(string $url, array $h, string $method = "GET"): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $h,
CURLOPT_CUSTOMREQUEST => $method,
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true) ?? [];
}
// List the caller's conversations
$conversations = call("$BASE/conversations", $H);
// One conversation's full transcript with citations
$cid = $conversations[0]["conversation_id"];
$thread = call("$BASE/conversations/$cid", $H);
// Erase it
call("$BASE/conversations/$cid", $H, "DELETE");
import java.net.URI;
import java.net.http.*;
import com.fasterxml.jackson.databind.*;
var base = "https://your-assistant/api/v1";
var key = System.getenv("NAXIS_MESSAGE_KEY");
var json = new ObjectMapper();
var http = HttpClient.newHttpClient();
java.util.function.BiFunction<String, String, JsonNode> call = (path, method) -> {
try {
var req = HttpRequest.newBuilder(URI.create(base + path))
.header("Authorization", "Bearer " + key)
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
return json.readTree(http.send(req, HttpResponse.BodyHandlers.ofString()).body());
} catch (Exception e) { throw new RuntimeException(e); }
};
// List the caller's conversations
JsonNode conversations = call.apply("/conversations", "GET");
// One conversation's full transcript with citations
var cid = conversations.get(0).get("conversation_id").asText();
JsonNode thread = call.apply("/conversations/" + cid, "GET");
// Erase it
call.apply("/conversations/" + cid, "DELETE");
using System.Net.Http.Json;
using System.Text.Json;
var baseUrl = "https://your-assistant/api/v1";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("NAXIS_MESSAGE_KEY"));
// List the caller's conversations
var conversations = await http.GetFromJsonAsync<JsonElement>($"{baseUrl}/conversations");
// One conversation's full transcript with citations
var cid = conversations[0].GetProperty("conversation_id").GetString();
var thread = await http.GetFromJsonAsync<JsonElement>($"{baseUrl}/conversations/{cid}");
// Erase it
await http.DeleteAsync($"{baseUrl}/conversations/{cid}");
package main
import (
"encoding/json"
"net/http"
"os"
)
func main() {
base := "https://your-assistant/api/v1"
key := os.Getenv("NAXIS_MESSAGE_KEY")
call := func(method, path string, out any) {
req, _ := http.NewRequest(method, base+path, nil)
req.Header.Set("Authorization", "Bearer "+key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
if out != nil {
json.NewDecoder(res.Body).Decode(out)
}
}
// List the caller's conversations
var conversations []struct {
ConversationID string `json:"conversation_id"`
FirstQuestion string `json:"first_question"`
}
call("GET", "/conversations", &conversations)
// One conversation's full transcript with citations
cid := conversations[0].ConversationID
var thread map[string]any
call("GET", "/conversations/"+cid, &thread)
// Erase it
call("DELETE", "/conversations/"+cid, nil)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base = "https://your-assistant/api/v1";
let key = std::env::var("NAXIS_MESSAGE_KEY")?;
let http = reqwest::Client::new();
// List the caller's conversations
let conversations: serde_json::Value = http
.get(format!("{base}/conversations"))
.bearer_auth(&key)
.send()
.await?
.json()
.await?;
// One conversation's full transcript with citations
let cid = conversations[0]["conversation_id"].as_str().unwrap();
let thread: serde_json::Value = http
.get(format!("{base}/conversations/{cid}"))
.bearer_auth(&key)
.send()
.await?
.json()
.await?;
// Erase it
http.delete(format!("{base}/conversations/{cid}"))
.bearer_auth(&key)
.send()
.await?;
let _ = thread;
Ok(())
}
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
| Status | Meaning |
|---|---|
202 | Ingest accepted; indexing runs in the background (carries a job id). |
400 | Malformed 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. |
401 | Missing or unknown Bearer key. |
404 | No document or conversation matches that id — including one that exists but belongs to a different key. |
413 | Document over 50 MB, or request over 100 MB. |
422 | The request body itself doesn't match the schema (wrong type,
missing required field) — a list-shaped {"detail": [...]} body, not the
plain string below. |
423 | This deployment is currently locked
({"error": "license_locked", …}) — contact your administrator. |
429 | Rate 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. |
503 | The 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.
/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.
/api/v1/openapi.json: point your generator
or Postman at it.
Last updated 3 Sep 2026