Upload documents as files
Upload up to 200 PDF, Word, PowerPoint, text or Markdown files into a batch, parsed on the server, each with the hash that proves which bytes were checked.
Last updated 12 September 2026
Upload documents as files
POST https://api.quorumtech.ch/attest/v1/batches
Uploads up to 200 files (PDF, Word, PowerPoint, plain text, Markdown) and parses them on the
server into a batch. Each document comes back with two hashes: fileHash is the SHA-256 of the
bytes as uploaded, which you can recompute from the file you hold; contentHash is the hash of
the normalised text actually searched. Only fileHash proves which document a verdict was
produced against.
Files that could not be parsed are listed in failed; the batch is still created from the
rest. A single file is capped at 25 MB.
This is the preferred way to load evidence. The server parses each file into citable spans in reading order, so a citation points at something the server actually read, and returns two hashes per document. fileHash is the SHA-256 of the bytes as uploaded, which you can recompute from the file you hold with shasum -a 256. contentHash is the hash of the normalised text searched, which depends on the parser's version and cannot be recomputed from the file. Only fileHash proves which document a verdict was produced against.
Files that could not be parsed are listed in failed with a reason, and the batch is created from the rest. When no file could be parsed the answer is 422 no_files_parsed. A single file is capped at 25 MB.
name labels the batch on listings. scope describes what the evidence covers, in a sentence, so the review can tell an out-of-scope claim from an unsupported one.
The batch lives in memory on one server and expires after six hours. Delete it when the report is written.
{
"name": "Board pack, September 2026",
"scope": "The September board pack: the CFO report, the minutes of the July meeting and the risk register."
}
Request
A multipart/form-data body with these parts:
| Part | Type | Required | Description |
| --- | --- | --- | --- |
| files | one or more files | yes | One or more documents. |
| name | text | no | A name for the batch, for example "Board Pack FY2024". |
| scope | text | no | What this evidence set covers. Describes the evidence, never the claims. |
Example request
curl -X POST "https://api.quorumtech.ch/attest/v1/batches" \
-H "Authorization: Bearer $QUORUM_API_KEY" \
-F "files=@board-pack.pdf" -F "files=@minutes.docx" \
-F "name=Board pack, September 2026" \
-F "scope=The September board pack: the CFO report, the minutes of the July meeting and the risk register."
Responses
201
The batch is loaded. Documents that failed to parse are named in failed.
| Field | Type | Description |
| --- | --- | --- |
| batchId | string | |
| documents | UploadedBatchDocument[] | |
| spans | integer | |
| totalChars | integer | |
| batchHash | string | |
| createdAt | string (date-time) | |
| failed | FailedFile[] | Files that could not be parsed. The batch was created from the rest. |
| failed[].file | string | |
| failed[].reason | string | |
Errors
The body is { "error": { "code", "message" } }; the full catalogue with what to do about each code is on the Errors page.
| Status | Codes | When |
| --- | --- | --- |
| 400 | invalid_upload, invalid_request | The multipart body is malformed or uses an unexpected field (invalid_upload), or no file part was sent at all (invalid_request). On a draft upload, a file that could not be read is also invalid_upload. |
| 401 | | The key is missing, not recognised, or revoked. |
| 402 | payment_required, credits_exhausted, remaining, needed | The workspace has no active API subscription (payment_required), or is prepaid and has no credits left for this request (credits_exhausted, with remaining and needed). Manage billing and add credits at https://developer.quorumtech.ch/billing. |
| 403 | | The key is not scoped to Attest. |
| 413 | | The batch exceeds the size the server holds in memory. |
| 422 | | No file could be parsed. |
| 429 | rate_limited, claims_rate_limited, claim_budget_exhausted, monthly_cap_reached, cap | Over the key's requests-per-minute limit (rate_limited), its claims-per-minute ceiling (claims_rate_limited), its claims-per-day budget (claim_budget_exhausted), or, for a postpaid workspace, its monthly credit cap (monthly_cap_reached, with cap and resetsAt). Retry-After says when to try again; the daily budget resets at midnight UTC and the cap on the first of the month. |
Cost
Free. Uploading costs no credits; the claims checked against the batch are charged when they are checked.
Related articles
Fetch a page in one call
The same work as the extract call, with the request held open until the page is done and the Markdown returned directly.
Watch an extraction
Server-sent events for one job: its state now, every change after it, and the finished page. Closes when the job does.
Read an extraction's progress
One frame of a job's state without its Markdown, for a progress bar that should not carry a page's text with every poll.
Collect an extraction
Poll a job that answered 202. Returns its state while it runs and the Markdown once it is done.
Still need help?
Ask Quincy in the chat bubble below, or write to support@quorumtech.ch and we will help you directly.