Skip to content

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.

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 gets 401 api_key_required, and 20 wrong keys from one address in 10 minutes (counted together with /v1) gives 429.
  • 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.

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.

Terminal window
$KEY = Read-Host "API key"

Where the ids come from

  • workspace_id: from GET /workspace (call 1 below), field data[].workspace_id.
  • document_id: from GET /workspace?workspace_id=..., field documents[].document_id. Every answer also carries it, in sources[].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.

Terminal window
$BASE = "https://dummydomain"
$H = @{ Authorization = "Bearer $KEY" }
# all your workspaces, with their documents
$r = $null
$r = Invoke-RestMethod -Uri "$BASE/workspace" -Headers $H
foreach ($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, status
Terminal window
$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, used

The 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.

knowledge.document_ids takes exactly one document_id. The answer’s source then comes back with used: true.

Terminal window
$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, used

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.

Terminal window
$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 workspace
Set-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 document
Set-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.

  • 400 invalid_request without a param field: the server did not receive a valid JSON body. Use the blocks as they are. In Windows PowerShell, never pass the JSON to curl.exe as 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_request with a param: that field is missing or has the wrong type, for example question.
  • 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 that workspace_id. Copy it from call 1.
  • 404 knowledge_not_found: the document_id is not in that workspace. Copy it from GET /workspace?workspace_id=....
  • 415 unsupported_media_type: the Content-Type: application/json header is missing.
  • Anything else: see Errors.
GET https://dummydomain/workspace
GET https://dummydomain/workspace?limit=10&include=none
GET https://dummydomain/workspace?workspace_id=WORKSPACE_ID&limit=200
Authorization: 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 https://dummydomain/workspace
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

The 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 (no knowledge): the answer may draw on every ready document.
  • One document (knowledge.document_ids with one id): faster, uses fewer tokens, and the source is exact. A document from another workspace gives 404 knowledge_not_found; one that is still being prepared gives 409 knowledge_not_ready.

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.

{
"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
  • used: true means 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: false means 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 say used: false and coverage.documents_used is null. 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. sources may then be empty and usage may be zero.

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

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.

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.

  • 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 answers 403 workspace_read_only.
  • Five file types: .txt, .pdf, .docx, .xlsx and .csv. Markdown (.md) and the old Office types (.doc, .xls) get 400 unsupported_file_type, and so does a file whose content doesn’t match its extension, for example a renamed image.
POST https://dummydomain/workspace/documents
Authorization: Bearer YOUR_API_KEY
Content-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.

  1. Give your API key, as in step 1 of the copy-paste commands.
  2. 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 300 in PowerShell (its default is 100) and --max-time 300 in curl.
  3. Wait. Paste the printed client_document_id into the status block and run it until the status is ready or failed. If the upload timed out, run only the status block. Do not send the file again.
  4. Keep the server document_id from the answer, and use it from now on to ask the document.

Upload (change YOUR_WORKSPACE_ID and the file path):

Terminal window
$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.status

The 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.

Wait until it is ready (paste the printed id in place of YOUR_CLIENT_DOCUMENT_ID):

Terminal window
$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)" }

Both loops ask every 4 seconds for up to 10 minutes and stop at ready or failed (the bash loop also stops at an error).

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 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
GET https://dummydomain/workspace/documents/{id}?workspace_id={workspace_id}
Authorization: Bearer YOUR_API_KEY
  • {id} is your client_document_id (12 letters or digits) or the server document_id. Anything else gets 400 invalid_request with param document_id.
  • workspace_id is required. Any other query parameter gets 400 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 is 200 for every status. A document that isn’t in that workspace gets 404 document_not_found.

Ask every 3 to 5 seconds and stop at ready or failed.

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 gets 202 with status: "processing".
  • Once the first upload has finished, sending the same id with the same file again returns 200 with 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 the document_id in that answer.
  • If the server restarted while it was reading your file, nothing was stored. Send it again.

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

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).

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.

  • 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_document or daily_ocr_limit). A scanned PDF also uses one OCR upload, even if the OCR fails. A replay (200), processing (202), duplicate_name and id_in_use cost 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, warnings has ocr_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 get 409 knowledge_not_ready; try again a little later. A question to the whole workspace is answered from the documents that are ready, with the warning documents_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.

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.

Terminal window
.\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.

  • Adding documents is off until it is switched on in the server’s API settings, and it needs /workspace to be on as well. Until then both document calls answer 404 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.

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.

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.

  • 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.