Set Up Bundle Uploads
How to implement adaptive direct and resumable uploads for spatial bundles and bundle archives.
Bundle uploads use the same adaptive upload-start model as document uploads. The API returns either mode: "direct" for a bounded small single-file upload or mode: "session" for large, multipart, many-file, or explicitly resumable uploads.
Bundles and their upload tickets or sessions must be owned by the organization that owns their location. User-owned bundle or upload state is unsupported and blocks startup/readiness for investigation; the service does not reassign it.
Authorization and capacity
Bundle permissions are location-wide. List, metadata, file, download, ZIP
export, and archive-status operations require final LocationBundles=Read for
the containing visible location. Create, upload, import, and delete operations
require final LocationBundles=Write. Individual bundles do not have ACLs or
assignment endpoints; a bundle ID resolves the permission result of its
location.
The organization entitlement version caps that result. PAT-authenticated requests are additionally narrowed by the token's organization and capability scope, and every transfer/finalization request keeps that credential identity.
Before a positive-byte upload is issued, the server atomically reserves the
organization's effective Bundle-storage pool together with the bundle
placeholder and durable upload state. Regular file finalization is queued
durably, rechecks current Write access, reuses the metadata reconciled during
file completion, and commits resolved bytes with the bundle attachment.
Archive completion keeps the reservation active while extraction runs. The
background finalizer revalidates the current requester or PAT, rechecks
LocationBundles=Write and Bundle capacity, then attaches the extracted file
set and commits its exact expanded object bytes in one transaction. The source
archive is staging data and is deleted rather than billed as another durable
Bundle object. Cancellation, expiry, denial, and pre-commit cleanup release the
reservation idempotently. A storage reduction below current use blocks new
positive-byte additions but does not remove existing data or independently
block an authorized read or deletion.
Capture, texture, reference, render-settings, and model artifact kinds are classified as Bundle storage. If a legacy or mixed bundle contains a Document-classified artifact, its metadata/download and a full bundle ZIP also require the artifact's final path-aware Document read permission. Generated bundle ZIPs are rebuildable, non-billable derived caches, and the API checks current permissions before returning a download URL.
Bundle ZIP exports
Admit an export of every artifact with POST, then poll its status with GET:
POST /v1/spatial/bundles/{bundleId}/zip
GET /v1/spatial/bundles/{bundleId}/zip/statusUse the POST flow when the client already has an exact artifact selection, for example the source photographs matched to one model point:
POST /v1/spatial/bundles/{bundleId}/zip-operations
Content-Type: application/json{
"artifactIds": [
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222"
]
}The selected-artifact request is content-addressed. Repeat the same POST while
the response status is Queued or Preparing; the API returns the prepared
download URL with Ready. Every identifier must belong to the addressed
bundle. Empty selections, empty GUIDs, and selections above 10,000 distinct
artifacts are rejected.
Regular per-file bundles:
POST /v1/spatial/locations/{locationId}/bundles/upload-start
POST /v1/spatial/locations/{locationId}/bundles/upload-completeArchive imports:
GET /v1/spatial/bundles/archive-upload-support
POST /v1/spatial/locations/{locationId}/bundles/archive-upload-start
POST /v1/spatial/locations/{locationId}/bundles/archive-upload-complete
GET /v1/spatial/bundles/archive-imports/{taskId}Use regular per-file bundle uploads when the client needs each file to remain distinct during transfer. Prefer archive uploads for huge file counts because one archive session avoids creating and refreshing URLs for thousands of individual objects.
Regular Bundle Start
{
"bundleType": "Model",
"detailLevel": "Full",
"displayName": "Drone Capture March",
"capturedAt": "2026-03-12T08:30:00Z",
"preferResumable": true,
"files": [
{
"fileName": "metadata.json",
"contentType": "application/json",
"sizeBytes": 2048,
"relativePath": ""
},
{
"fileName": "tileset.bin",
"contentType": "application/octet-stream",
"sizeBytes": 171798691840,
"relativePath": "tiles"
}
]
}capturedAt follows the OpenAPI date-time format, which is the RFC 3339 profile of ISO 8601. Send a timezone designator with the value; prefer UTC Z, for example 2026-03-12T08:30:00Z. The API stores and returns capturedAt in UTC.
For regular multi-file bundle uploads, entries declared with sizeBytes: 0 are
omitted instead of failing the entire batch. The successful upload-start response
returns one warning per omitted entry so clients can name the affected files:
{
"warnings": [
{
"code": "zero_byte_file_skipped",
"message": "File was skipped because its declared size is 0 bytes.",
"fileName": "DJI_0042.JPG",
"relativePath": "photos"
}
]
}The upload target or session contains only the remaining positive-byte files, and Bundle capacity is reserved only for those files. A batch containing no non-empty files is rejected. Negative sizes remain invalid.
The logical bundle layout combines each upload request's relativePath directory with fileName. Pipelines and ZIP downloads receive that layout: a root file is mounted as fileName, and a file with relativePath: "tiles" is mounted as tiles/fileName. Metadata and download responses return the complete logical path in relativePath, including the file name. Resolve nested model assets by this complete path; a bare file name may be ambiguous. Storage keys are an implementation detail and can differ between processing attempts.
If upload-start returns mode: "direct", upload to direct.uploadUrl and complete the scoped route:
{
"directUploadId": "11111111-1111-1111-1111-111111111111",
"completionToken": "server-issued-token"
}The completion response includes result: { kind: "bundle", bundleId } when finalization succeeds. Upload admission returns HTTP 201 with mode, operationId, statusUrl, and exactly one of direct or session. Poll the returned status URL; a created transfer is not yet a published resource.
Session Transfer
If upload-start returns mode: "session", inspect the returned session files and request short-lived URLs only when a worker is ready to transfer each file or part.
Only the user who created the session can inspect, transfer, complete, or cancel it. The API also re-checks current bundle write access on every session request.
GET /v1/upload-sessions/{sessionId}
POST /v1/upload-sessions/{sessionId}/files/{fileId}/upload-url
POST /v1/upload-sessions/{sessionId}/files/{fileId}/parts/{partNumber}/upload-url
POST /v1/upload-sessions/{sessionId}/files/{fileId}/parts/{partNumber}/complete
POST /v1/upload-sessions/{sessionId}/files/{fileId}/complete
POST /v1/upload-sessions/{sessionId}/files/complete-batch
POST /v1/upload-sessions/{sessionId}/complete
POST /v1/upload-sessions/{sessionId}/cancelFor many small files, use bounded batch refresh:
POST /v1/upload-sessions/{sessionId}/upload-urls/batchBatch requests must contain at least one item and must stay within the configured maximum.
{
"items": [
{ "fileId": "33333333-3333-3333-3333-333333333333" },
{ "fileId": "44444444-4444-4444-4444-444444444444" }
]
}Batch refresh is intentionally bounded. Request URLs for the next small transfer window, not for the entire upload queue.
Multipart files include partSizeBytes, expectedPartCount, and completed parts in the session status response. After each object-storage part upload succeeds, report the ETag with the expected byte range. Repeating the same part and ETag is idempotent; reporting a different ETag for an already-completed part is rejected.
File 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.
URL refresh and part reporting are accepted only while the session and file are pending or uploading. Expired sessions are marked expired at request time, and files that are already uploaded or terminal no longer receive fresh upload URLs.
After each bounded transfer window, send its file ids to
POST /v1/upload-sessions/{sessionId}/files/complete-batch. The API reconciles
object storage with bounded concurrency and returns per-item success or error;
successful progress remains persisted when another item fails.
Session completion actively reconciles any remaining stored files in bounded chunks. It does not wait for transfers or multipart parts that are still incomplete, so finish those and retry when final completion reports an incomplete session.
For regular organization bundle files, the first successful completion call
atomically acquires the finalization fence and queues a durable BackgroundJobs
task with the requester authorization snapshot. It normally returns
finalizing immediately. BackgroundJobs rechecks current Write access and
Bundle capacity, reuses the exact size and content type already reconciled
during file completion, batch-applies all files, and atomically commits the
reservation and session state. It does not issue another object metadata
request for every prepared file.
Session completion returns HTTP 202 and a Location while work is active. Poll
GET /v1/upload-sessions/{sessionId}/operation; this reads the persisted receipt
without renewing the lease or enqueueing another task. The completion/read wrapper
contains session and operation; operation.status is finalizing until terminal
and operation.result is null until publication. A completed archive transfer returns
{ kind: "bundle-archive", bundleId, archiveImportTaskId }; its extraction task
continues separately. Regular bundle files return { kind: "bundle", bundleId }.
If a completion response is lost, read the operation before deciding whether to retry
the command. Completion is idempotent. Use GET /v1/upload-sessions/{sessionId} for
detailed per-file resume state. Entering finalization extends expiresAtUtc with a
bounded server-managed lease; progress GETs do not renew it. A finalizing session
cannot be canceled.
Archive Uploads
Archive upload-start uses one archive file and can also return direct or session mode:
{
"bundleType": "Upload",
"displayName": "Drone Capture March",
"capturedAt": "2026-03-12T08:30:00Z",
"archiveFileName": "drone-run-001.7z",
"contentType": "application/x-7z-compressed",
"sizeBytes": 1876543210,
"stripSingleRootFolder": true,
"preferResumable": true
}When an archive direct upload completes, call archive-upload-complete with the direct upload id and completion token. When an archive session completes, call POST /v1/upload-sessions/{sessionId}/complete.
Both completion paths enqueue extraction and return:
{
"succeeded": true,
"status": "completed",
"bundleId": "22222222-2222-2222-2222-222222222222",
"archiveImportTaskId": "33333333-3333-3333-3333-333333333333"
}Poll the archive import task until terminal:
GET /v1/spatial/bundles/archive-imports/{taskId}The completion response means the source archive was accepted for background
processing; it does not commit the compressed source size as final usage. The
reservation remains active until the background finalizer has the exact
expanded file set. If current Write access, PAT validity, or final Bundle
capacity no longer permits the import, finalization returns the stable
AuthorizationChanged or CapacityChanged task status, publishes no files,
quarantines staged objects for bounded cleanup, and releases the reservation.
A fresh archive upload retries the full set with a new reservation.
The support endpoint is the source of truth for archive formats and limits. Current server support includes .zip, .7z, .tar, .tar.gz, and .tgz. Encrypted archives, symlinks, absolute paths, path traversal, duplicate normalized targets, and archives that exceed configured limits are rejected.
Background extraction downloads the source archive to local temporary storage before unpacking it. The background-jobs host therefore needs enough free disk space for the archive itself plus extraction overhead.
Cancellation And Cleanup
Cancel unfinished direct uploads with:
POST /v1/uploads/direct/cancelCancel unfinished sessions with:
POST /v1/upload-sessions/{sessionId}/cancelCanceled and expired upload state is cleaned by BackgroundJobs. Cleanup aborts incomplete multipart uploads and deletes uploaded objects that were not finalized into bundle records or archive extraction tasks.
Active session transfers use a sliding idle lease. Each accepted upload URL request, completed-part report, and file-completion request atomically moves a pending session to uploading and extends its expiresAtUtc. The renewed lease covers every presigned URL returned by the request, preventing cleanup from reclaiming a slow but active upload. Progress reads do not extend the lease, and expired, finalizing, or terminal sessions cannot be renewed.