Organizations, Clusters, and Locations
The ownership and access scopes that drive most public API workflows.
Spatial data is organized as organization-owned clusters with locations inside them. Access is assigned through organization permission groups or direct user permissions. Existing direct-user Contributor shares remain a read/write compatibility source for their exact cluster, but clients cannot create new legacy shares.
Cluster responses include display metadata for map and list UIs: color is a muted #RRGGBB background color, and useLightText tells clients whether light text should be used over that color. POST /v1/spatial/clusters and PATCH /v1/spatial/clusters/{clusterId} accept an optional color as #RGB or #RRGGBB; if a new cluster omits it, the API chooses from a generated muted color set to contrast with the owner's existing cluster colors.
Cluster lifecycle permissions are independent. Creating an organization-owned
cluster requires final ClusterCreate=Allow in the organization, updating one
requires final ClusterUpdate=Allow, and deleting one requires final
ClusterDelete=Allow. New user-owned protected clusters are rejected. These
decisions include the owning organization's exact entitlement ceiling and the
current credential ceiling, so a broad organization role or narrower personal
access token cannot bypass them.
Deleting Clusters and Locations
Delete a building through its location, or delete a cluster together with its locations:
DELETE /v1/spatial/locations/{locationId}
DELETE /v1/spatial/clusters/{clusterId}These actions require final LocationDelete=Allow or ClusterDelete=Allow,
respectively. The API checks the permission again inside the deletion transaction.
Organization roles do not bypass credential ceilings or these capability decisions.
A successful response is 204 No Content. Resource records, dependent document and
model records, and organization resource counts change in one transaction. Completed
job and execution history is retained for audit. Location-scoped access ends when
its location is removed; deleted inputs cannot be used to retry an old job.
Active jobs, uploads, processing work, surviving dependencies, or a concurrent change
can prevent deletion. The API returns 409 Conflict with problem code
spatial_resource_deletion_conflict. No partial database deletion is committed and
object cleanup does not run for that failed transaction. Finish or cancel active
work, then refresh the resource before trying again. A 403 remains a permission
denial; a 503 indicates a service failure rather than missing delete authority.
Physical object cleanup runs asynchronously after the database commit. It uses the recorded exact object keys. Objects outside the deleted resource's storage namespace or still referenced elsewhere are retained for operational review; deletion does not sweep a shared folder or storage prefix. Search cleanup is also queued durably with the deletion, so a temporary background-service outage does not lose that work.
Editing Without Losing Concurrent Changes
Cluster and location responses include a revision string. Keep it with the
snapshot displayed in your editor. Submit that value as baseRevision, together
with only the fields the user changed:
PATCH /v1/spatial/locations/{locationId}
Content-Type: application/json
{
"baseRevision": "12",
"name": "North building",
"description": null
}Omitting a field preserves its current value. Explicit null clears a location's
description, latitude, longitude or altitude, or a cluster's description.
name, address and color cannot be null. Cluster edits accept name,
description and color; location edits accept name, description, address,
latitude, longitude and altitude. Empty updates, unknown fields, and missing
or invalid revisions return 400. Treat revision strings as opaque tokens, not
JavaScript numbers.
If another client changed a different field after your snapshot, the API merges your edit automatically and returns the current resource with a new revision. Store the complete returned editable state and its revision together. Moving a map marker should submit latitude and longitude together and omit altitude unless it was also edited.
If any submitted field changed, the entire update returns 409:
{
"status": 409,
"code": "resource_revision_conflict",
"currentRevision": "14",
"conflictingFields": ["name"]
}Refresh the resource, show the user's unsaved values alongside the current
values, and let the user review the conflict before submitting again. Never
silently replace baseRevision and retry an overlapping edit. A future revision
also returns this conflict code, with an empty field list. An accepted nonempty
patch advances the revision even when a submitted value already matches.
Read responses contain resource metadata rather than nested persistence graphs. Use the dedicated locations, documents, bundles, annotations and permissions routes to load related content.
Cluster Images
A cluster can have one custom image for compact cards and the in-cluster sidebar hero. Upload a JPEG, PNG, or WebP source up to 25 MiB with the same presigned-upload pattern used by other assets:
POST /v1/spatial/clusters/{clusterId}/thumbnail/upload
PUT {returned uploadUrl}
POST /v1/spatial/clusters/{clusterId}/thumbnail/completeThe start request declares fileName, contentType, and sizeBytes. Send the
returned bucket and objectKey, the same declared content metadata, and the
optional multipart completion fields to the complete route. Starting,
completing, replacing, or deleting the image requires final
ClusterUpdate=Allow and consumes the owning organization's Document storage
pool.
Completion returns immediately with optimizationStatus set to processing.
Background processing strips embedded image metadata and creates quality-82 WebP
variants bounded to 256 px and 1024 px. Cluster list responses expose the
256 px result as nullable thumbnailUrl; cluster detail and update responses
also expose the 1024 px heroImageUrl. Each URL has a matching expiry field,
so clients should refresh the cluster response instead of persisting the URL.
Use the explicit route when a view needs to poll for readiness or choose a variant:
GET /v1/spatial/clusters/{clusterId}/thumbnail?size=small
GET /v1/spatial/clusters/{clusterId}/thumbnail?size=medium
DELETE /v1/spatial/clusters/{clusterId}/thumbnailsmall is the card variant and medium is the default sidebar-hero variant.
The read route requires final ClusterView=Read and returns HTTP 204 while no
image is configured or optimization is still pending. Replacing or deleting
the image removes the previous source and its derivatives.
Protected clusters, locations, documents, bundles, upload state, and jobs must all resolve to one organization owner. Startup and readiness fail closed if the database contains an unexpected user-owned protected row or a child whose owner does not match its parent; the service never silently reassigns that data.
Listing Across Clusters
Use the cluster list first when a client needs a picker:
GET /v1/spatial/clustersThe list includes only clusters where the current credential's final
ClusterView=Read decision survives the organization entitlement ceiling and,
for personal access tokens, the token's organization and capability ceilings.
Authorization is applied before search, sorting, and pagination, so hidden
matches do not shorten a page or change hasNextPage. Optional ownerType=Org
and ownerId values further narrow this already-authorized set. Supplying
ownerType=User returns 400 Bad Request.
The cluster detail route applies the same ClusterView=Read requirement. During
the migration window, an active direct-user Contributor compatibility row can
still supply the cluster-local read source without exposing the organization or
another cluster; it does not supply cluster update or delete authority.
When the interface specifically needs clusters marked as shared across all
organizations, use GET /v1/spatial/shared-clusters. It unions active
shared-resource provenance and unmigrated Contributor compatibility candidates,
rechecks final ClusterView=Read, deduplicates, and then paginates. Returned
provenance and shared-guest flags are display metadata only; the accompanying
effective capability values are the current evaluator result.
For read-only views that span more than one cluster, use the aggregate routes:
GET /v1/spatial/locations
GET /v1/spatial/documents
GET /v1/spatial/documents/typesOmit clusterIds to include all clusters the current user can read. Provide clusterIds as repeated query values or a comma-separated list to limit the response to selected clusters:
GET /v1/spatial/locations?clusterIds=11111111-1111-1111-1111-111111111111,22222222-2222-2222-2222-222222222222
GET /v1/spatial/documents?clusterIds=11111111-1111-1111-1111-111111111111Location rows require both final ClusterView=Read for their parent and final
LocationView=Read for the location from the same effective principal. The API
applies those gates before search, sorting, and pagination, including on
GET /v1/spatial/clusters/{clusterId}/locations. A location or descendant grant
does not reveal a hidden cluster, and a visible cluster does not automatically
reveal all of its locations. PAT organization scopes and capability maxima are
preserved for both decisions.
The document aggregate route lists documents attached directly to clusters. It does not include documents attached to individual locations.
Location list and detail responses include annotationCount, so clients can show annotation badges without fetching each location's annotation list first.
POST /v1/spatial/clusters/{clusterId}/locations requires final
LocationCreate=Allow for the parent cluster and a non-empty name. New
locations are limited to organization-owned clusters. Updating a location
requires final LocationUpdate=Allow; deleting one independently requires
final LocationDelete=Allow. Create and patch requests accept optional
latitude, longitude, and altitude: latitude must be finite and between
-90 and 90, longitude must be finite and between -180 and 180, and
altitude must be finite when provided. Known authorization failures use the
same structured permission-denial response as cluster lifecycle operations.
Location Annotations
Annotations belong to locations and can represent a point, line, or polygon. New clients should send geometryType with coordinates; each coordinate is [longitude, latitude] or [longitude, latitude, altitude], and altitude can be null when altitudeMode is clampToSurface. The older position field remains the anchor point and is still accepted for point annotations.
Annotation reads include an opaque revision string. To edit, send that snapshot's
baseRevision and only changed fields to PATCH /v1/spatial/annotations/{annotationId}.
Omitted fields are preserved. Explicit null clears body, normal, priority, color,
savedView or assigneeUserId; an empty documentIds array removes linked documents.
Other fields cannot be null. Non-overlapping edits merge automatically. Geometry
fields share one conflict group, and assignee/document links share another, so
related edits cannot silently invalidate one another. A stale overlapping edit
returns 409 with code: resource_revision_conflict; refresh, review the changes,
and explicitly resubmit the revised draft. Never replace its revision and retry
silently. Annotation ClearPriority/ClearColor/ClearSavedView/ClearAssignee flags
are no longer accepted.
Annotations inherit the containing location's final, tier-capped
LocationBundles result. Listing requires Read; creating, updating, and
deleting require Write. There is no annotation-specific capability and no
individual Bundle or annotation ACL. Personal access token organization scopes
and capability maxima apply to every annotation route.
Annotation responses include workflow and display metadata: category, status, optional priority, optional hex color, optional savedView, authorUserId, optional assigneeUserId, and linked documents. Authorship is server-owned and comes from the authenticated creator.
Load the assignee picker from
GET /v1/spatial/locations/{locationId}/annotations/assignable-users. Supply
each proposed linked document as a repeated documentId query parameter. The
caller and every returned candidate must have final LocationBundles=Read for
that exact location plus final path-aware LocationDocuments=Read for every
supplied document. Create and update use the same predicate, including the
annotation's existing links when an update does not replace documentIds.
Owning-organization membership is only a candidate source, not authority;
shell-only members are omitted. Active direct-user legacy Contributor subjects
remain candidates for their exact shared cluster until migration, but still
pass the owning organization's current tier ceiling. Inaccessible users and
grant sources are removed before search, sorting, paging, and profile-picture
URL creation.
Use documentIds on create or update to link location documents, such as uploaded inspection images, to an annotation. Each proposed link separately requires final path-aware LocationDocuments=Read. The link is stored by document id, not by filename or folder path. Renaming a document or moving it to another folder keeps the annotation link intact, and deleting the document removes it from the annotation response.
Linked-document access is independent from annotation access. When the caller can read the annotation but cannot read a linked document at its current case-sensitive logical path, the API omits that document's ID and metadata from the annotation response without deleting the stored link. Document downloads and thumbnails continue to run their own document authorization checks; an annotation response never grants document content access.
{
"title": "Facade crack",
"geometryType": "point",
"coordinates": [[10.5, 53.5, null]],
"documentIds": [
"44444444-4444-4444-4444-444444444444"
]
}Annotation lists can be filtered with category, status, and updatedSince. category and status can be repeated or sent as comma-separated values, for example ?category=defect,hazard&status=open&status=in_progress. updatedSince is an inclusive ISO 8601 timestamp filter against updatedAt, falling back to createdAt for records that have not been updated.
Volume measurements are an additive opt-in annotation kind. Interactive users with the ordinary location and source permissions can deliberately choose Volume through secondary measurement-mode options, attach a structured definition to a polygon annotation, and request an immutable server calculation from a bounded triangle surface snapshot. Ordinary distance, area, VOB, report, and DXF measurement contracts are unchanged. Volume-owned annotations are omitted from ordinary authenticated annotation lists when the backend feature or actor eligibility is unavailable.
Location share links capture a separate bounded volume display snapshot. The public response can therefore include the footprint and then-current successful cut, fill, and net result even when authenticated volume authoring is disabled. That public projection is immutable and read-only: it provides no definition editing, recalculation, history polling, input, or evidence access. See Annotation volume measurements for the authoring workflow and limits.