API reference: Workspace
/workspace answers questions from a ZenithAI workspace, with an API key. The answer is the
same one the ZenithAI app gives in that workspace, and it comes only from the workspace’s documents: the
endpoint never searches the web. A program can also add a document to one of its owner’s own
workspaces and follow it until it can be asked. For a walk-through with code, read the
workspace guide.
| Method | Path | Purpose |
|---|---|---|
GET |
/workspace |
List the workspaces, and their documents, that the key can use |
POST |
/workspace |
Ask a question of a whole workspace or of one document in it |
POST |
/workspace/documents |
Add one file to one of your own personal workspaces, when your administrator has enabled it |
GET |
/workspace/documents/{id}?workspace_id={workspace_id} |
The status of an added file, by your client_document_id or the server document_id |
Asking stores nothing: no chat is saved and questions and answers are not kept. A document you add is different: it is stored in the workspace like any document added in the web app, and stays there until it is deleted in the web app.
Authentication
Section titled “Authentication”Send a key created by a user on the API keys page, as Authorization: Bearer YOUR_API_KEY or
X-API-Key: YOUR_API_KEY.
- The key answers as its owner. It sees the workspaces that person can open in the app: their own, ones shared with them, and organisation workspaces they are a member of. Nothing else.
- If the owner loses access to a workspace (it is unshared, or they leave the organisation), the key loses it
at once, even in the middle of an answer (
404 workspace_access_lost). - A key with no owner gets
403 api_key_owner_required. A wrong or missing key gets401 api_key_required, and 20 wrong keys from one address in 10 minutes (counted together with/v1) gives429. - Adding documents needs more. Only a key that an administrator has ticked Can add documents may add
files, and only to its owner’s own personal workspaces. Every other key, including every key made before
17-10-2026, stays read-only and gets
403 key_not_allowed. See Add a document.
Copy-paste commands
Section titled “Copy-paste commands”Ready-made commands for Windows PowerShell and for Linux or macOS. Copy a whole block from its first line to its
last, paste it, and change only YOUR_WORKSPACE_ID, YOUR_DOCUMENT_ID and the question. The line continuations
(a backtick or a backslash at the end of a line) are already in place. The bash blocks also work in zsh, the
macOS default. Replace https://dummydomain with your ZenithAI server, or set it once in Your server at the
top of this site.
Step 1. Give your API key, once per window. Run this on its own first. It asks for the key, so the key is not typed into a command or kept in your command history. Paste the key when it asks, then press Enter.
$KEY = Read-Host "API key"printf 'API key: '; read -rs KEY; echoWhere the ids come from
workspace_id: fromGET /workspace(call 1 below), fielddata[].workspace_id.document_id: fromGET /workspace?workspace_id=..., fielddocuments[].document_id. Every answer also carries it, insources[].document_id.
1. List your workspaces and their documents
Section titled “1. List your workspaces and their documents”The first command lists all your workspaces with their documents; the second shows one workspace.
$BASE = "https://dummydomain"$H = @{ Authorization = "Bearer $KEY" }
# all your workspaces, with their documents$r = $null$r = Invoke-RestMethod -Uri "$BASE/workspace" -Headers $Hforeach ($w in $r.data) { "$($w.workspace_id) $($w.name)" foreach ($d in $w.documents) { " $($d.document_id) $($d.name) [$($d.status)]" }}
# one workspace, with its documents$r = $null$r = Invoke-RestMethod -Uri "$BASE/workspace?workspace_id=YOUR_WORKSPACE_ID" -Headers $H$r.data[0].documents | Select-Object document_id, name, statusBASE="https://dummydomain"curl -s -w "\n" "$BASE/workspace" -H "Authorization: Bearer $KEY"curl -s -w "\n" "$BASE/workspace?workspace_id=YOUR_WORKSPACE_ID" -H "Authorization: Bearer $KEY"If jq is installed, add | jq -r '.data[] | .workspace_id + " " + .name' to the end of the first curl
line for a short list.
2. Ask the whole workspace
Section titled “2. Ask the whole workspace”$BASE = "https://dummydomain"$H = @{ Authorization = "Bearer $KEY" }
$body = @{ workspace_id = 'YOUR_WORKSPACE_ID' question = 'How many days of annual leave do permanent employees get?'} | ConvertTo-Json -Depth 4
$r = $null$r = Invoke-RestMethod -Method Post -Uri "$BASE/workspace" -Headers $H -ContentType "application/json" -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
$r.answer$r.found$r.sources | Select-Object index, document_id, name, usedThe values are in single quotes, so a $ in a question (for example $500) is sent as written. If your question
contains a single quote, write it twice (it''s). GetBytes sends the body as UTF-8, so Hindi and other
languages arrive correctly in Windows PowerShell 5.1.
BASE="https://dummydomain"curl -s -w "\n" "$BASE/workspace" \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d @- <<'JSON'{ "workspace_id": "YOUR_WORKSPACE_ID", "question": "How many days of annual leave do permanent employees get?"}JSONIf jq is installed, put | jq -r '.answer' at the end of the line -d @- <<'JSON' to print only the answer.
3. Ask one document
Section titled “3. Ask one document”knowledge.document_ids takes exactly one document_id. The answer’s source then comes back with used: true.
$BASE = "https://dummydomain"$H = @{ Authorization = "Bearer $KEY" }
$body = @{ workspace_id = 'YOUR_WORKSPACE_ID' question = 'How many days of unused annual leave can be carried forward?' knowledge = @{ document_ids = @('YOUR_DOCUMENT_ID') }} | ConvertTo-Json -Depth 4
$r = $null$r = Invoke-RestMethod -Method Post -Uri "$BASE/workspace" -Headers $H -ContentType "application/json" -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
$r.answer$r.found$r.sources | Select-Object index, document_id, name, usedBASE="https://dummydomain"curl -s -w "\n" "$BASE/workspace" \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d @- <<'JSON'{ "workspace_id": "YOUR_WORKSPACE_ID", "knowledge": { "document_ids": ["YOUR_DOCUMENT_ID"] }, "question": "How many days of unused annual leave can be carried forward?"}JSONcurl.exe in Windows PowerShell
Section titled “curl.exe in Windows PowerShell”Windows PowerShell 5.1 removes the inner double quotes of a JSON string passed to curl.exe on the command
line, so the server receives broken JSON. Write the body to a file and send the file. This writes body.json in
the current folder, so first move to a normal folder of yours (for example cd $HOME), not
C:\Windows\System32.
$BASE = "https://dummydomain"
# list workspaces (no body, so no quoting problem)curl.exe -s -w "\n" "$BASE/workspace" -H "Authorization: Bearer $KEY"
# ask the whole workspaceSet-Content -Path body.json -Encoding ascii -NoNewline -Value '{"workspace_id":"YOUR_WORKSPACE_ID","question":"How many days of annual leave do permanent employees get?"}'curl.exe -s -w "\n" "$BASE/workspace" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" --data-binary "@body.json"
# ask one documentSet-Content -Path body.json -Encoding ascii -NoNewline -Value '{"workspace_id":"YOUR_WORKSPACE_ID","knowledge":{"document_ids":["YOUR_DOCUMENT_ID"]},"question":"How many days of unused annual leave can be carried forward?"}'curl.exe -s -w "\n" "$BASE/workspace" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" --data-binary "@body.json"-Encoding ascii keeps the file plain. For a question in Hindi or another language, use the Invoke-RestMethod
blocks above.
If a command fails
Section titled “If a command fails”400 invalid_requestwithout aparamfield: the server did not receive a valid JSON body. Use the blocks as they are. In Windows PowerShell, never pass the JSON tocurl.exeas a quoted string. If you split a command over several lines, every line but the last must end with the continuation character (a backtick in PowerShell, a backslash in bash); a missing one sends the request with no body.400 invalid_requestwith aparam: that field is missing or has the wrong type, for examplequestion.401 api_key_required: the key is missing or wrong. Run step 1 again and paste the key without spaces or quotes.404 workspace_not_found: the key can’t open thatworkspace_id. Copy it from call 1.404 knowledge_not_found: thedocument_idis not in that workspace. Copy it fromGET /workspace?workspace_id=....415 unsupported_media_type: theContent-Type: application/jsonheader is missing.- Anything else: see Errors.
GET /workspace
Section titled “GET /workspace”GET https://dummydomain/workspaceGET https://dummydomain/workspace?limit=10&include=noneGET https://dummydomain/workspace?workspace_id=WORKSPACE_ID&limit=200Authorization: Bearer YOUR_API_KEY| Parameter | Default | Meaning |
|---|---|---|
limit |
20, or 100 with workspace_id |
Workspaces per page, 1 to 50. With workspace_id: documents per page, 1 to 200 |
after |
none | The next_after value from the previous page. Cursors stop working after a server restart (400 invalid_cursor); start again from the first page |
workspace_id |
none | Return only this workspace, with its documents in pages |
include |
documents,sources |
What to list per workspace: documents, sources (website imports), both, or none |
The response is {"object": "list", "data": [...], "has_more": true, "next_after": "...", "request_id": "..."}.
While has_more is true, send next_after as after to get the next page. Personal workspaces come first,
then organisation workspaces, each sorted by name. The list never shows e-mail addresses, member lists or
workspace instructions.
| Field | Meaning |
|---|---|
workspace_id |
The id to send to POST /workspace. Organisation workspaces start with org_ |
name |
The workspace name |
scope |
personal or organization |
access |
The key owner’s access: owner, shared, member or manager |
counts |
documents, ready, not_ready and failed |
documents[] |
document_id, name, source_type, status (ready, not_ready while it is being prepared, or failed), updated (DD-MM-YYYY HH:MM, IST) and updated_at (ISO 8601). A page imported from a website also has source_id and url; a file added through the API also has the client_document_id you chose |
sources[] |
Website imports: source_id, name, pages, ready, not_ready and document_ids |
{ "object": "list", "data": [ { "workspace_id": "9f2c41d07a3b4e6c8d1f5a2b3c4d5e6f", "name": "HR policies 2026", "scope": "personal", "access": "owner", "counts": {"documents": 2, "ready": 2, "not_ready": 0, "failed": 0}, "documents": [ {"document_id": "3c1e7b52-...", "name": "Leave Policy.pdf", "source_type": "file", "status": "ready", "updated": "15-10-2026 10:14", "updated_at": "2026-10-15T10:14:01+05:30"} ] } ], "has_more": false, "next_after": null, "request_id": "7fe98b0d..."}POST /workspace
Section titled “POST /workspace”POST https://dummydomain/workspaceAuthorization: Bearer YOUR_API_KEYContent-Type: application/jsonThe body must be JSON (otherwise 415). Booleans are JSON true and false, not "true" or 1.
| Field | Type | Description |
|---|---|---|
workspace_id |
string, required | From GET /workspace |
question |
string, required | 1 to 8,000 characters |
knowledge |
object | {"document_ids": ["DOCUMENT_ID"]} answers from that one document only. Leave it out to use the whole workspace |
history |
array | Earlier turns, for follow-up questions. Default [] |
model |
string | fast (default), smart or genius, the tiers Fast, Smart and Genius |
thinking |
boolean | Let the model reason before it answers: slower, sometimes better. Default: the tier’s own setting |
stream |
boolean | true sends stream events as the answer is written |
format |
string | json (default) or text for the answer as plain text |
include_sources |
boolean | false leaves out the sources list. Default true |
max_output_tokens |
integer | The longest answer you want. Values above the tier’s limit are lowered to it. Default: your server’s answer limit |
metadata |
object | Up to 16 string pairs (key up to 64, value up to 512 characters), sent back unchanged and never logged |
language |
string | Only auto: the answer follows the language of the question |
include_excerpts |
boolean | Only false for now |
Any other field gives 400 unknown_parameter. These are known but not supported, and give
400 unsupported_parameter: web_search, tools, attachments, store, session_id,
previous_response_id, schema, format: "json_schema", knowledge.source_ids, and more than one id in
knowledge.document_ids. Asking several chosen documents or website imports at once is planned for a later
release, and the error message says so.
Whole workspace or one document
Section titled “Whole workspace or one document”- Whole workspace (no
knowledge): the answer may draw on every ready document. - One document (
knowledge.document_idswith one id): faster, uses fewer tokens, and the source is exact. A document from another workspace gives404 knowledge_not_found; one that is still being prepared gives409 knowledge_not_ready.
Follow-up questions
Section titled “Follow-up questions”The server keeps no conversation. To ask a follow-up, send the earlier turns in history:
{ "workspace_id": "9f2c41d07a3b4e6c8d1f5a2b3c4d5e6f", "question": "And how many sick days?", "history": [ {"role": "user", "content": "How many days of annual leave do permanent employees get?"}, {"role": "assistant", "content": "Permanent employees get 24 days of annual leave per calendar year."} ]}The turns alternate user, assistant, user and so on, start with user and end with assistant. Each
turn has exactly role and a non-empty content. At most 20 turns and 24,000 characters in all. Anything else
gives 400 invalid_history.
The answer
Section titled “The answer”{ "id": "wsa_60b1e8f1...", "object": "workspace.answer", "created": 1792074756, "workspace_id": "9f2c41d07a3b4e6c8d1f5a2b3c4d5e6f", "model": "fast", "status": "completed", "found": true, "answer": "Permanent employees get **24 days** of annual leave per calendar year (Leave Policy.pdf).", "sources": [ {"index": 1, "document_id": "3c1e7b52-...", "name": "Leave Policy.pdf", "source_type": "file", "url": null, "used": false}, {"index": 2, "document_id": "7a90c4d1-...", "name": "Monthly sales.csv", "source_type": "file", "url": null, "used": false} ], "coverage": {"mode": "whole", "documents_selected": 2, "documents_checked": 2, "documents_used": null, "documents_not_ready": 0}, "warnings": [], "usage": {"input_tokens": 4172, "output_tokens": 35, "total_tokens": 4207}, "thinking": false, "metadata": {}, "request_id": "09a3a955..."}| Field | Meaning |
|---|---|
id |
The answer id, wsa_.... It is not stored, so it cannot be fetched later |
object |
workspace.answer |
created |
Unix time |
workspace_id |
As sent |
model |
The tier that answered: fast, smart or genius |
status |
completed, or incomplete when the answer was cut off at the length limit; warnings then holds max_output_tokens_reached |
found |
true if the documents held the answer. false if they did not: the answer then says so and sources is empty. Check it before you use the answer |
answer |
The answer, in Markdown. It often names the documents it quotes |
sources[] |
The documents behind the answer: index, document_id, name, source_type, url and used. Left out when include_sources is false |
coverage |
mode (how the answer was made, such as whole, search, mapreduce or compute), documents_selected, documents_checked, documents_used and documents_not_ready |
warnings |
For example documents_not_ready (some documents were still being prepared and were not used), prompt_trimmed or max_output_tokens_reached |
usage |
input_tokens, output_tokens and total_tokens |
thinking |
Whether the model reasoned before answering |
metadata |
As sent |
notice |
Only when the app adds a note about which documents were used |
request_id |
Quote it when you report a problem |
What sources and coverage tell you
Section titled “What sources and coverage tell you”used: truemeans the answer is known to come from that document. One-document answers, search, comparison and per-document summary answers mark the documents they used this way.used: falsemeans the document was given to the model as context. When the whole workspace is asked, every document is given to the model and the server cannot tell which one the answer came from, so all of them sayused: falseandcoverage.documents_usedisnull. The answer text usually names the document it quotes. To know the source exactly, ask one document.mode: "compute"means the answer was calculated from a spreadsheet or CSV, for example a total over a column, rather than read from text.sourcesmay then be empty andusagemay be zero.
Plain text answers
Section titled “Plain text answers”With "format": "text" the body is only the answer (text/plain), and the details come in headers:
| Header | Meaning |
|---|---|
X-Zenith-Found |
true or false, as found |
X-Zenith-Model |
The tier that answered |
X-Zenith-Sources |
The number of sources |
X-Request-ID |
The request id |
Stream events
Section titled “Stream events”With "stream": true the response is text/event-stream. Each event has a name and one JSON data line, and
every data object carries the answer id and a sequence_number that counts from 0.
| Event | When | Data |
|---|---|---|
answer.created |
First | object, workspace_id, model, created and status: "in_progress" |
answer.status |
Progress | stage: retrieving, computing, reading or writing |
answer.sources |
Before the text, if include_sources |
sources, provisional; the final list is in answer.completed |
answer.delta |
Many times | delta, the next piece of the answer |
answer.reset |
Sometimes | Throw away the text shown so far; the corrected text follows as new deltas |
answer.completed |
Last, on success | The full answer. This is the final answer |
answer.failed |
Last, on failure | error: {code, message} and request_id |
A : ping line comes after 20 seconds without events, to keep the connection open; ignore it. There is no
[DONE] line: the stream ends after answer.completed or answer.failed. Treat the deltas as a live preview
and keep answer.completed as the result.
Add a document
Section titled “Add a document”POST /workspace/documents adds one file to one of the key owner’s own personal workspaces, and
GET /workspace/documents/{id} follows it until it can be asked. A document you add is an ordinary workspace
document: it shows in the web app and in GET /workspace, and it is answered from like any other. It stays in the
workspace until someone deletes it in the web app; the API has no delete.
Who may add documents
Section titled “Who may add documents”- Only a key ticked Can add documents. An administrator turns on its Add docs switch in Settings › API & Usage › Keys. Every other
key, including every key made before 17-10-2026, stays read-only and gets
403 key_not_allowed. - Only the owner’s own personal workspaces. An organisation workspace, a workspace shared with the owner, or
another person’s workspace answers
404 workspace_not_found. The owner’s own workspace that has been published (shared) for others answers403 workspace_read_only. - Five file types:
.txt,.pdf,.docx,.xlsxand.csv. Markdown (.md) and the old Office types (.doc,.xls) get400 unsupported_file_type, and so does a file whose content doesn’t match its extension, for example a renamed image.
Request
Section titled “Request”POST https://dummydomain/workspace/documentsAuthorization: Bearer YOUR_API_KEYContent-Type: application/json| Field | Type | Description |
|---|---|---|
workspace_id |
string, required | From GET /workspace, field data[].workspace_id. One of your own personal workspaces |
client_document_id |
string, required | Exactly 12 letters or digits (A-Z, a-z, 0-9), chosen by you before you send the file. Your handle to find this upload again. Compared without regard to upper or lower case, and unique within the workspace |
name |
string, required | The file name with its extension, 1 to 200 characters. No /, \, control characters or a leading . |
data |
string, required | The whole file as one line of standard base64. Wrapped base64 (with line breaks) and invalid characters get 400 invalid_request. A data: URL prefix is accepted and removed |
metadata |
object | Up to 16 string pairs (key up to 64 characters, value up to 512). Checked and sent back in the upload answer, but never stored: the status call doesn’t return it |
Any other field gives 400 unknown_parameter. A request bigger than the request limit is refused before it is
read (413 file_too_large), and a request with no Content-Length gets 411 length_required.
To make one-line base64: base64 -w0 file.pdf on Linux, base64 < file.pdf | tr -d '\n' on macOS (which has no
-w0), and [Convert]::ToBase64String([IO.File]::ReadAllBytes($path)) in PowerShell.
Add a file and wait until it is ready
Section titled “Add a file and wait until it is ready”- Give your API key, as in step 1 of the copy-paste commands.
- Upload. The block makes a 12-character
client_document_id, prints it before it sends, and sends the file. A scanned PDF can take several minutes because it is read with OCR, so the client time-out is 300 seconds:-TimeoutSec 300in PowerShell (its default is 100) and--max-time 300in curl. - Wait. Paste the printed
client_document_idinto the status block and run it until the status isreadyorfailed. If the upload timed out, run only the status block. Do not send the file again. - Keep the server
document_idfrom the answer, and use it from now on to ask the document.
Upload (change YOUR_WORKSPACE_ID and the file path):
$BASE = "https://dummydomain"$H = @{ Authorization = "Bearer $KEY" }$WS = 'YOUR_WORKSPACE_ID'$FILE = 'C:\Users\me\Documents\Leave Policy.pdf'
$CID = -join ((48..57)+(65..90)+(97..122) | Get-Random -Count 12 | ForEach-Object {[char]$_})"client_document_id: $CID"
$item = Get-Item -LiteralPath $FILE$data = [Convert]::ToBase64String([IO.File]::ReadAllBytes($item.FullName))$json = '{"workspace_id":"' + $WS + '","client_document_id":"' + $CID + '","name":' + ($item.Name | ConvertTo-Json -Compress) + ',"data":"' + $data + '"}'
$r = $null$r = Invoke-RestMethod -Method Post -Uri "$BASE/workspace/documents" -Headers $H -ContentType "application/json" -TimeoutSec 300 -Body ([System.Text.Encoding]::UTF8.GetBytes($json))$r.document_id$r.statusThe JSON is built by hand, not with ConvertTo-Json, so a large file stays fast in Windows PowerShell 5.1. The
file name goes through ConvertTo-Json, so quotes and other special characters in it are escaped.
BASE="https://dummydomain"WS="YOUR_WORKSPACE_ID"FILE="$HOME/Documents/Leave Policy.pdf"CID=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 12); echo "client_document_id: $CID"{ printf '{"workspace_id":"%s","client_document_id":"%s","name":"%s","data":"' "$WS" "$CID" "$(basename "$FILE")" base64 < "$FILE" | tr -d '\n' printf '"}'; } > body.jsoncurl -s -w "\n" --max-time 300 "$BASE/workspace/documents" \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ --data-binary @body.jsonThis block doesn’t escape a " or \ in the file name: rename such a file first. The server accepts such
names, and the PowerShell block escapes them.
Wait until it is ready (paste the printed id in place of YOUR_CLIENT_DOCUMENT_ID):
$BASE = "https://dummydomain"$H = @{ Authorization = "Bearer $KEY" }$WS = 'YOUR_WORKSPACE_ID'$CID = 'YOUR_CLIENT_DOCUMENT_ID'
$until = (Get-Date).AddMinutes(10)do { Start-Sleep -Seconds 4 $s = Invoke-RestMethod -Uri "$BASE/workspace/documents/$($CID)?workspace_id=$WS" -Headers $H $s.status} while (($s.status -eq 'not_ready' -or $s.status -eq 'processing') -and (Get-Date) -lt $until)"document_id: $($s.document_id)"if ($s.failed_reason) { "failed_reason: $($s.failed_reason)" }BASE="https://dummydomain"WS="YOUR_WORKSPACE_ID"CID="YOUR_CLIENT_DOCUMENT_ID"for i in $(seq 1 150); do R=$(curl -s "$BASE/workspace/documents/$CID?workspace_id=$WS" -H "Authorization: Bearer $KEY") echo "$R" echo "$R" | grep -Eq '"status": ?"(ready|failed)"|"error"' && break sleep 4doneBoth loops ask every 4 seconds for up to 10 minutes and stop at ready or failed (the bash loop also stops at
an error).
The two ids
Section titled “The two ids”| Id | Who makes it | Use it for |
|---|---|---|
client_document_id |
You, before the upload: 12 letters or digits | Finding the upload again, especially after a time-out. It works in the status call only |
document_id |
The server, when the file is stored | Everything else: asking with knowledge.document_ids, the workspace listing and the web app |
Both come back in every upload and status answer. document_id is null only while the status is
processing.
The upload answer
Section titled “The upload answer”The upload answers 202 when the file is stored and its indexing continues in the background. Its status is
usually not_ready.
{ "object": "workspace.document", "document_id": "3c1e5b52-9d0e-4b8b-8a55-0a4f3c2d1e00", "client_document_id": "Ab12Cd34Ef56", "workspace_id": "9f2c41d07a3b4e6c8d1f5a2b3c4d5e6f", "name": "Leave Policy.pdf", "source_type": "file", "status": "not_ready", "bytes": 482113, "warnings": ["ocr_used"], "created": "17-10-2026 14:05", "created_at": "2026-10-17T14:05:11+05:30", "added_by": "api:llm_9b2f3c1a", "metadata": {}, "request_id": "8a64b554...", "limits": { "max_file_bytes": 3145728, "max_request_bytes": 4259840, "workspace_bytes_limit": 104857600, "workspace_bytes_used": 482391, "daily_uploads_limit": 200, "daily_uploads_used": 1, "daily_uploads_remaining": 199, "daily_ocr_limit": 20, "daily_ocr_used": 1, "daily_ocr_remaining": 19, "max_ocr_pages": 30 }}| Field | Meaning |
|---|---|
object |
workspace.document |
document_id |
The server id. null only while status is processing |
client_document_id |
The id you chose |
workspace_id |
As sent |
name |
The file name |
source_type |
file |
status |
processing, not_ready, ready or failed, see the status values |
bytes |
The file’s size |
warnings |
Fixed tokens, see Upload warnings. [] when there are none |
created, created_at |
When it was added: DD-MM-YYYY HH:MM in IST, and ISO 8601 |
added_by |
api: and the first 12 characters of the key, which are its public prefix. Never the whole key |
failed_reason |
Only when status is failed: indexing_failed |
metadata |
As sent, {} when none was sent. In upload answers only, never in status answers |
request_id |
Quote it when you report a problem |
limits |
The live limits and your use of them, see The limits object |
The same upload can also answer:
| HTTP | status |
When |
|---|---|---|
202 |
not_ready (or ready) |
The file was stored. This is the normal answer |
202 |
processing |
The same client_document_id and the same file are already being read for another key. document_id is null |
200 |
the current status | The same client_document_id and the same file were already added. Nothing is stored twice and nothing is counted |
Check the status
Section titled “Check the status”GET https://dummydomain/workspace/documents/{id}?workspace_id={workspace_id}Authorization: Bearer YOUR_API_KEY{id}is yourclient_document_id(12 letters or digits) or the serverdocument_id. Anything else gets400 invalid_requestwithparamdocument_id.workspace_idis required. Any other query parameter gets400 unknown_parameter.- Any key may check the status, not only a key ticked Can add documents. It works on every workspace the key can open.
- The answer has the same fields as the upload answer, without
metadata. It is200for every status. A document that isn’t in that workspace gets404 document_not_found.
Ask every 3 to 5 seconds and stop at ready or failed.
The status values
Section titled “The status values”status |
Meaning |
|---|---|
processing |
Only when you ask by client_document_id: the server is still reading your file. document_id is null |
not_ready |
Stored and being indexed. It can’t be asked as a single document yet |
ready |
It can be asked |
failed |
Indexing failed. The answer also has failed_reason: indexing_failed. You can add the same file again |
After a time-out: ask for the status, don’t resend
Section titled “After a time-out: ask for the status, don’t resend”If the upload call times out, the file may still be on its way in. Ask for the status with your
client_document_id and don’t send the file again:
- While the first upload is still running, a resend from the same key gets
429 too_many_uploads, and one from another key gets202withstatus: "processing". - Once the first upload has finished, sending the same id with the same file again returns
200with the current status. It never creates a second copy and isn’t counted. - The same id with a different file gets
409 id_in_use. - A name that already exists gets
409 duplicate_name. After a time-out this usually means your first upload arrived: ask for the status of thedocument_idin that answer. - If the server restarted while it was reading your file, nothing was stored. Send it again.
Upload warnings
Section titled “Upload warnings”warnings holds fixed tokens only, never text from your file:
| Token | Meaning |
|---|---|
ocr_used |
A scanned PDF was read with OCR |
ocr_warning |
OCR left something out, for example pages after the page limit or text over the limit for one scan |
document_cleaned |
Raw e-mail or web-page source in the file was cleaned to its readable text |
xlsx_rows_truncated |
A sheet was read up to 5,000 rows; later rows were not read |
csv_header_row, xlsx_header_row |
The first row doesn’t look like column headings, so table answers may be weaker |
xlsx_several_sheets |
The workbook has more than one sheet. Table questions work best with one sheet per file |
The limits object
Section titled “The limits object”limits carries the live limits and your use of them, so your program doesn’t need to keep any numbers. The
sizes below are the limits currently in force; read limits for the real ones on your server.
| Field | Meaning |
|---|---|
max_file_bytes |
The largest file. Currently 3 MiB (3,145,728 bytes) |
max_request_bytes |
The largest request body, base64 included. Follows from the file limit: 4,259,840 bytes at 3 MiB |
workspace_bytes_limit |
The most one workspace can hold. Currently 100 MiB (104,857,600 bytes) |
workspace_bytes_used |
What this workspace holds now. null before the workspace is known |
daily_uploads_limit, daily_uploads_used, daily_uploads_remaining |
Uploads per day for the key’s owner, all of the owner’s keys together. Currently 200 |
daily_ocr_limit, daily_ocr_used, daily_ocr_remaining |
Scanned (OCR) PDFs per day for the owner. Currently 20 |
max_ocr_pages |
Pages read of a scanned PDF added through the API. Currently 30 |
limits is in every upload answer once the request has been accepted for processing, and in status answers
once the workspace is known. It is not in api_disabled, api_key_required, api_key_owner_required,
key_not_allowed, 415, 411 and 429 rate_limited answers, nor in status errors that come before the
workspace is known (an id of the wrong shape, an unknown workspace).
Errors when adding documents
Section titled “Errors when adding documents”The errors have the usual format, and are listed in the errors table. Some carry numbers or an id as extra fields:
{"error": "A document with this name already exists in this workspace. Delete it in the web app first to replace it.", "code": "duplicate_name", "retryable": false, "request_id": "5e7d...", "document_id": "3c1e5b52-9d0e-4b8b-8a55-0a4f3c2d1e00", "limits": {"max_file_bytes": 3145728, "...": "..."}}code |
Extra fields |
|---|---|
duplicate_name (409) |
document_id of the document with that name (upper and lower case ignored). null while an upload of that name is still running. To replace a file, delete the old one in the web app first |
id_in_use (409) |
document_id of the document that holds the id. null while that upload is still running |
workspace_full (409) |
limit_bytes, used_bytes and received_bytes. Delete documents in the web app to make room |
file_too_large (413) |
limit_bytes and received_bytes, for the file or for the whole request |
file_too_complex (413) |
rule and that rule’s numbers: unpacked_bytes (limit_bytes, unpacked_bytes), members (limit_members, members) or ratio (limit_ratio, ratio) |
too_many_uploads (429) |
A Retry-After: 5 header |
daily_upload_limit, daily_ocr_limit (429) |
A Retry-After header with the seconds until midnight IST |
A Word file or workbook is judged by what it unpacks to. More than 64 MiB unpacked, more than 1,000 parts in a
workbook (10,000 in a Word file), or a part of a workbook that compresses better than 100 to 1, gets
413 file_too_complex. Ordinary workbooks unpack to 9 to 16 times their size, so a workbook at the 3 MiB file
limit fits.
Good to know
Section titled “Good to know”- Counting. Every upload that gets as far as being read uses one of the day’s uploads, also when it then
fails (
file_unreadable,empty_documentordaily_ocr_limit). A scanned PDF also uses one OCR upload, even if the OCR fails. A replay (200),processing(202),duplicate_nameandid_in_usecost nothing. The counts are per owner, for all of the owner’s keys together, and reset at midnight IST and when the server restarts. - Scanned PDFs are read with OCR up to 30 pages through the API; later pages are not read. The text kept from
one scan is currently about 48,000 characters, so a long, dense scan can lose text before page 30. When pages
or text are left out,
warningshasocr_warning. - A failed document doesn’t block its name or id. Add the same file again.
- Deleting a document in the web app frees its name and its
client_document_id. A late retry of that upload stores it again. - Text files must be UTF-8. A UTF-16 CSV or text file, such as Excel’s Unicode Text, gets
400 unsupported_file_type. Save it as CSV UTF-8 instead. - Names that differ only in spelling. When grounded answers are switched on for your server, a name that
differs from an existing one only in spelling can get
409 name_conflict. - Ask with the server
document_id. A question to the new document right after the upload can get409 knowledge_not_ready; try again a little later. A question to the whole workspace is answered from the documents that are ready, with the warningdocuments_not_ready. - One upload at a time per key. A key can run one upload at a time, and the server two in all, by default.
Test from Windows with ask-workspace.ps1
Section titled “Test from Windows with ask-workspace.ps1”The installer’s API examples include ask-workspace.ps1, a Windows PowerShell tester for the whole /workspace
API. It comes with the next installer after 4.6.0, in the api-addon\examples folder. It reads the key from
$env:ZENITH_API_KEY, or asks for it without showing it.
.\ask-workspace.ps1 -Action list.\ask-workspace.ps1 -Action docs -WorkspaceId <workspace_id>.\ask-workspace.ps1 -Action ask -WorkspaceId <workspace_id> -Question "How many days of annual leave do staff get?".\ask-workspace.ps1 -Action askdoc -WorkspaceId <workspace_id> -DocumentId <document_id> -Question "...".\ask-workspace.ps1 -Action add -WorkspaceId <workspace_id> -File .\policy.pdf -Wait.\ask-workspace.ps1 -Action status -WorkspaceId <workspace_id> -DocumentId <client id or document_id> -Wait-Action add makes a 12-character client id and prints it before it sends, uses a 300-second time-out
(change it with -TimeoutSec), and with -Wait asks for the status until ready or failed and ends by
printing the document_id to keep. -ClientId sets your own id. After a time-out it never sends again by
itself: run -Action status -DocumentId <that client id> -Wait. Other actions are stream, text and chat.
For administrators
Section titled “For administrators”- Adding documents is off until it is switched on in the server’s API settings, and it needs
/workspaceto be on as well. Until then both document calls answer404 api_disabled. - Then tick Can add documents for each key that may add files, in Settings › API & Usage › Keys (the Add docs switch). Keys stay read-only until you do.
- The per-file and per-workspace sizes are the web app’s. The daily limits (200 uploads, 20 scanned PDFs per person), the OCR pages (30) and the uploads running at once (1 per key, 2 for the server) can be changed in the server’s API settings.
- Documents added through the API count as API use in Admin › Usage. The server logs record ids, sizes, timings and codes, never file names, file contents, metadata, e-mail addresses or keys.
Errors
Section titled “Errors”Every error is JSON. param, when present, names the field at fault, and messages never repeat what you sent.
{"error": "Workspace not found.", "code": "workspace_not_found", "retryable": false, "request_id": "9b0985b4...", "param": "workspace_id"}| HTTP | code |
Meaning | Retry |
|---|---|---|---|
| 400 | invalid_request |
A field is missing, or has the wrong type, size or shape (see param). A workspace_id or document id that is not shaped like one also gets this |
No |
| 400 | unknown_parameter |
A field this endpoint does not know | No |
| 400 | unsupported_parameter |
A known field or value that is not supported (see POST /workspace) | No |
| 400 | invalid_history |
history breaks the rules |
No |
| 400 | unsupported_model |
model is not fast, smart or genius |
No |
| 400 | invalid_cursor |
The after cursor is not valid, or is from before a server restart |
No, start again |
| 400 | unsupported_file_type |
Adding a document: not one of the five file types (.md too), the content doesn’t match the extension, or a UTF-16 text file |
No |
| 400 | file_unreadable |
Adding a document: the file could not be read, for example a damaged or password-protected file | No |
| 400 | empty_document |
Adding a document: the file has no text to add | No |
| 401 | api_key_required |
No key, or a wrong key | No |
| 403 | api_key_owner_required |
The key has no owner. Use a key created by a user | No |
| 403 | key_not_allowed |
Adding a document: the key is not ticked Can add documents | No |
| 403 | workspace_read_only |
Adding a document: the workspace is shared (published). Add only to your own personal workspaces | No |
| 404 | workspace_not_found |
No such workspace, or the key’s owner cannot open it. When adding a document, also an organisation workspace, one shared with you or another person’s | No |
| 404 | knowledge_not_found |
The document is not in this workspace | No |
| 404 | workspace_access_lost |
Access changed during the answer | Yes |
| 404 | document_not_found |
Status call: no such document in that workspace | No |
| 404 | api_disabled |
/workspace is switched off on this server, or adding documents is not switched on |
No |
| 409 | knowledge_not_ready |
The chosen document is still being prepared | Yes, later |
| 409 | no_knowledge |
The workspace has no ready documents yet | Yes, later |
| 409 | duplicate_name |
Adding a document: a document with this name exists (upper and lower case ignored). The answer has its document_id |
No |
| 409 | id_in_use |
Adding a document: this client_document_id is already used in this workspace for another file |
No |
| 409 | name_conflict |
Adding a document, with grounded answers on: a name that differs only in spelling from an existing one | No |
| 409 | workspace_full |
Adding a document: the workspace is at its size limit | No |
| 411 | length_required |
Adding a document: the request has no Content-Length |
No |
| 413 | file_too_large |
Adding a document: over the file or request limit; the answer has limit_bytes and received_bytes |
No |
| 413 | file_too_complex |
Adding a document: a Word file or workbook unpacks to more than the server accepts; the answer names the rule |
No |
| 415 | unsupported_media_type |
The body is not application/json |
No |
| 429 | rate_limited |
Too many requests. Wait Retry-After seconds |
Yes |
| 429 | too_many_streams |
Too many answers running for this key or the server. Wait Retry-After (5 s) |
Yes |
| 429 | too_many_uploads |
Adding a document: an upload is already running for this key, or the server is busy. Ask for the status; don’t resend. Retry-After 5 s |
Yes |
| 429 | daily_upload_limit |
Adding a document: the owner’s uploads for the day are used up. Retry-After is the seconds to midnight IST |
Yes, tomorrow |
| 429 | daily_ocr_limit |
Adding a document: the owner’s scanned-PDF uploads for the day are used up. Retry-After as above |
Yes, tomorrow |
| 500 | internal_error |
The answer, or the upload, could not be completed. Quote the request_id |
Yes |
| 503 | model_unavailable |
The tier could not answer right now | Yes |
| 504 | model_timeout |
A non-stream answer took longer than 180 s. Use stream: true for long answers |
Yes |
Retry only when retryable is true. Once a stream has started, failures arrive as an answer.failed event
with the same codes.
Limits
Section titled “Limits”| Limit | Default |
|---|---|
| Requests per key | 60 a minute, GET and POST together |
| Answers running at once | 4 per key, 8 for the whole server |
| Question | 8,000 characters |
| History | 20 turns, 24,000 characters |
| Answer length | Your server’s answer limit (max_output_tokens lowers it) |
| Non-stream answer | 180 seconds |
| Stream | No 180-second limit; a : ping after every 20 seconds of silence keeps it open |
| File you add | Currently 3 MiB; the request with base64 4,259,840 bytes. Read limits.max_file_bytes |
| Workspace size | Currently 100 MiB. Read limits.workspace_bytes_limit |
| Uploads per day | 200 per person, and 20 of them scanned PDFs, all of a person’s keys together. Reset at midnight IST |
| Uploads running at once | 1 per key, 2 for the whole server |
| Scanned PDF | 30 pages read, about 48,000 characters kept |
Limits per address apply too. Your administrator can change these limits, or switch /workspace off, in the
server’s API settings; answers through /workspace count as API use in Admin › Usage. Server logs record
only ids, counts, timings and codes, never questions, answers, document names, e-mail addresses or keys.
Differences from the app
Section titled “Differences from the app”- No web search, attachments, Word, Excel or PowerPoint files, or slash commands.
- The API never asks a clarifying question back; it answers directly.
- Follow-ups use only the text in
history, so they can be a little less precise than in the app, which also remembers the passages used in earlier turns. - A very long computed list, such as every row that matches a condition, comes back as a count with a note to use the export in the app.
Common questions
What does POST /workspace return when the documents do not hold the answer?
It returns HTTP 200 with found set to false, an answer that says the documents do not cover the question and names the documents checked, and an empty sources list. Check found before you show or store an answer.
Which workspaces can a ZenithAI API key ask?
Exactly the ones its owner can open in the ZenithAI app, that is their own workspaces, workspaces shared with them and organisation workspaces they belong to. A workspace the owner cannot open answers 404 workspace_not_found, as if it did not exist.
Does /workspace keep my questions or answers?
No. No chat is saved and the question and answer are not kept on the server. The answer id cannot be fetched later. For follow-up questions, your program keeps the conversation and sends the earlier turns back in history.
Why do whole-workspace sources say used false?
When the whole workspace is asked, every ready document is given to the model and the server cannot tell which one the answer came from, so each source is listed with used false. Ask one document with knowledge.document_ids to get a source marked used true.
Can I add a document to a ZenithAI workspace through the API?
Yes, when your administrator has switched it on and ticked Can add documents for your key. POST /workspace/documents adds one txt, pdf, docx, xlsx or csv file, sent as base64, to one of your own personal workspaces. Then poll GET /workspace/documents with your client_document_id until the status is ready.
What should my program do when an upload to /workspace/documents times out?
Do not send the file again. Poll GET /workspace/documents/{client_document_id} until the status is ready or failed. While the first upload still runs, a resend gets 429 too_many_uploads; once it has finished, the same id and file return 200 with the current status and never create a second copy.
Which id do I use to ask a document I added through the API?
The server document_id that the upload and status answers return. The client_document_id you chose is only your handle while the upload is processed. Questions in knowledge.document_ids, the workspace listing and the web app all use the server document_id.
