Spatial Share Links
Create public read-only links for documents, document folders, model bundles, or mixed collections.
Spatial share links let clients expose documents, document folders, processed model bundles, or selected mixed collections through a public, read-only URL. The URL itself belongs to the Atlas frontend. The API stores the link settings and resolves the UID when an anonymous viewer opens the link.
Only processed model bundles can be shared as bundle resources. Source upload bundles are not supported by this flow.
Preflight a proposed scope
Use the advisory preflight before showing a create or scope-replacement action:
POST /v1/spatial/share-links/preflight
Authorization: Bearer <personal-access-token>
Content-Type: application/json
{
"operation": "Create",
"scope": {
"documentIds": ["11111111-1111-1111-1111-111111111111"],
"folderIds": ["33333333-3333-3333-3333-333333333333"],
"bundleIds": ["22222222-2222-2222-2222-222222222222"]
}
}Use Manage instead of Create when checking a proposed replacement for an existing link. The API resolves the complete current exposure set, including bundle-location annotations, annotation-linked documents, and every document currently beneath a selected folder. It then applies the same central final evaluator used for current entitlement ceilings, credential/PAT ceilings, and same-principal cluster/location visibility gates. The request does not create or change a link.
The response includes:
isAuthorized: every current requirement is satisfiedisScopeResolvable: every proposed explicit and transitive resource resolved into one organizationhasOpaqueTargets: at least one hidden or unresolved target was collapsedmissingRequirementCount: the number of safely reportable missing layersgroups: visible requirements grouped by organization, cluster, and location, plus at most one opaque group
A visible missing entry identifies its capabilityKey, required and actual values, whether the ordinary ACL, entitlement ceiling, credential ceiling, visibility gate, or action prerequisite is missing, and the exact resource/path correlation when safe. A hidden target appears only as one group with opaque=true; that group has no organization, cluster, location, resource, path, or missing-permission details.
Storage, current Cluster/Location count, and monthly or lifetime Job admission
do not appear in this preflight because creating or replacing share-link
metadata consumes none of those operational limits. Capacity-consuming
operations expose those typed families through current-user effective
permission probes and structured known-resource 403 responses.
Preflight is advisory. Do not cache isAuthorized as authority or treat it as a token: link creation and update must re-evaluate current state at mutation time.
Manage links
Authenticated clients can manage links from a model bundle:
GET /v1/spatial/bundles/{bundleId}/share-links
Authorization: Bearer <personal-access-token>POST /v1/spatial/bundles/{bundleId}/share-links
Authorization: Bearer <personal-access-token>
Content-Type: application/json
{
"password": "optional-password",
"expiresAt": "2026-06-30T12:00:00Z",
"bundleIds": [
"22222222-2222-2222-2222-222222222222",
"33333333-3333-3333-3333-333333333333"
]
}Set password to null, empty, or whitespace for a no-password link. Set expiresAt to null for no expiration. Omit bundleIds to share only the route model bundle. Include bundleIds to add more processed model bundles to the same link; the route {bundleId} is always included and remains the primary model.
Each response includes:
uid: the public share identifierpublicUrl: the frontend URL to give to a viewerpasswordRequired: whether the link needs a password unlock requestexpiresAt: the optional expiration timestampbundleId: the primary model bundle id clients should open by defaultscope: the resources included in the link, including every model bundle id
The list endpoint uses the standard PagedListResponse<T> shape with offset and limit. It returns every share link that includes the route model bundle, including links with multiple model bundles.
Documents have matching document-scoped endpoints:
GET /v1/spatial/documents/{documentId}/share-links
Authorization: Bearer <personal-access-token>POST /v1/spatial/documents/{documentId}/share-links
Authorization: Bearer <personal-access-token>
Content-Type: application/json
{
"password": null,
"expiresAt": null
}Document folders have matching folder-scoped endpoints:
GET /v1/spatial/documents/folders/{folderId}/share-links
Authorization: Bearer <personal-access-token>POST /v1/spatial/documents/folders/{folderId}/share-links
Authorization: Bearer <personal-access-token>
Content-Type: application/json
{
"password": null,
"expiresAt": null
}A folder-scoped link captures the folder and every document currently in that folder and descendant folders. Documents added later are not exposed until an authorized scope replacement captures them.
When a model bundle is captured, the link's exact membership also captures the spatial annotations currently exposed by that model location and their currently linked documents. Later annotations and later document links are not added automatically. Each captured annotation in annotations includes its captured linked documents entries so viewers can render picture pins and open the matching files without an authenticated annotation API call.
Create a mixed-scope link with the generic endpoint:
POST /v1/spatial/share-links
Authorization: Bearer <personal-access-token>
Content-Type: application/json
{
"password": "optional-password",
"expiresAt": "2026-06-30T12:00:00Z",
"scope": {
"documentIds": ["11111111-1111-1111-1111-111111111111"],
"folderIds": ["33333333-3333-3333-3333-333333333333"],
"bundleIds": ["22222222-2222-2222-2222-222222222222"]
}
}Replace link settings, and optionally the full document/folder/model scope, with:
PATCH /v1/spatial/share-links/{shareLinkId}
Authorization: Bearer <personal-access-token>
Content-Type: application/json
{
"password": null,
"expiresAt": null,
"scope": {
"documentIds": [
"11111111-1111-1111-1111-111111111111",
"33333333-3333-3333-3333-333333333333"
],
"folderIds": ["44444444-4444-4444-4444-444444444444"],
"bundleIds": ["22222222-2222-2222-2222-222222222222"]
}
}Omit password or expiresAt to preserve that setting. Explicit null clears password protection or expiration. scope: null is rejected.
Omit scope to keep the existing exact membership unchanged. Include scope to replace the entire resource set and capture a new exact membership version. Use this to manage the model list after link creation by replacing bundleIds with the desired processed model bundle ids. The generic endpoint preserves the current primary model when that model is still in the replacement scope; the bundle-scoped PATCH /v1/spatial/bundles/{bundleId}/share-links/{shareLinkId} endpoint keeps the route model included and primary. The API validates both the stored current membership and the complete proposed closure before it persists any part of the mutation.
An expired legacy link has no authoritative exact membership to retain. Extending its expiry or setting expiresAt to null therefore runs a fresh Manage preflight over its current complete legacy scope, including folder descendants, bundle annotations, and annotation-linked documents. If scope is supplied, the replacement closure is also validated. The API stores the newly resolved exact closure and ready migration state atomically before the new expiry becomes effective; any denied or unresolved requirement leaves the expired link unchanged.
Revoke a link with:
DELETE /v1/spatial/share-links/{shareLinkId}
Authorization: Bearer <personal-access-token>Creating links requires final ExternalShareLinksCreate; editing links requires final ExternalShareLinksManage. Both operations also require same-principal ClusterView and LocationView for every represented location and the required read capability for every member of the complete scope. Organization entitlement ceilings and credential or PAT maxima cap those ordinary permissions.
Revocation is exact-target cleanup. A caller with underlying manage authority across the stored membership, protected owner authority, or platform authority can revoke even when a current entitlement ceiling or creator content permission would block editing. Unknown and unauthorized ids both return 404 Not Found and disclose no link or resource details.
Open a public link
Atlas extracts the UID from its frontend route and calls:
GET /v1/spatial/shared/{uid}For a no-password link, the response includes bundles, folders, documents, and annotations from the latest captured exact membership. bundleId, displayName, version, detailLevel, capturedAt, and top-level files describe the primary model bundle to open first; additional model bundles are available from bundles. Bundle files use the same BundleFilePresignedDownloadResponse shape as authenticated bundle file download lists. Documents include metadata, folderPath, and a presigned document link. The document link includes thumbnailUrl when a generated small thumbnail already exists for an image or PDF document; use url as the fallback and keep downloads pointed at url. Captured folder documents and captured annotation-linked documents are returned in the same documents list as explicitly scoped documents.
Each entry in annotations contains the public annotation geometry and display metadata, including annotationId, locationId, title, body, category, status, priority, color, position, altitudeMode, geometryType, coordinates, savedView, normal, createdAt, and updatedAt. The annotation's nested documents list uses the same public document shape as the top-level documents list and includes presigned link values after the share link is accessible.
For a password-protected link, the first response returns passwordRequired=true and no file links. Submit the password in a second request:
POST /v1/spatial/shared/{uid}/unlock
Content-Type: application/json
{
"password": "viewer-password"
}The unlock response includes the same scoped metadata and download entries.
Public share-link access is read-only. It exposes annotation records only as shared view data and does not expose upload, edit, delete, job, or share-management operations.
Anonymous resolution is a separate UID/password boundary. It does not use authenticated shared-cluster membership and does not recheck the creator's link-management ACL. The API still checks the owning organization's current represented-content ceiling for every captured member. If any required content value is no longer sufficient, resolution fails for the whole link rather than returning a partial closure; the stored link remains available for possible reactivation after a later tier change.
Deleting a captured annotation omits that annotation without invalidating surviving model content. Deleting a captured document does not invalidate the rest of an exact share link. The deleted document is omitted while surviving captured bundles, folders, documents, and annotations remain available. If no deliverable scoped resource remains, the link resolves as unavailable. A resource that still exists but no longer matches the captured organization, type, or location boundary fails the whole link closed.
Download URLs are short-lived storage credentials. Each resolve or unlock request rechecks the link state and current represented-content ceilings before issuing fresh URLs. Revoking a link or lowering a ceiling stops future issuance, but an already-returned storage URL cannot be revoked retroactively and may continue to work until its bounded expiry. The deployment setting defaults to 15 minutes and accepts only 1–60 minutes.
Error behavior
- Failed authenticated creation or update:
403 Forbiddenwithcode=share_link_scope_access_denied. Thelocationsarray groups missing current requirements by each location the caller can already see. A hidden location or resource is represented by one entry withopaque=true, no location id, and no resource id. The mutation is not partially persisted. - Public content ceiling no longer permits every captured member:
404 Not Found; no partial metadata or downloads are returned, and the link is retained - Captured document was deleted: the document is omitted; if no deliverable scoped resource remains, the link returns
404 Not Found - Unknown UID:
404 Not Found - Incorrect password:
401 Unauthorized - Expired link:
410 Gone - Revoked link:
410 Gone
Model measurements and downloadable bundle files
For new links and explicit scope replacements, implicit model annotations are filtered by their model identity. PixAtlas measurement annotations (__pixmeas) must identify the selected bundle (bundle-{id} / pointcloud-{id}) or one of its file artifacts. Volume annotations must reference the selected source bundle. Location-wide notes remain included, together with their captured linked documents. Measurements of another model at the same location and their linked documents are not implicitly included.
Existing exact memberships are not rewritten by this change. Use an authorized scope replacement to recapture an existing link under the new selection rule. Merely opening a share dialog must never replace scope. Annotation content is still live; exact membership captures resource identities, not an immutable copy of every annotation body.
Public bundle downloads are filtered before storage URLs are signed. Supported rendering, geometry, texture and coordinate-reference files remain available; log files, logs/_internal paths and unsupported extensions are excluded. This is a bundle-file policy, not a restriction on documents explicitly selected for sharing. JSON render metadata remains supported; the filter is not a content-level redaction of arbitrary JSON or viewer configuration.
Clients must use the returned expiresAt for storage URLs. Renew an active share through resolve, or unlock again using its password, before those URLs expire. A resolve/unlock rechecks access; an already issued storage URL retains the bounded lifetime described above, including after link revocation. Link expiry is not currently propagated as a shorter storage-signature expiry. There is no database migration for these changes.