Skip to content

Ask workspace documents from your app

Your policies, manuals, circulars and price lists already sit in ZenithAI workspaces, where people ask about them in chat. POST /workspace brings the same answers to your own software: the intranet, a help desk, a branch app, a bot in your team chat, a script that checks contracts every night.

You send a question. ZenithAI finds the answer in the workspace’s documents, on your own server, and sends back the answer, the documents it came from, and a found flag that is false when the documents don’t say. There is no index to build, no embeddings to store and no retrieval code to write.

  1. Put the documents in a workspace, or use one you already have. See Workspaces.
  2. Create an API key on the API keys page. See API keys. The key answers as you: it can ask every workspace you can open, and nothing else.
  3. Keep the key in an environment variable, API_KEY, on the machine that runs your code.

Inside ZenithAI, Settings › API guide has a Workspace tab that sums up this endpoint, with your server’s base URL.

ZenithAI Settings showing the API guide's Workspace section with the base URL and a POST /workspace requestZenithAI Settings showing the API guide's Workspace section with the base URL and a POST /workspace request
Settings › API guide › Workspace: the same endpoint, explained inside the app. The server address shown is an example.

GET /workspace lists the workspaces your key can use, with their documents.

import os, requests
H = {"Authorization": "Bearer " + os.environ["API_KEY"]}
r = requests.get("https://dummydomain/workspace", headers=H, timeout=60)
r.raise_for_status()
for ws in r.json()["data"]:
print(ws["workspace_id"], ws["scope"], ws["counts"]["ready"], "ready |", ws["name"])

Copy the workspace_id of the workspace you want. Organisation workspaces start with org_. The examples below read it from an environment variable, WORKSPACE_ID.

You can also find it in the app. Open the workspace in Settings › Workspaces: the Workspace API card, after Knowledge, shows the Workspace ID with a Copy button, an Example request with this workspace already filled in, and See the API guide and Get an API key. The card appears when the API guide is on for your server.

ZenithAI workspace settings with the Workspace API card: workspace ID, Copy buttons and an example curl request to POST /workspaceZenithAI workspace settings with the Workspace API card: workspace ID, Copy buttons and an example curl request to POST /workspace
Each workspace shows its ID and a ready-to-copy request. An example with fictional data; the server address is illustrative.
import os, requests
H = {"Authorization": "Bearer " + os.environ["API_KEY"]}
r = requests.post("https://dummydomain/workspace", headers=H, timeout=200, json={
"workspace_id": os.environ["WORKSPACE_ID"],
"question": "How many days of annual leave do permanent employees get?",
})
r.raise_for_status()
a = r.json()
if a["found"]:
print(a["answer"])
print("Sources:", ", ".join(s["name"] for s in a["sources"]))
else:
print("Not in the documents:", a["answer"])

A typical answer, shortened:

{
"object": "workspace.answer",
"model": "fast",
"status": "completed",
"found": true,
"answer": "Permanent employees get **24 days** of annual leave per calendar year (Leave Policy.pdf).",
"sources": [{"index": 1, "name": "Leave Policy.pdf", "source_type": "file", "used": false},
{"index": 2, "name": "Monthly sales.csv", "source_type": "file", "used": false}],
"coverage": {"mode": "whole", "documents_selected": 2, "documents_checked": 2, "documents_used": null}
}

Every field is described in the API reference.

Three fields tell your program how far to trust an answer.

  • found. false means the documents don’t cover the question. The answer then says so and names the documents it checked, and sources is empty. Show your own message, offer a person to talk to, or log the gap so someone adds the missing document. Never present a found: false answer as a fact.
  • sources. The documents behind the answer. When the whole workspace is asked, every document is given to the model, so the server can’t say which one the answer came from, and they all say used: false. Ask one document (next section) and its source says used: true.
  • status. incomplete means the answer was cut off at the length limit. Ask for a shorter answer, raise max_output_tokens, or use streaming.

When you know which document holds the answer, send its id. The answer is faster, uses fewer tokens and its source is exact. Document ids come from GET /workspace?workspace_id=....

import os, requests
H = {"Authorization": "Bearer " + os.environ["API_KEY"]}
a = requests.post("https://dummydomain/workspace", headers=H, timeout=200, json={
"workspace_id": os.environ["WORKSPACE_ID"],
"knowledge": {"document_ids": [os.environ["DOCUMENT_ID"]]},
"question": "How many days of unused annual leave can be carried forward?",
}).json()
print(a["found"], a["answer"])
print([(s["name"], s["used"]) for s in a["sources"]])

Asking several chosen documents at once is planned for a later release. Today it is the whole workspace or one document.

A CSV or Excel file in the workspace can answer questions such as totals and averages. ZenithAI calculates the result rather than reading it from text, and coverage.mode is compute.

import os, requests
H = {"Authorization": "Bearer " + os.environ["API_KEY"]}
a = requests.post("https://dummydomain/workspace", headers=H, timeout=200, json={
"workspace_id": os.environ["WORKSPACE_ID"],
"question": "What were total sales from January to April?",
}).json()
print(a["coverage"]["mode"], "|", a["answer"])

The server keeps no conversation, and stores neither the question nor the answer. Your program keeps the turns and sends them back in history, ending with the assistant’s last answer.

import os, requests
H = {"Authorization": "Bearer " + os.environ["API_KEY"]}
URL, WS = "https://dummydomain/workspace", os.environ["WORKSPACE_ID"]
history = []
for question in ["How many days of annual leave do permanent employees get?", "And how many sick days?"]:
a = requests.post(URL, headers=H, timeout=200,
json={"workspace_id": WS, "question": question, "history": history}).json()
print("Q:", question, "\nA:", a["answer"], "\n")
history += [{"role": "user", "content": question}, {"role": "assistant", "content": a["answer"]}]
history = history[-20:] # at most 20 turns

Summaries and questions across many documents can take a while. With "stream": true you get progress and the text as it is written, and the 180-second limit for a single answer doesn’t apply.

import json, os, requests
H = {"Authorization": "Bearer " + os.environ["API_KEY"]}
body = {"workspace_id": os.environ["WORKSPACE_ID"], "question": "Summarise the leave policy in three lines.",
"stream": True}
final, event = None, None
with requests.post("https://dummydomain/workspace", headers=H, json=body, stream=True, timeout=900) as r:
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
data = json.loads(line[6:])
if event == "answer.delta":
print(data["delta"], end="", flush=True)
elif event == "answer.reset":
print("\n[the text above was corrected]")
elif event == "answer.completed":
final = data # the final answer: found, sources, usage
elif event == "answer.failed":
raise RuntimeError(data["error"]["code"])
print("\nfound:", final["found"])

Treat the deltas as a preview. answer.completed carries the final answer, the same object as a non-stream call. All events are listed in the reference.

"format": "text" returns only the answer, which suits a chat bot or a shell script. found and the model come in the X-Zenith-Found and X-Zenith-Model headers.

Terminal window
curl --fail-with-body -s -D headers.txt "https://dummydomain/workspace" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d "{\"workspace_id\": \"$WORKSPACE_ID\", \"question\": \"How many sick days do employees get?\", \"format\": \"text\"}"
grep -i "x-zenith-found" headers.txt

Your software can also keep a workspace up to date: add a new circular, policy or contract the moment it is approved, then ask it. POST /workspace/documents adds one .txt, .pdf, .docx, .xlsx or .csv file to one of your own personal workspaces. It needs two things from your administrator: adding documents switched on for the server, and Can add documents ticked for your key. Without them you get 404 api_disabled or 403 key_not_allowed.

The pattern is always the same: choose a 12-character client_document_id, send the file as base64, then ask for the status with that id until it is ready, and keep the server document_id for your questions.

import base64, os, random, string, time
from pathlib import Path
import requests
BASE = "https://dummydomain"
H = {"Authorization": "Bearer " + os.environ["API_KEY"]}
WS = os.environ["WORKSPACE_ID"]
path = Path("Leave Policy.pdf")
# 1. choose your id first, and keep it: it finds the upload again after a time-out
cid = "".join(random.choices(string.ascii_letters + string.digits, k=12))
print("client_document_id:", cid)
# 2. send the file as one line of base64 (a scanned PDF can take minutes: 300 s time-out)
body = {"workspace_id": WS, "client_document_id": cid, "name": path.name,
"data": base64.b64encode(path.read_bytes()).decode("ascii")}
try:
r = requests.post(BASE + "/workspace/documents", headers=H, json=body, timeout=300)
if r.status_code not in (200, 202):
print(r.status_code, r.json()["code"])
raise SystemExit(1)
except requests.Timeout:
print("Timed out: asking for the status, not sending again")
# 3. ask for the status until it is ready or failed
for _ in range(150):
s = requests.get(f"{BASE}/workspace/documents/{cid}", headers=H,
params={"workspace_id": WS}, timeout=60).json()
if s.get("status") in ("ready", "failed") or "code" in s:
break
time.sleep(4)
print(s.get("status"), s.get("document_id"), s.get("warnings"))
# 4. ask the new document with the server document_id
if s.get("status") == "ready":
a = requests.post(BASE + "/workspace", headers=H, timeout=200, json={
"workspace_id": WS, "knowledge": {"document_ids": [s["document_id"]]},
"question": "How many days of annual leave do permanent employees get?"}).json()
print(a["answer"] if a.get("found") else "Not in the document.")

If the upload times out, don’t send the file again: ask for the status with the same client_document_id, as step 3 does. A resend while the first upload is still running gets 429 too_many_uploads; once it has finished, the same id and file return 200 with the current status and never make a second copy.

Ready-made PowerShell and bash blocks, every status and error code, the limits object and the upload warnings are in the API reference: Add a document.

Where the file comes from What your program does
A document management system Every evening, adds the circulars approved that day, so staff can ask them the next morning
An HR portal When a policy is published, adds it to the HR workspace that the intranet asks
A contract intake form Adds the signed contract, waits for ready, then asks it one question: the notice period, the renewal date
A reporting job Adds the month’s sales CSV, so questions such as totals by region are calculated from the file
Where the question comes from What it asks Workspace
Intranet search box “What is the hotel limit for a manager in a metro city?” HR and travel policies
Help desk, before an agent replies “What is the warranty on the 1.5 tonne model?” Product manuals and warranty terms
Branch staff app “Which documents are needed to open a current account for a partnership firm?” Circulars and process notes
Sales tool “Which plans include on-site support?” Price lists and plan sheets
A nightly job “Does this contract allow termination without notice?” One contract, asked with knowledge.document_ids
Team chat bot Any question, answered in plain text with format: "text" An organisation workspace

For each one, decide in advance what happens when found is false. That gap list is also your to-do list for the next documents to add.

  • Check found before you show, store or act on an answer.
  • Give the integration its own account with access to only the workspaces it needs, and create the key there. Anyone holding the key can read everything its owner can open.
  • Keep keys on the server. Never put them in browser or mobile app code. Call /workspace from your back end and pass the answer on.
  • Keep workspaces current. Answers come from the documents in the workspace, so remove superseded versions when a new one is added.
  • Retry only when retryable is true, waiting Retry-After seconds when it is given. See errors.
  • Use metadata to tag requests with your own ids. It comes back unchanged and is never logged.

Common questions

How do I ask questions of my company documents from my own application?

Put the documents in a ZenithAI workspace, create an API key, call GET /workspace to find the workspace id, then POST /workspace with that id and a question. The answer comes only from those documents, with the source documents listed and a found flag.

Can ZenithAI answer only from my documents and say when it doesn't know?

Yes. POST /workspace never searches the web. When the documents do not cover the question it returns found false, says the answer is not in the documents and names the documents it checked, instead of guessing.

Does the workspace API respect who can see which documents?

Yes. A key answers as the person who created it and sees only the workspaces that person can open in the app. If their access is removed, the key loses it at once. Create integration keys from an account with only the access the integration needs.

Is /workspace a RAG API?

It does the job of a retrieval-augmented generation API without the plumbing. ZenithAI prepares, indexes and searches the documents and writes the answer on your own server. Your program sends a question and gets back an answer, sources and coverage.

Can my application add documents to a ZenithAI workspace?

Yes, when your administrator has switched it on and ticked Can add documents for your key. POST /workspace/documents adds a txt, pdf, docx, xlsx or csv file to one of your own personal workspaces. Ask for its status with your client_document_id until it is ready, then ask it with the server document_id.

Can I ask a spreadsheet a question through /workspace?

Yes. For questions such as a total or an average over a CSV or Excel file in the workspace, ZenithAI calculates the result instead of reading it from text, and coverage.mode is compute.