curl --request POST \
--url https://api.vaquill.ai/workspace/v1/uploads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"matterId": "mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"filename": "msa-acme-v3.docx",
"contentType": "application/pdf",
"sizeBytes": 248193
}
'import requests
url = "https://api.vaquill.ai/workspace/v1/uploads"
payload = {
"matterId": "mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"filename": "msa-acme-v3.docx",
"contentType": "application/pdf",
"sizeBytes": 248193
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
matterId: 'mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6',
filename: 'msa-acme-v3.docx',
contentType: 'application/pdf',
sizeBytes: 248193
})
};
fetch('https://api.vaquill.ai/workspace/v1/uploads', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.vaquill.ai/workspace/v1/uploads"
payload := strings.NewReader("{\n \"matterId\": \"mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6\",\n \"filename\": \"msa-acme-v3.docx\",\n \"contentType\": \"application/pdf\",\n \"sizeBytes\": 248193\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"uploadId": "upl_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"partSize": 5242880,
"totalParts": 3,
"parts": [
{
"partNumber": 1,
"url": "https://storage.vaquill.ai/exports/msa-acme-v3-redline.docx?signature=...",
"expiresAt": "2026-08-19T14:32:10Z"
}
],
"expiresAt": "2026-08-19T14:32:10Z"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"errors": [
{
"location": "<string>",
"message": "<string>",
"type": "<string>"
}
],
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}Start an upload
Open an upload session and return a presigned URL for every part.
sizeBytes must be exact: it fixes the part size and part count for the
life of the session, and a wrong value is not detected until assembly fails.
Split the file on exactly partSize and PUT each part to its own URL,
keeping the ETag each one returns. Bytes go straight to storage and never
through this API.
A small file is a one-part upload rather than a separate code path, so there is one flow to implement at every size.
curl --request POST \
--url https://api.vaquill.ai/workspace/v1/uploads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"matterId": "mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"filename": "msa-acme-v3.docx",
"contentType": "application/pdf",
"sizeBytes": 248193
}
'import requests
url = "https://api.vaquill.ai/workspace/v1/uploads"
payload = {
"matterId": "mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"filename": "msa-acme-v3.docx",
"contentType": "application/pdf",
"sizeBytes": 248193
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
matterId: 'mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6',
filename: 'msa-acme-v3.docx',
contentType: 'application/pdf',
sizeBytes: 248193
})
};
fetch('https://api.vaquill.ai/workspace/v1/uploads', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.vaquill.ai/workspace/v1/uploads"
payload := strings.NewReader("{\n \"matterId\": \"mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6\",\n \"filename\": \"msa-acme-v3.docx\",\n \"contentType\": \"application/pdf\",\n \"sizeBytes\": 248193\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"uploadId": "upl_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"partSize": 5242880,
"totalParts": 3,
"parts": [
{
"partNumber": 1,
"url": "https://storage.vaquill.ai/exports/msa-acme-v3-redline.docx?signature=...",
"expiresAt": "2026-08-19T14:32:10Z"
}
],
"expiresAt": "2026-08-19T14:32:10Z"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"errors": [
{
"location": "<string>",
"message": "<string>",
"type": "<string>"
}
],
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}{
"type": "https://vaquill.ai/docs/workspace-api/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This credential carries matters:read. This operation needs matters:write.",
"instance": "/workspace/v1/matters/mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6",
"requestId": "req_5f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
}Authorizations
Workspace credential issued from the automation console at /automation. Send it as Authorization: Bearer vq_ws_.... This is NOT a Data API key: a vq_key_ credential is refused here and names the other product in the error.
Body
Ask for somewhere to put a file.
sizeBytes is required and is not advisory: it decides the part size and
the part count, both of which are fixed for the life of the session. R2
cannot be asked to assemble parts it was not told to expect, so a size that
turns out to be wrong fails at complete time rather than corrupting a
document.
mat_ identifier of the matter to upload into. Must belong to your organization.
"mat_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
Filename to store, including its extension. Trimmed; whitespace alone is refused. This exact string is used to build the stored object, so it cannot be changed afterwards.
1 - 255"msa-acme-v3.docx"
IANA media type of the file, for example application/pdf. Must be on the supported list or the request is refused before any URL is signed.
1 - 255"application/pdf"
Exact size of the file in bytes. Not advisory: it fixes the part size and part count for the life of the session, and a wrong value fails at complete time. Must be greater than zero.
x <= 2147483648248193
fld_ identifier to file the document under on the way in. Saves a second call. Must belong to your organization.
"fld_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
Set true for scanned or handwritten pages to route the file through vision OCR instead of the text-layer reader. Leaving it false on a scanned exhibit ingests it badly.
false
Response
Successful Response
An open upload, and every URL needed to fill it.
Two things are deliberately not here.
The R2 object KEY. It is inside each presigned URL and cannot not be, since an S3 presigned URL is a signed PATH. What is refused is publishing it as a FIELD: a field is a stable, structured handle a client would build on and we could never withdraw, whereas the URL is a fifteen-minute capability for one object the caller was already granted. Nothing cross-tenant is disclosed either way; the uuids in that path are the caller's own.
The DOCUMENT ID. There is no document until the bytes land, only an
intention to create one, and publishing an id for a row that may never be
filled invites a customer to store it and then poll something that never
starts. It arrives on the operation's resource at complete time, which is
the first moment it means anything.
Public identifier for this upload session, upl_ followed by 32 hex characters. Use it to presign a part and to complete the upload.
"upl_9f2c8b1e4a7d43c9b6e0f1a2c3d4e5f6"
Size in bytes of every part except the last. Split the file on exactly this boundary or the assembled object will not match.
5242880
How many parts to upload. Always at least 1: a small file is a one-part upload rather than a separate code path.
3
One presigned URL per part, in order.
Show child attributes
Show child attributes
When the whole SESSION expires (RFC 3339), which is much later than any individual part URL. Past this, storage discards the parts and the upload has to start again.
"2026-08-19T14:32:10Z"
Was this page helpful?

