Document Library Management
The workflow for listing, uploading, replacing, downloading, and organizing documents across scopes.
Document libraries exist at organization, cluster, location, and Project scope. A document has a stable ID and can be listed, downloaded, versioned, zipped, searched, commented on, and opened in collaboration flows when the file type is supported.
Document metadata uses FileArtifactResponse: scalar fields, version identity,
folderPath, and relativePath (the complete logical path including the file
name). Responses do not expose storage bucket names, object keys or database
navigation graphs. Request a download URL from the API instead of constructing
one from metadata. Ignore unrecognized response fields so later additive features
remain compatible.
Core Flow
List documents from the scope the user is viewing:
GET /v1/spatial/documents
GET /v1/document-libraries/cluster/{clusterId}/documents
GET /v1/document-libraries/location/{locationId}/documents
GET /v1/document-libraries/organization/{organizationId}/documents
GET /v1/document-libraries/project/{projectId}/documentsUse GET /v1/spatial/documents for documents attached directly to all readable clusters, or add clusterIds to limit the response to selected clusters. The route accepts repeated query values and comma-separated values. It does not include location documents.
Use search, sortBy, sortDir, versionFilter, documentTypes, offset, and limit to keep large libraries navigable. Set linkType=file, linkType=tiny, linkType=small, or linkType=medium only when the current view needs immediate download or thumbnail URLs.
For thumbnail grids, request the preview size on the list itself instead of calling the single-document thumbnail endpoint once per row:
GET /v1/document-libraries/location/{locationId}/documents?offset=0&limit=200&linkType=smallEach returned item contains its document plus a nullable link descriptor. The API checks current-revision task state for the complete page in batches and performs object-storage metadata reads with bounded concurrency. Missing generation work is also queued in a single database batch. A null link means the preview is still pending or unavailable; it does not remove the document from the page.
When a view already has document IDs and only needs preview links for visible cache misses, resolve up to 32 at once:
POST /v1/spatial/documents/thumbnails/resolve
Content-Type: application/json
{"documentIds":["44444444-4444-4444-4444-444444444444"],"size":"tiny"}The response has one items entry per requested ID. ready includes a presigned link and resolvedSize (small when a requested tiny or medium preview used the existing small fallback). pending includes a generation reason and bounded retryAfterSeconds; retry later if the tile remains visible. unsupported means the authorized file cannot generate a thumbnail. unavailable covers both missing and unauthorized IDs. These three non-ready states have no link. Send 1–32 distinct, nonempty IDs and tiny, small, or medium; malformed requests return 400. Never persist these expiring URLs or use them for original downloads.
Image and PDF thumbnails are generated asynchronously as stripped WebP variants. Request size=tiny for a 128 px maximum edge, size=small (the default) for 256 px, or size=medium for 1024 px from GET /v1/spatial/documents/{documentId}/thumbnail. The route queues generation when the requested variant is missing; it returns an existing small variant when tiny or medium is missing, or HTTP 204 while no thumbnail is ready. Existing successful small and medium previews remain available while the server adds tiny. Retry after a short delay when an immediately opened detail view needs the preview. Upload and thumbnail HTTP requests do not wait for image conversion.
Thumbnail keys are trusted only after the background task for the document's current content revision succeeds. Authenticated preview requests enqueue a retry and return HTTP 204 when the current revision is not verified. Anonymous share responses omit the thumbnail URL until verification succeeds.
After document deletion, the API stops issuing preview links and prevents pending generation from publishing a replacement preview. Generated objects are removed asynchronously after the cleanup grace period. A previously issued storage URL can remain usable until it expires or its object is removed; deletion does not promise immediate removal from historical backups.
Use folderPath to show one folder and recursive=true when a view should include descendants. Omit folderPath for the whole library, or send folderPath= for root-level documents only.
Folders
Folder APIs exist for organization, cluster, location, and Project libraries:
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}
PATCH /v1/spatial/documents/{documentId}/folderReplace location with cluster, organization, or project and use the corresponding scope ID. All four scopes share the same library contract. Folder list responses include directDocumentCount for authorized newest-version documents directly in the folder and descendantDocumentCount for those in the folder and all nested folders. childFolderCount counts immediate child folders. The response also says whether the folder was explicitly created or inferred from document paths.
To rename a folder, send { "baseRevision": "1", "path": "Drawings" }, using its current opaque revision from the folder list. A stale edit returns revision_conflict (409); refresh and review the new path before retrying. Moving a parent also advances descendant revisions.
Renaming a folder moves the logical subtree and updates document folderPath values. It does not rewrite object-storage keys. Deleting a folder requires it to be empty.
Projects
A Project represents work at one existing location. Create it with POST /v1/spatial/projects and { "locationId": "…", "name": "Roof renewal", "description": "Replace damaged tiles" }. The response is HTTP 201 with a Location header and the Project's id, locationId, locationName, metadata, creation audit and decimal-string revision. Names are trimmed, nonblank and at most 200 characters; optional plain-text descriptions have a 4,000-character limit. Duplicate names are allowed. Location and creation audit are immutable.
GET /v1/spatial/projects lists authorized Projects across locations. Optional locationId, organizationId and case-insensitive name search only narrow the result. Use offset and limit (default 100, maximum 200); the response is { items, hasNextPage }, ordered by creation time descending and ID. Read a Project directly through /v1/spatial/projects/{projectId} without selecting a location first.
PATCH that same route with { "baseRevision": "1", "name": "New label" } or { "baseRevision": "1", "description": null }. Only name/description are editable; omitted fields are preserved. Stale same-field edits return resource_revision_conflict (409); disjoint fields can merge. Use the returned revision for the next edit. Refetch affected lists/detail after mutations or the existing authorized project_created, project_updated, project_deleted notifications; their location origin includes data.projectId. Discard cached data after access revocation.
Metadata reads require the parent's LocationView; create/edit require LocationUpdate. Project documents additionally require LocationDocuments at each Project-relative folder path. No separate Project memberships or permission editor exist. The same inherited plans/2026 rule applies independently in every Project. Permission-managed folders retain their locks.
Use /v1/document-libraries/project/{projectId} for the same folder, upload, version, preview, download and ZIP workflows. A rename changes the Project label without moving folders or storage objects. Documents expose spatialProjectId; search results include Project context. Project documents never become ordinary location-library contents. Versions cannot cross Projects. Project documents use the existing collaboration workflow, including live plain-text and GAEB X86 editing. Publishing preserves the Project library and version lineage. Project public sharing, cross-library transfers and annotation/model assignment remain unavailable.
Delete an empty or populated Project with DELETE /v1/spatial/projects/{projectId}?baseRevision=1. This requires LocationUpdate and document write authority across the whole library, including protected paths. The location and siblings remain. HTTP 202 returns { projectId, operationId, cleanupStatus } after durable logical deletion; physical object/upload/derived cleanup runs asynchronously. Repeat the same DELETE/revision to read pending/failed/succeeded cleanup while the parent remains available. Completion replay cannot publish into the removed Project. Existing signed URLs follow their expiry and object-removal limits.
Comments
When a user opens a document detail or preview surface, load comments with:
GET /v1/spatial/documents/{documentId}/comments?offset=0&limit=50Each comment includes author identity fields for rendering:
{
"id": "77777777-7777-7777-7777-777777777777",
"documentId": "44444444-4444-4444-4444-444444444444",
"authorUserId": "11111111-1111-1111-1111-111111111111",
"authorUsername": "pascal",
"authorDisplayName": "Pascal",
"authorProfilePicture": null,
"body": "Please verify permit revision 3 before handover.",
"createdAtUtc": "2026-05-31T15:00:00Z",
"updatedAtUtc": null,
"canEdit": true,
"canDelete": true
}Create comments with:
POST /v1/spatial/documents/{documentId}/comments{
"body": "Please verify permit revision 3 before handover."
}Edit and delete use the comment id:
PATCH /v1/spatial/documents/{documentId}/comments/{commentId}
DELETE /v1/spatial/documents/{documentId}/comments/{commentId}Final path-aware read access for the exact current credential is required to list comments. Final path-aware write access is required to create, edit, or delete comments. PAT scope and capability ceilings remain in force, and users can edit or delete only their own comments.
Assign a review comment
Comments, including PDF page pins, can carry a responsible person and a review state. Assignment stays with the comment across document versions within the same library scope and folder. It never changes the comment's author or the PDF's text.
Load eligible people only when the user opens the assignment picker:
GET /v1/spatial/documents/{documentId}/comments/assignable-usersThe response is an array of { "id": "user-id", "displayName": "Name" }. The API
reuses the annotation candidate directory, then checks current path-aware document
write access for each active candidate. Organization membership alone is not sufficient.
The caller also needs current document write access.
Use the comment's assignmentRevision when changing assignment or state:
PATCH /v1/spatial/documents/{documentId}/comments/{commentId}/assignment{
"expectedRevision": 0,
"assigneeUserId": "11111111-1111-1111-1111-111111111111",
"status": "in_progress"
}- Only the author can assign or unassign. Send
clearAssignee: trueto unassign; do not also sendassigneeUserId. Omitting both preserves the assignee. - Author or current assignee can set
statustoopen,in_progress, orresolved. Both still need document write access with their exact current credential. - The response is the updated comment with
assigneeUserId,assigneeDisplayName,status,assignmentRevision,assignmentUpdatedAtUtc,canAssign, andcanChangeStatus. Render actions from these flags;canEdit/canDeleteremain author-only. 409means another assignment/state write won or the comment was deleted during the update. Reload comments, show the current state, and ask the user to choose again. Never silently retry against a newer revision.403means current permission is insufficient.- Text edits and soft deletion update separate columns, so an older client cannot erase assignment metadata. Assignment changes do not mark the comment body as edited.
Existing comments start unassigned and open at revision zero. Deploy the additive
AddDocumentCommentAssignments database migration before the API. Older frontend
clients can continue commenting. New clients should hide assignment controls when
assignmentRevision/capability flags are absent from an older server response.
This endpoint stores review responsibility; it does not send notifications or add PDF editing to the text collaboration protocol. Comment updates are read through the existing list endpoint. PDF binary content is not submitted as text operations.
Recognize text in a scanned PDF
Read GET /v1/spatial/documents/{documentId}/ocr to inspect recognition without
starting work. A new source returns not_requested and an opaque sourceVersion.
Send POST to the same path with that sourceVersion and retry: false to request
recognition. 202 means accepted and includes the status URL in Location; poll
that URL with GET. Reads always return 200, including queued and running states.
A successful response includes a short-lived result URL.
A failed operation stays failed until an explicit POST with retry: true.
Active work and successful results are reused for the same source. A changed
source returns 409 document_source_version_conflict; refresh status before
requesting work again. Both reads and requests require current document Read
permission. OCR is unavailable when disabled or when the document is not a PDF.
Upload and ZIP operations
Create a transfer with POST /v1/document-libraries/{scope}/{scopeId}/uploads,
or /uploads/batch for multiple files. HTTP 201 returns mode, operationId,
statusUrl, and one direct or session body. Keep the operation identity;
creation does not mean the document has been published.
Completion returns an operation with status and a tagged result, for example
{ "kind": "documents", "documentIds": ["..."] }. Direct completion receipts
survive retries and lost responses. Poll /v1/uploads/direct/{uploadId} or
/v1/upload-sessions/{sessionId}/operation while finalization is pending.
Session operation reads return { session, operation }.
Request a ZIP with POST /v1/document-libraries/{scope}/{scopeId}/zip-operations.
Send { "folderPath": "Plans" }, a documentIds array, or {} for all documents.
Poll GET of the returned Location. Ready responses include the download URL.
Reads never start or retry work. If sources changed, document_zip_source_changed
(409) asks the client to request a new operation explicitly.