Set Up Document Uploads
How to implement adaptive direct and resumable uploads for document libraries.
Document uploads use adaptive upload-start endpoints. The API decides whether the transfer should be a small direct upload or a durable resumable session:
- Call the matching
upload-startendpoint with the file metadata and final document intent. - If the response has
mode: "direct", upload to the returned URL and call the matchingupload-completeendpoint with the direct upload id and completion token. - If the response has
mode: "session", use/v1/upload-sessions/{sessionId}endpoints to request fresh upload URLs, report multipart parts, complete files, and complete the session.
Document records, collaboration-published versions, and their direct tickets or upload sessions must be organization-owned. Unexpected user-owned protected state blocks startup/readiness for investigation and is never silently reassigned.
Collaboration publishing rechecks the editor's current document write access, the organization's current document entitlement, the document scope and folder destination, and document storage capacity immediately before it attaches a new version. If any check changed while the draft was open, no version or publish-history record is created; refresh access or capacity and retry the same publish action.
Use the cluster, location, organization, or Project upload endpoints depending on where the document should appear:
POST /v1/document-libraries/cluster/{clusterId}/uploads
POST /v1/document-libraries/cluster/{clusterId}/uploads/batch
POST /v1/document-libraries/cluster/{clusterId}/uploads/complete
POST /v1/document-libraries/location/{locationId}/uploads
POST /v1/document-libraries/location/{locationId}/uploads/batch
POST /v1/document-libraries/location/{locationId}/uploads/complete
POST /v1/document-libraries/organization/{organizationId}/uploads
POST /v1/document-libraries/organization/{organizationId}/uploads/batch
POST /v1/document-libraries/organization/{organizationId}/uploads/complete
POST /v1/document-libraries/project/{projectId}/uploads
POST /v1/document-libraries/project/{projectId}/uploads/batch
POST /v1/document-libraries/project/{projectId}/uploads/completeProject admission stores the exact Project identity and inherits LocationDocuments permissions at the destination's Project-relative path. Completing under a different Project is rejected even if both are readable. Session DTOs identify projectDocument and spatialProjectId. URL refresh, completion and receipt reads recheck current access and Project existence. Only the owning organization's existing Document pool is charged.
Upload Start
Send document metadata at upload-start. Completion is no longer the source of truth for bucket, object key, folder, document type, or custom metadata.
{
"fileName": "site-a.pdf",
"contentType": "application/pdf",
"sizeBytes": 482193,
"checksumSha256": "f32e6d1775c8baf5b7d560a9304f7b34cfe1bbab2d9f8d3f8fd3db2cc7d6b2d0",
"folderPath": "permits/2026",
"documentType": "Permit",
"customMetadata": {
"trade": "electrical",
"revision": 3
},
"preferResumable": false
}customMetadata must be a JSON object and is limited to 16384 bytes after serialization. Omit it to preserve existing metadata when replacing a document, or send clearCustomMetadata: true to remove existing metadata. Do not send both in the same request.
Set preferResumable: true when a client wants resume behavior even for a small file.
For organization-owned documents, upload-start checks your final tier-capped
Write access at the normalized target folder path, including any PAT ceiling,
and the organization's effective Document-storage capacity. The declared bytes
are reserved before the API returns a direct ticket or session, so concurrent
uploads cannot all claim the same remaining capacity.
Upload Several Documents in One Session
Use the matching upload-batch-start endpoint when a user selects multiple
documents and you want one resumable session instead of a separate start and
final completion for every file. The request requires at least two files:
{
"folderPath": "permits/2026",
"documentType": "Permit",
"customMetadata": {
"trade": "electrical"
},
"files": [
{
"fileName": "site-a.pdf",
"contentType": "application/pdf",
"sizeBytes": 482193,
"checksumSha256": "f32e6d1775c8baf5b7d560a9304f7b34cfe1bbab2d9f8d3f8fd3db2cc7d6b2d0"
},
{
"fileName": "site-b.pdf",
"contentType": "application/pdf",
"sizeBytes": 391022
}
]
}folderPath, documentType, and customMetadata apply to every document in
the selection. The response is always mode: "session" and reserves the sum
of the declared file sizes. Transfer and acknowledge the session files through
the shared session endpoints, then call:
POST /v1/upload-sessions/{sessionId}/completeThat final call reconciles remaining stored files, creates every document in
one finalization transaction, and returns documentIds in session-file order.
Repeating the completed session call replays the persisted ID list.
Direct Mode
Direct mode is optimized for small single-file uploads. It uses one API call to start, one object-storage PUT, and one API call to complete.
{
"mode": "direct",
"direct": {
"directUploadId": "11111111-1111-1111-1111-111111111111",
"completionToken": "server-issued-token",
"bucket": "pixservice-dev",
"objectKey": "user/.../documents/__direct-uploads/11111111-1111-1111-1111-111111111111/site-a.pdf",
"uploadUrl": "https://storage.example.test/presigned-put",
"expiresAt": "2026-07-07T12:15:00Z",
"fileName": "site-a.pdf",
"contentType": "application/pdf",
"sizeBytes": 482193
}
}Upload bytes directly to direct.uploadUrl. Then call the scoped upload-complete endpoint with only the id and token:
{
"directUploadId": "11111111-1111-1111-1111-111111111111",
"completionToken": "server-issued-token"
}The completion response includes status, directUploadId, and documentId when finalization succeeds. The API validates the ticket, token, route scope, object metadata, and stored upload intent. For organization-owned documents it also rechecks your current tier-capped path Write access and effective Document-storage capacity. Document attachment, reservation commit, and the completed ticket state are atomic; if either check changed, no document is attached and the reserved capacity is released for cleanup.
Only one direct-completion request owns the finalization fence. A concurrent request returns HTTP 200 with status: "finalizing", succeeded: false, and no error; retry the same scoped completion request until it becomes terminal. Finalization continues if the original HTTP request disconnects. The bounded finalization lease protects live work from cleanup and lets cleanup reclaim an abandoned finalizer after expiry. The winning response is currently the only direct response that carries documentId, so retain it.
Cancel an unfinished direct upload with:
POST /v1/uploads/direct/cancelOnly the user who created the direct upload can complete or cancel it. Creator cancellation does not require current Document Write, but is rejected after finalization acquires the fence. Atomic cancellation, expiry, and cleanup transitions prevent stale cleanup work from deleting an object after finalization wins the race.
Session Mode
Session mode is returned for large files, multipart files, explicitly resumable uploads, or any upload shape the server decides should not depend on one long-lived presigned URL.
{
"mode": "session",
"session": {
"sessionId": "22222222-2222-2222-2222-222222222222",
"status": "pending",
"expiresAtUtc": "2026-07-08T12:00:00Z",
"files": [
{
"fileId": "33333333-3333-3333-3333-333333333333",
"fileName": "large.ifc",
"isMultipart": true,
"partSizeBytes": 67108864,
"expectedPartCount": 1600,
"parts": []
}
]
}
}Inspect or resume a session:
GET /v1/upload-sessions/{sessionId}Only the user who created the session can inspect, transfer, complete, or cancel it. For organization-owned documents, every fresh file, part, or batch upload-URL request rechecks current tier-capped Write access at the stored target path. If access changed, the session fails and its reserved capacity is released before the API denies the URL request.
For a non-multipart session file, request a fresh upload URL shortly before transfer:
POST /v1/upload-sessions/{sessionId}/files/{fileId}/upload-urlUpload the file to the returned URL, then complete the file:
POST /v1/upload-sessions/{sessionId}/files/{fileId}/completeFor a multipart file, request each part URL just in time:
POST /v1/upload-sessions/{sessionId}/files/{fileId}/parts/{partNumber}/upload-urlUpload exactly the returned byte range and send the file's declared contentType as the Content-Type header. The header is part of the storage signature and must match exactly.
After each successful part upload, report the part range and ETag:
{
"eTag": "\"abc123\"",
"offsetBytes": 402653184,
"sizeBytes": 67108864
}Then complete the file and session. The individual route remains useful for one file or a targeted retry:
POST /v1/upload-sessions/{sessionId}/files/{fileId}/complete
POST /v1/upload-sessions/{sessionId}/completeFile completion is idempotent after the file reaches uploaded or completed. If a response is lost, repeat the file-complete request to receive the persisted state without completing the object-storage upload again. Multipart retries also reconcile an exact-size stored object when object storage completed the upload before the API could persist the result. For organization-owned sessions, file completion rechecks current tier-capped Write at the stored target before completing object storage. Denial fails the session and releases its reservation, matching denied URL refresh and part reporting.
For a many-file transfer window, reconcile all uploaded objects with one bounded request:
POST /v1/upload-sessions/{sessionId}/files/complete-batch{
"fileIds": [
"33333333-3333-3333-3333-333333333333",
"44444444-4444-4444-4444-444444444444"
]
}The response contains one result per id. Successful file state is retained and
marked prepared after object metadata and exact size are reconciled, even when
another item fails. Already uploaded or completed ids are idempotent. Session
responses expose uploadedFileCount, preparedFileCount, and
completedFileCount for progress UI.
For large browser uploads, pipeline one bounded file-completion request behind
the next bounded storage-transfer window. Server-side reconciliation then
overlaps later PUTs without unbounded API or storage concurrency.
Session completion returns documentId when the document is finalized. Before
finalization, it actively reconciles remaining files in bounded chunks, so a
client can use the final session call as the authoritative completion boundary.
It does not poll for storage transfers that are still running: missing objects,
missing multipart parts, or size mismatches preserve other successful progress
and reject finalization for a later retry.
For organization-owned documents the API checks current tier-capped path
Write access and effective Document-storage capacity before atomically
acquiring the finalization fence and queuing a durable BackgroundJobs task with
the requester authorization snapshot. BackgroundJobs rechecks authorization and
capacity before committing. The first completion call normally returns
finalizing immediately. BackgroundJobs batch-writes artifacts, thumbnail
tasks, and search tasks, then atomically commits attachment, reservation, and
session completion. Denial or failure creates no document and releases the
reservation.
Only one request owns finalization. Concurrent or replayed completion requests
return HTTP 200 with both session.status and result.status set to
finalizing, result.succeeded set to false, and no error. Repeat the same
completion call until terminal. Completion responses use a compact session
summary with aggregate counts and an empty files array; use
GET /v1/upload-sessions/{sessionId} only for detailed per-file resume state.
Completed retries replay the persisted documentId or documentIds.
Finalization continues if the initiating HTTP request disconnects. Entering
finalizing extends expiresAtUtc with a bounded server-managed lease, so
cleanup cannot remove live finalization work but can reclaim a session if its
finalizer crashes and the lease expires.
Detailed authoritative capacity is available from GET /v1/orgs/{organizationId}/usage only when the exact session or PAT actor has final OrganizationUsageView. Membership and legacy role labels are not enough. The response includes Document and Bundle storage plus current Cluster/Location and monthly/lifetime Job usage. The former GET /v1/orgs/{organizationId}/storage and GET /v1/users/me/storage owner-quota routes are hidden compatibility stubs that return HTTP 410.
URL refresh and part reporting are accepted only while the session and file are pending or uploading. Every accepted upload URL request, completed-part report, and file-completion request atomically moves a pending session to uploading and extends its sliding idle lease. The renewed expiresAtUtc always covers newly returned presigned URLs, preventing cleanup from reclaiming a slow but active upload. Reading session progress does not renew the lease. Expired sessions are marked expired at request time, and finalizing or terminal sessions and files that are already uploaded or terminal no longer receive fresh upload URLs.
For many small session files, request a bounded batch of fresh URLs:
POST /v1/upload-sessions/{sessionId}/upload-urls/batch{
"items": [
{ "fileId": "33333333-3333-3333-3333-333333333333" },
{ "fileId": "44444444-4444-4444-4444-444444444444", "partNumber": 7 }
]
}Batch requests must contain at least one item, and the server enforces a configured maximum. Request URLs shortly before transfer; do not prefetch URLs for an entire long upload queue.
Cancel an unfinished session with:
POST /v1/upload-sessions/{sessionId}/cancelA session cannot be canceled after it enters finalizing. Only its creator can cancel it, and current Document Write is not required for that access-reducing action.
Canceled, expired, denied, and failed state is cleaned by BackgroundJobs. Cleanup releases or expires organization Document-storage reservations idempotently, aborts incomplete multipart uploads, and deletes unfinalized uploaded objects.
Document Versions
Single-file upload-start accepts an optional previousVersionArtifactId to upload the file as a
new version of an existing document in the same library scope. The parent must be writable by the
caller at its own folder and must currently be the newest version of its lineage; a superseded
parent returns 409 Document version conflict, while unknown, cross-scope and unauthorized parents
all return 404 so the parent stays opaque. The new version always inherits the parent's folder —
a declared folderPath is ignored — so versions stay co-located. The finalized document carries
version and previousVersionId, and document lists keep serving only the newest version by
default (versionFilter=newest).
GET /v1/spatial/documents/{documentId}/versions
POST /v1/spatial/documents/{documentId}/revertThe versions endpoint returns the full lineage newest-first plus headDocumentId. Revert restores
an OLDER version as a new head by copying its bytes server-side into a new object (Document-storage
capacity is reserved and committed for the copy); reverting the current head returns 409.
Folders And Listing
Read document metadata from:
GET /v1/spatial/documents/{documentId}
GET /v1/spatial/documents
GET /v1/document-libraries/cluster/{clusterId}/documents
GET /v1/document-libraries/location/{locationId}/documents
GET /v1/document-libraries/organization/{organizationId}/documentsUse GET /v1/spatial/documents without clusterIds for documents attached directly to all clusters the current user can read, or provide clusterIds as repeated query values or a comma-separated list for a selected-cluster document library.
GET /v1/document-libraries/organization/{organizationId}/documents is the direct organization library
only; it does not include cluster or location documents owned by the same
organization. Your current final tier-capped OrganizationDocuments value and
the longest matching ordinal case-sensitive path rule are applied before
search, sorting, paging, type values, folder results, or optional links are
created. Metadata, download, and thumbnail requests recheck Read at the
document's stored path. Deletion rechecks Write at that path.
Organization-owned cluster and location libraries use the same authorization
order. Cluster documents apply final ClusterDocuments access at each stored
path after organization defaults and cluster overrides. Location documents
apply final LocationDocuments access after organization, cluster, and
location overrides. The required discovery gates, entitlement cap, and PAT
ceiling are included in the result. Unauthorized rows are removed before
paging, type values, folder counts, and optional file or thumbnail links. The
single-document metadata, download, thumbnail, move, and delete routes always
recheck the current exact path.
Document list endpoints accept folderPath and recursive. Omit folderPath for all documents in the scope, send folderPath= for root-level documents, or send a path like folderPath=permits/2026 for one folder. Add recursive=true to include descendant folders.
Folders are logical document metadata. Moving a document or renaming a folder updates the document folderPath; it does not move or rename the underlying object-storage key.
Folder and permission-path identity is case-sensitive, so A and a are
different paths.
For an organization-owned document, PATCH /v1/spatial/documents/{documentId}/folder
checks final tier- and credential-capped Write access at both the exact current
folder path and the destination path. Source Write is the removal authority.
If destination access is missing, the document and folder tree stay unchanged.
An allowed move remains in the same Document storage pool, preserves the object
key and charged bytes, and creates only missing destination-folder metadata, so
the API evaluates zero additional capacity and does not reserve or charge the
document again. Moving content out of a
permission-managed folder does not unlock, rename, or move the managed root.
The API does not currently provide a Document copy or cross-library move endpoint. Creating an additional charged object requires a normal upload or publish path that reserves destination Document capacity before attachment.
If the organization's tier makes Documents unavailable, new upload URLs and document attachment are denied. Existing permission-managed folder bindings, logical folders, and their contents remain in place and become usable again when a compatible tier is restored.
Manage folder trees with:
GET /v1/document-libraries/cluster/{clusterId}/folders
POST /v1/document-libraries/cluster/{clusterId}/folders
PATCH /v1/document-libraries/cluster/{clusterId}/folders/{folderId}
DELETE /v1/document-libraries/cluster/{clusterId}/folders/{folderId}
GET /v1/document-libraries/location/{locationId}/folders
POST /v1/document-libraries/location/{locationId}/folders
PATCH /v1/document-libraries/location/{locationId}/folders/{folderId}
DELETE /v1/document-libraries/location/{locationId}/folders/{folderId}
GET /v1/document-libraries/organization/{organizationId}/folders
POST /v1/document-libraries/organization/{organizationId}/folders
PATCH /v1/document-libraries/organization/{organizationId}/folders/{folderId}
DELETE /v1/document-libraries/organization/{organizationId}/folders/{folderId}
PATCH /v1/spatial/documents/{documentId}/folderUse parentPath on folder list endpoints to list one level below a parent folder. Creating or uploading into permits/2026 creates missing ancestor folders such as permits. Deleting a folder requires it to have no child folders and no documents.
For organization-owned organization, cluster, and location libraries, folder listing removes inaccessible paths before returning results and keeps only the ancestor names needed to reach an authorized descendant. Creating a folder requires the library's document Write capability at its new path plus the zero-byte Document-capacity prerequisite. Ordinary folder creation only adds logical folder metadata and cannot create, change, or adopt a permission-managed binding. Managed roots are materialized only by an authorized permission-rule mutation and its bounded, audited system reconciler. Renaming or moving a folder requires that same capability across every affected source and destination subtree path, and deletion requires Write at the empty folder path.
Download An Organization-Owned Document ZIP
Use the folder forms for one subtree:
GET /v1/document-libraries/organization/{organizationId}/documents/zip/status?folderPath=Board
GET /v1/document-libraries/organization/{organizationId}/documents/zip/download?folderPath=BoardUse the POST forms for all authorized direct organization documents or an
explicit documentIds selection:
POST /v1/document-libraries/organization/{organizationId}/documents/zip/status
POST /v1/document-libraries/organization/{organizationId}/documents/zip/downloadCluster and location libraries use the same folder GET and all-or-selected POST shapes:
GET /v1/document-libraries/cluster/{clusterId}/documents/zip/status?folderPath=Board
POST /v1/document-libraries/cluster/{clusterId}/documents/zip/status
GET /v1/document-libraries/cluster/{clusterId}/documents/zip/download?folderPath=Board
POST /v1/document-libraries/cluster/{clusterId}/documents/zip/download
GET /v1/document-libraries/location/{locationId}/documents/zip/status?folderPath=Board
POST /v1/document-libraries/location/{locationId}/documents/zip/status
GET /v1/document-libraries/location/{locationId}/documents/zip/download?folderPath=Board
POST /v1/document-libraries/location/{locationId}/documents/zip/downloadPixService filters the set with your current path-aware
OrganizationDocuments=Read result before it calculates the ZIP fingerprint.
The background request stores those exact authorized document IDs; it does not
run a later whole-folder query that could add hidden or newly uploaded files.
When your authorized set changes, status and download resolve a different
snapshot rather than returning a previously prepared broader archive.
Cluster and location archives use the same exact-ID snapshot rule with their
path-aware ClusterDocuments=Read or LocationDocuments=Read result.
Location documents can be linked to spatial annotations with documentIds on the annotation create and update payloads. The annotation stores document ids, so folder moves and filename changes do not break the link.
Comments
Document comments are stored by PixService and use the same file permissions as the document. The exact current credential is evaluated against the document's current library and folder path. Listing requires final Document read access. Creating, editing, and deleting require final Document write access, including personal access token scope and capability ceilings, and only the author may edit or delete a comment.
GET /v1/spatial/documents/{documentId}/comments
POST /v1/spatial/documents/{documentId}/comments
PATCH /v1/spatial/documents/{documentId}/comments/{commentId}
DELETE /v1/spatial/documents/{documentId}/comments/{commentId}