Skip to content

Processing and Monitoring

After input files are uploaded, the platform can generate processed outputs (standardized point clouds, mesh HLODs, photospheres, and so on). This guide explains how to list processing options, launch processing, and poll progress until every output reaches a terminal state.

It assumes you have a bundle with at least one completed upload session and hold read:bundle and write:bundle scopes.


Processing is fire-and-forget: the API enqueues a processing job and returns immediately.

sequenceDiagram
  participant Client as Client
  participant RCAPI as RCAPI

  Client->>RCAPI: GET processing/options
  RCAPI-->>Client: options + metadata

  Client->>RCAPI: POST processing
  RCAPI-->>Client: accepted or typed error

  loop until all terminal
    Client->>RCAPI: GET bundle/processings
    RCAPI-->>Client: processing records (status, progress)
  end
MethodPathScopePurpose
GET/v1/bundles/{bundleId}/processing/optionsread:bundleList possible output types and their metadata
POST/v1/bundles/{bundleId}/processingwrite:bundleLaunch processing for requested outputs
GET/v1/bundles/{bundleId}/processingsread:bundleList the bundle’s processing records to poll progress
GET/v1/site/{siteId}/bundles/{bundleId}read:hierarchyGet the bundle with its processed output components (signed links)

Before launching, discover which outputs the bundle can produce and how much each one costs.

GET {api_url}/v1/bundles/{bundleId}/processing/options?page=1&limit=20&sortBy=type&sortDir=ASC
Authorization: Bearer {access_token}
ParameterTypeDescription
pagenumber1-based page index (default 1)
limitnumberPage size (default 20, maximum 20)
sortBystringtype or cost (default type)
sortDirstringASC or DESC (default ASC)

The response is a paginated list ({ items, total }). Each option describes one output type:

FieldMeaning
typeOutput component type to pass when starting processing (e.g. StandardizedPointCloud, OgcPointCloudHlod)
hasProcessingtrue when a processing for this output has already been launched
costProcessing cost in bytes for paid outputs; 0 for free-tier outputs

Example:

{
"items": [
{
"type": "StandardizedPointCloud",
"hasProcessing": false,
"cost": 734003200
},
{
"type": "OgcPointCloudHlod",
"hasProcessing": false,
"cost": 734003200
}
],
"total": 2
}

Skip outputs where hasProcessing is true unless you intend to reprocess.

Pass the list of output component types you want the pipeline to produce, plus a cost field that acknowledges the total processing charge. The server sums the cost values from Step 1 for every type in requestedComponents and rejects the request when your acknowledgment does not match.

Processing starts asynchronously once the cost is accepted.

POST {api_url}/v1/bundles/{bundleId}/processing
Authorization: Bearer {access_token}
Content-Type: application/json
{
"requestedComponents": [
"StandardizedPointCloud",
"OgcPointCloudHlod"
],
"cost": 1468006400
}

In the example above, cost is 734003200 + 734003200 from the Step 1 options list.

204 No Content: Processing was accepted and enqueued.

When processing cannot be launched, the API returns 409 Conflict with a typed error:

error valueMeaning
ProcessingCostMismatchThe submitted cost does not match the computed total for the requested outputs
InsufficientProcessingCapacityOrganization processing limit would be exceeded
NoInputDataFoundForProcessingNo finalized input files are available on the bundle
FailedToLaunchProcessingPipeline submission failed for an unexpected reason

Example:

{
"error": "ProcessingCostMismatch"
}

Structural validation of the uploaded batch (mandatory types, extensions, dependencies) is enforced by the processing pipeline after launch. If your upload session completed but violated the requirements contract, processing may fail later even though every file uploaded successfully and the start call returned 204.

After a successful start, poll the bundle’s processing records until every one reaches a terminal state.

GET {api_url}/v1/bundles/{bundleId}/processings
Authorization: Bearer {access_token}

The response is a list ({ items, total }) with every processing record for the bundle. There are no query parameters; all records are returned in a single call.

Each item tracks one processing job:

FieldMeaning
idIdentifier of the processing record
statusPipeline status (see table below)
progressCompletion percentage (0100) while active
createdAtWhen the job was created
updatedAtWhen the job last changed state
launchedByIdId of the user who launched the job, or null
knownErrorsStructured failures, each with an errorCode and the affected errorFilepaths (null when not file-specific)
StatusTerminal?Meaning
PendingnoQueued, not yet started
ProcessingnoPipeline is running
RestartednoJob was restarted and is running again
ReadyyesOutput produced successfully
FailedyesUnstructured pipeline failure
FailedToLaunchyesJob never entered the pipeline
KnownErroryesStructured validation or format error

Poll until every record is in a terminal state (Ready, Failed, FailedToLaunch, or KnownError).

Example mid-flight response:

{
"items": [
{
"id": "3f2b1c4d-8e7a-4b2c-9d1e-0a1b3c4d4e6f",
"status": "Processing",
"progress": 42,
"createdAt": "2026-06-01T14:05:00Z",
"updatedAt": "2026-06-01T14:12:00Z",
"launchedById": "b1a2c3d4-5e6f-7a8b-9c0d-1e2f3a2b2c1d",
"knownErrors": []
},
{
"id": "9c0d1e2f-3a4b-5c6d-7e8f-9a0b2c3d3e4f",
"status": "Pending",
"progress": 0,
"createdAt": "2026-06-01T14:05:00Z",
"updatedAt": "2026-06-01T14:05:00Z",
"launchedById": "b1a2c3d4-5e6f-7a8b-9c0d-1e2f3a3b1c1d",
"knownErrors": []
}
],
"total": 2
}

Example with a known error:

{
"items": [
{
"id": "3f2b1c4d-8e7a-4b2c-9d1e-0a2b3c4d4e5f",
"status": "KnownError",
"progress": 100,
"createdAt": "2026-06-01T14:05:00Z",
"updatedAt": "2026-06-01T14:48:00Z",
"launchedById": "b1a2c3d4-5e6f-7a8b-1c0d-2e6f3a4b5c6d",
"knownErrors": [
{
"errorCode": "MissingTrajectory",
"errorFilepaths": ["scan.las"]
}
]
}
],
"total": 1
}

The processing list does not include the output type per record. To map finished outputs back to their component types and signed links, fetch the bundle’s components (see Retrieving processed outputs).

  • Interval: 1-5 minutes is a reasonable default. Processing can run for multiple minutes to several hours depending on input size.
  • Rate limits: the bundle endpoints share a rate limit bucket with multiple other endpoints. Avoid sub-minute polling.

Once a processing record reaches Ready, fetch the bundle to get its output components and their signed download links. Bundle details live on the hierarchy endpoint:

GET {api_url}/v1/site/{siteId}/bundles/{bundleId}
Authorization: Bearer {access_token}

This endpoint requires the read:hierarchy scope and returns the bundle with its components array:

FieldMeaning
idBundle id
nameBundle name
componentsOne entry per available component (inputs and processed outputs)

Each component carries:

FieldMeaning
idComponent id
typeComponent type, e.g. StandardizedPointCloud, OgcPointCloudHlod
versionComponent version
signedLinkSigned URL to download the component; no auth header required
rootsEntry-point paths within the component, when applicable

Example:

{
"id": "a6f75b3c-f261-4b27-9a2a-9a6cc4178a13",
"name": "ZF capture 2026-06-01",
"components": [
{
"id": "5d6e7f8a-9b0c-1d2e-3f4a-1b3c7d4e8f0a",
"type": "StandardizedPointCloud",
"version": "1",
"signedLink": "https://cdn…/components/…?X-Amz-…"
}
]
}

A RealityPlan project exposes the same bundle through GET /v1/reality-plan/{projectId}/bundles/{bundleId} (scope read:twin).

import time
import requests
API_URL = "https://<your-regional-api-url>"
TOKEN = "<access_token>"
BUNDLE_ID = "<bundleId>"
headers = {"Authorization": f"Bearer {TOKEN}"}
base = f"{API_URL}/v1/bundles/{BUNDLE_ID}"
# 1. Discover outputs.
options = requests.get(
f"{base}/processing/options",
headers=headers,
params={"page": 1, "limit": 20},
).json()
options_by_type = {item["type"]: item for item in options["items"]}
to_process = [
item["type"]
for item in options["items"]
if not item["hasProcessing"]
]
total_cost = sum(options_by_type[t]["cost"] for t in to_process)
print(f"Launching: {to_process} (cost: {total_cost})")
# 2. Start processing.
response = requests.post(
f"{base}/processing",
headers=headers,
json={"requestedComponents": to_process, "cost": total_cost},
)
if response.status_code == 409:
raise RuntimeError(
f"Processing failed to start: {response.json().get('error')}"
)
response.raise_for_status() # expect 204
# 3. Poll until every job is terminal.
TERMINAL = {"Ready", "Failed", "FailedToLaunch", "KnownError"}
POLL_SECONDS = 15
TIMEOUT_SECONDS = 3600
deadline = time.time() + TIMEOUT_SECONDS
while time.time() < deadline:
listing = requests.get(
f"{base}/processings",
headers=headers,
).json()
processings = listing.get("items", [])
for p in processings:
print(f" {p['id']}: {p['status']} ({p['progress']}%)")
if processings and all(p["status"] in TERMINAL for p in processings):
break
time.sleep(POLL_SECONDS)
else:
raise TimeoutError("Processing did not finish within the timeout")

To retrieve the raw input files that were uploaded (for archival, reprocessing, or audit), see Downloading Input Files.