Skip to main content

Uploading

One flow, three calls, at every file size up to 2 GB. Your bytes go straight to storage and never pass through the API.
1

Initiate

matterId, filename, contentType and sizeBytes are all required. folderId is optional, and handwritten: true routes the file through OCR tuned for handwriting.Answers 201 with {uploadId, partSize, totalParts, parts, expiresAt}. Each entry in parts carries a partNumber and a presigned url.All three upload calls need documents:write.
2

PUT the bytes

Split the file on exactly the partSize boundary the initiate response gave you, and PUT each piece to the matching partNumber. partSize is not advisory: parts are reassembled in order and every part except the last must be that size, so a different split produces a corrupt object that nothing rejects until complete.Keep the ETag each PUT returns. That ETag is the only proof the bytes landed.A small file comes back as a single part, so there is no separate small-file path to implement. That is the point.
3

Complete

Quoted and unquoted ETags are both accepted, so pass through whatever storage gave you rather than tidying it.Answers 202 with an operation. When it reaches succeeded, resource.id is your documentId.
Presigned URLs expire, and expiresAt tells you when. If one lapses part way through a large upload, re-sign that single part with GET /v1/uploads/{uploadId}/parts/{partNumber} rather than starting the whole upload again.

Supported file types

The Workspace API accepts every format Vaquill supports: 37 extensions and 42 media types, spanning PDF and Word, plain text and markup, spreadsheets, presentations, images, audio, email and archives.
Both the extension and the contentType are checked at initiate, against their own allowlists.
They are checked separately. Nothing verifies that they agree, so {"filename": "contract.pdf", "contentType": "text/plain"} is accepted and a URL is signed for it. The mismatch surfaces later, when the worker reads the file’s actual magic bytes, which is after you have uploaded every byte. Send the content type that matches the file.
Accepted for upload is not the same as usable by every operation. Compare produces a redline and needs a document format, so a plain text file is refused there with 415. Check the page for the operation you intend to run.

Reading a document

The list takes one filter, folderId, and it is the only content filter anywhere on this API:
A document’s status uses the same five values as an operation: queued, running, succeeded, failed, cancelled. It is never the internal ingestion vocabulary, so you can branch on it the same way everywhere. GET .../text answers {documentId, text, chunkCount, truncated}. Check truncated: on a very large document the text is capped, and a caller that ignores the flag will silently analyze part of a contract. A 409 document-not-readable on /text means ingestion has not finished or did not produce text. Poll the ingestion operation rather than retrying blindly.

Downloading originals

/download streams the file with its real content type and filename:
There is no signed URL to hand around. That is deliberate: a signed URL outlives the credential that asked for it, and these are your clients’ confidential files. Authorization is checked at the moment of the request, so revoking a credential stops downloads immediately. It also means the same call works whether or not the document is stored encrypted, which some are. Downloading requires documents:download, a scope separate from documents:read. Listing metadata to check ingestion status and pulling an original out of a client file are different risks, so an integration that only needs the first should not be able to do the second.

Deleting

Answers 204. This is a hard delete with no undo, and it is the same delete the web app’s button performs.
It takes more with it than the document. Deleting a document also removes, by database cascade and without further warning:
  • its extracted text, search index entries and stored summaries
  • its version history, and any documents derived from it
  • its timeline events, so a matter’s chronology loses those entries
  • its row in every matrix it appears in, so a completed matrix silently loses a row and its answers
  • its highlights, its extracted obligations, and its record in any workflow run that used it
None of that is recoverable and none of it is reported back to you. If you only want the file out of circulation, consider leaving it in place rather than deleting it.
Last modified on August 19, 2026