REST API reference

All paths are prefixed with /v1 unless noted. The canonical, always-accurate reference is the live OpenAPI spec rendered as Swagger UI on the OpenAPI page — this document gives the resource-shape overview and the most-used endpoint groups for quick orientation.

Resource model

        flowchart LR
    Tenant["Tenant"] --> Project["Project"]
    Project --> Dataset["Dataset"]
    Project --> Job["Job"]
    Project --> Reconstruction["Reconstruction"]
    Dataset --> Image["Image"]
    Reconstruction --> SubModel["SubModel"]
    Reconstruction --> SealedSnapshot["Sealed snapshot"]
    Image -.-> Blob
    Upload["Upload"] --> Blob["Blob"]
    Job --> Task["Task"]
    

Resource

Lifetime

Owns

Created via

Notes

Project

persistent

Datasets / Reconstructions / Jobs

POST /v1/projects

Top-level workspace

Dataset

persistent

Images, ImageSource

POST /v1/projects/{pid}/datasets

Discriminated source.kind: upload / local / s3

Image

persistent

(links a Blob or rel_path)

POST /v1/datasets/{did}/images, :batchCreate

Per-image EXIF / thumbnail / bytes endpoints

Upload

TTL (24h default)

one Blob after finalize

POST /v1/uploads

Chunked: PATCH chunks, POST :finalize to seal

Blob

content-addressed

none (deduplicated)

implicit from upload finalize

sha256-keyed; reference-counted

Job

persistent

Tasks, ProgressEvents

every job-submitting POST returns 202 + Location

LRO; :cancel / :resume colon verbs

Task

persistent

(worker result via outputs_ref)

created by orchestrator from DAG

Status enum: `pending

Reconstruction

persistent

SubModels + SealedSnapshots

implicit from :features / pipelines

Status mirrors driving Job

SubModel

persistent

one sealed dir per model

implicit (one per pycolmap component)

Indexed by idx; sealed paths in sparse/N/

SealedSnapshot

append-only

files (cameras, images, points.bin, …)

implicit on phase boundary

Reads via /snapshots/{seq}/{name}; points.bin is the binary points wire format

ApiKey

persistent

tenant binding

POST /v1/admin/api-keys (admin)

Raw key returned once at issue time

Conventions

  • Auth: see authentication — default auth_mode=none for dev; auth_mode=api_key enables Authorization: Bearer <key>.

  • Errors: application/problem+json per RFC 7807 — see errors. Typed errors[] array on 422.

  • IDs: 26-char ULIDs (Crockford base32, sortable, timestamp-prefixed).

  • Timestamps: ISO-8601 / RFC 3339 in UTC.

  • Pagination (AIP-158): keyset via ?page_token= + ?page_size=. Responses include next_page_token (null ends the cursor). Page tokens are opaque — clients MUST NOT parse them.

  • Long-running ops: POST returns 202 Accepted with JobAcceptedResponse{job_id, task_ids[], …} and a Location: /v1/jobs/{id} header. Cancel via POST /v1/jobs/{jid}:cancel (AIP-136 colon verb), not DELETE. Poll /v1/jobs/{jid} for lifecycle state, /v1/jobs/{jid}/progress for dashboard-friendly progress, or /v1/jobs/{jid}/events for the full event stream.

  • Capabilities vs actions: /v1/capabilities advertises portable sfmapi feature flags only. Backend-native commands live in /v1/backend/actions; action ids are stable dot-namespaced strings. Treat action ids as opaque and URL-encode them when building path requests.

  • Portable vs backend-specific options: stage specs keep portable knobs at the top level. Provider-specific knobs go in backend_options and are discoverable from /v1/backend/config-schemas.

  • Plugin hub: install and enable backend plugins with the sfmapi plugins ... CLI or /v1/admin/plugins... operator routes. Public SfM job APIs never install plugins implicitly; HTTP execution requires allow_unsafe_execution=true.

  • Idempotency: Idempotency-Key header on POST /v1/uploads (replay-safe).

  • Caching: sealed snapshot files emit strong ETag + Cache-Control: public, max-age=31536000, immutable.

  • SSE / WebSocket: GET /v1/jobs/{id}/events (SSE) or GET /ws/v1/jobs/{id} (WS). Both honor Last-Event-ID resume.

Discovery & meta

Method

Path

Purpose

GET

/healthz

Liveness probe

GET

/readyz

Readiness — DB + queue checks

GET

/version

sfmapi + backend SHAs

GET

/spec

Spec discovery envelope

GET

/v1/capabilities

Backend feature flags (dot-notated names)

GET

/v1/backend

Active backend identity, runtime versions, action links, and config-schema links

GET

/v1/backend/providers

Enabled providers discovered from installed plugins

GET

/v1/backend/routing

Provider priority and default routing-profile state

GET

/v1/camera-models

Portable camera model parameter layouts

GET

/openapi.json

OpenAPI 3.1 document

GET

/metrics

Prometheus exposition

Backend actions

Backend actions expose backend-native tools, such as COLMAP or OpenMVG commands, without turning those tool names into portable sfmapi capabilities.

Method

Path

Body / Query

Returns

GET

/v1/backend

-

BackendOut

GET

/v1/backend/actions

?page_token=&page_size=&include_schemas=false

Page<BackendAction>

GET

/v1/backend/actions/{action_id}

-

BackendAction with schemas

POST

/v1/backend/actions/{action_id}:validate

{inputs}

BackendActionValidateResponse

POST

/v1/backend/actions/{action_id}:run

{project_id, inputs}

202 + JobAcceptedResponse

GET

/v1/backend/config-schemas

?page_token=&page_size=&include_schemas=true

Page<BackendConfigSchema>

GET

/v1/backend/config-schemas/{config_id}

-

BackendConfigSchema

GET

/v1/backend/artifact-contracts

?page_token=&page_size=

Page<BackendArtifactContract>

GET

/v1/backend/artifact-contracts/{contract_id}

-

BackendArtifactContract

GET

/v1/backend/providers

?page_token=&page_size=

Page<Provider>

GET

/v1/backend/routing

-

RoutingOut

GET /v1/backend/actions omits input_schema and output_schema by default so catalog reads stay small. Pass include_schemas=true, or read one action, when a UI needs form fields. :run enqueues a normal backend_action job, returns Location: /v1/jobs/{id}, and includes optional action_id and backend fields in the accepted-job body. When SFMAPI_MCP_MODE=local or SFMAPI_MCP_ENABLED=true mounts MCP into the API process, GET /v1/backend also advertises _links.mcp and _links.mcp_status.

Backend config schemas describe valid keys for backend_options on portable stage specs. They are scoped by stage, optional capability, and optional provider. sfmapi validates unknown keys and basic JSON types before queuing a job when the active backend publishes a matching schema; otherwise the options pass through and the backend validates them.

Backend artifact contracts describe the portable artifact kinds and format ids a stage accepts and emits. Core sfmapi.*.v1 formats are stable interchange contracts; backend-native formats remain namespaced extensions such as colmap.features.database.v1 or hloc.features.h5.v1. Conversions are explicit metadata on the contract and should declare whether they are lossless.

Provider discovery is driven by sfm_hub plugin manifests plus local install state. A clean sfmapi install can return an empty provider page and still run the configured backend directly. Once multiple enabled providers expose the same portable capability, sfmapi requires either a request-level provider or a project, workspace, default, or priority routing rule.

Example feature extraction request:

{
  "spec": {
    "type": "sift",
    "max_num_features": 8192,
    "backend_options": {
      "SiftExtraction.peak_threshold": 0.01
    }
  }
}

Projects

Method

Path

Body

Returns

POST

/v1/projects

{name, description?}

Project

GET

/v1/projects

?page_token=&page_size=

Page<Project>

GET

/v1/projects/{pid}

Project

PATCH

/v1/projects/{pid}

ProjectPatch + optional ?update_mask=

Project

DELETE

/v1/projects/{pid}

204

POST

/v1/projects/{pid}/datasets:from_video

VideoFramesRequest

202 + JobAccepted

POST

/v1/projects/{pid}/datasets:import_kapture

KaptureImportRequest

202 + JobAccepted

PATCH accepts an optional AIP-161 update_mask query parameter. Mask paths are comma-separated, body-relative, and must also be present in the JSON body; without a mask, sfmapi applies the body fields that are present.

Uploads (chunked)

Method

Path

Body / Headers

Returns

POST

/v1/uploads

{expected_size, content_type?, expected_sha?} + Idempotency-Key

Upload

GET

/v1/uploads/{uid}

Upload

PATCH

/v1/uploads/{uid}

raw chunk + Content-Range: bytes A-B/T

Upload

POST

/v1/uploads/{uid}:finalize

{} (or X-Content-SHA256 header)

Upload

Datasets & images

Method

Path

Body

Returns

POST

/v1/projects/{pid}/datasets

{name, source: SourceSpec, camera_model, intrinsics_mode, is_spherical, rig_config?, respect_exif_orientation}

Dataset

GET

/v1/projects/{pid}/datasets

?page_token=&page_size=

Page<Dataset>

GET

/v1/projects/{pid}/datasets/{did}

Dataset

PATCH

/v1/projects/{pid}/datasets/{did}

DatasetPatch + optional ?update_mask=

Dataset

DELETE

/v1/projects/{pid}/datasets/{did}

204

POST

/v1/datasets/{did}:render_cubemap

CubemapProjectionRequest or ?face_size=

202 + JobAccepted

POST

/v1/datasets/{did}:render_equirectangular

EquirectangularProjectionRequest

202 + JobAccepted

POST

/v1/datasets/{did}:render_perspective

PerspectiveProjectionRequest

202 + JobAccepted

POST

/v1/datasets/{did}:project_images

ProjectionJobRequest

202 + JobAccepted

POST

/v1/datasets/{did}/images

ImageCreate

Image

POST

/v1/datasets/{did}/images:batchCreate

BatchCreateImagesRequest{requests[]}

BatchCreateImagesResponse{images[]}

GET

/v1/datasets/{did}/images

?page_token=&page_size=

Page<Image>

DELETE

/v1/images/{iid}

204

DELETE

/v1/datasets/{did}/images/{name}

legacy label-addressed delete

204

GET

/v1/images/{iid}

Image

GET

/v1/images/{iid}/bytes

If-None-Match (optional)

image bytes

GET

/v1/images/{iid}/thumbnail

?size=N

JPEG; requires the optional image-processing extra

GET

/v1/images/{iid}/exif

ImageExifResponse

GET / PUT / DELETE

/v1/images/{iid}/pose_prior

PosePrior

PosePrior | null

GET / PUT

/v1/datasets/{did}/pose_priors

PosePriorsBulkRequest

PosePriorsBulkResponse

Projection jobs produce a projection.images.v1 stage artifact and a projection_manifest.json. The manifest includes generic SfM metadata: source_images, output_images, face geometry when applicable, and an optional derived_dataset block. When output.create_dataset=true (default), sfmapi registers the generated image directory as a normal dataset so later feature, match, and mapping stages can consume it. If the requested derived dataset name already exists in the project, sfmapi appends a deterministic task suffix instead of failing the job; task retries reuse the already-registered derived dataset.

The built-in projection engine is deliberately narrow. With the projection extra installed it handles projection.equirectangular_to_cubemap using vectorized NumPy plus OpenCV image I/O, with nearest and linear sampling. cubic, lanczos, projection.cubemap_to_equirectangular, and projection.equirectangular_to_perspective are contract-only in core; a backend must advertise those capabilities to serve them.

SfM stages (single-task jobs)

Method

Path

Body

Returns

POST

/v1/datasets/{did}/features

{spec: FeaturesSpec}

202 + JobAccepted

POST

/v1/datasets/{did}/matches

{pairs: PairsSpec, matcher: MatcherSpec, input_artifacts?}

202 + JobAccepted

POST

/v1/datasets/{did}/verify

{spec: VerifySpec, input_artifacts?}

202 + JobAccepted

PairsSpec and MatcherSpec are independent. A deployment can select pairs with hloc-style retrieval and still match with a COLMAP or learned matcher. Use optional provider only when two installed providers expose the same portable capability.

Explicit pair lists are portable:

{
  "pairs": {
    "strategy": "explicit",
    "image_pairs": [{"image_name1": "a.jpg", "image_name2": "b.jpg"}]
  },
  "matcher": {"type": "superglue", "provider": "hloc"}
}

For large hloc/COLMAP pair files, upload the text file through /v1/uploads, finalize it, then pass {"strategy": "explicit", "pairs_blob_sha": "<sha256>"}. The file format is one image1 image2 pair per line.

Use input_artifacts to select a previous stage output, for example {"features": {"artifact_id": "...", "kind": "features.local.v1"}} when matching against a specific feature artifact.

Pipelines (recipe sugar)

Method

Path

Body

Returns

POST

/v1/projects/{pid}/pipelines/{recipe}

{dataset_id, features?, pairs?, matcher?, verify?, spec: PipelineSpec, input_artifacts?}

202 + JobAccepted

recipe {incremental, global, hierarchical, spherical} and spec.kind MUST match.

Reconstructions / submodels / snapshots

Method

Path

Query / Body

Returns

GET

/v1/reconstructions/{rid}

-

Reconstruction

GET

/v1/reconstructions/{rid}/artifacts

?page_token=&page_size=&kind=&task_id=&name=

Page<StageArtifact>

GET

/v1/reconstructions/{rid}/submodels

?page_token=&page_size=

Page<SubModel>

GET

/v1/submodels/{smid}

-

SubModel

GET

/v1/reconstructions/{rid}/snapshots

-

SnapshotListResponse

GET

/v1/reconstructions/{rid}/snapshots/{seq}/{name}

?download=true

file bytes

GET

/v1/reconstructions/{rid}/snapshots/{seq}/submodels/{idx}/{name}

?download=true

component file bytes

GET

/v1/reconstructions/{rid}/two_view_geometries.json

-

JSON

GET

/v1/reconstructions/{rid}/correspondence_graph.json

-

JSON

POST

/v1/reconstructions:merge

MergeRequest

202 + JobAccepted

Mapping jobs persist one SubModel per disconnected component. The root snapshot file routes expose the largest/default model; use the submodel snapshot route for a specific component.

Stage artifacts are typed outputs produced by worker stages. They are the selection surface for ambiguous pipelines, for example when a job produces both COLMAP SIFT matches and hloc/LightGlue matches, or when verification emits several candidate pair sets.

Artifacts

Method

Path

Query

Returns

GET

/v1/artifacts/kinds

-

Page<ArtifactKind>

GET

/v1/artifacts/formats

-

Page<ArtifactFormat>

POST

/v1/artifacts:import

ArtifactImportRequest

StageArtifact

GET

/v1/artifacts/{artifact_id}

-

StageArtifact

GET

/v1/artifacts/{artifact_id}/content

?download=true

file bytes

POST

/v1/artifacts/{artifact_id}:conversionPlan

{to_format? , accepted_formats?, require_lossless?}

ArtifactConversionPlan

POST

/v1/artifacts/{artifact_id}:convert

{to_format? , accepted_formats?, to_kind?, name?, options?}

202 + JobAccepted

POST

/v1/artifacts/{artifact_id}:validate

-

ArtifactValidation

StageArtifact.uri is metadata, not a portability contract. Use /content for local server-managed file artifacts. Remote URIs and paths outside sfmapi-managed storage are not dereferenced by the API.

Stage submissions can pass selected artifacts through input_artifacts, for example:

{
  "input_artifacts": {
    "verified_matches": {
      "artifact_id": "01HZ...",
      "kind": "matches.verified.v1"
    }
  }
}

Core roles validate expected kinds before job creation. Unknown backend-specific roles are allowed if they use the same dot-key syntax. Use /v1/artifacts:import to register an existing server-local or remote artifact URI without copying bytes. sfmapi creates a completed import job/task so the artifact has the same ownership, listing, and validation behavior as worker-produced artifacts.

Core portable artifact kinds include features.local.v1, features.global.v1, pairs.image_names.v1, matches.indexed.v1, matches.coordinates.v1, matches.dense.v1, matches.verified.v1, and reconstruction.sparse.v1. Each kind has a default canonical format id such as sfmapi.features.local.v1 or sfmapi.matches.verified.v1. Backend-native artifacts should use a namespaced same-family kind such as features.hloc_h5 or matches.database.colmap, plus a backend-owned artifact_format such as hloc.features.h5.v1 or colmap.matches.database.v1.

/v1/artifacts/{artifact_id}:conversionPlan chooses the shortest conversion path from the active backend’s artifact contracts. Pass accepted_formats in preference order to let sfmapi negotiate the target format; pass to_format for an exact target. :convert submits the selected conversion as a normal job and requires the backend to implement convert_artifact(...). Multi-step paths are executed inside that conversion task by calling the backend once per step. :validate checks the artifact descriptor, core kind/format compatibility, local managed files, declared byte sizes, SHA-256 digests, and JSON manifests when bytes are available.

{name} is one of cameras.json | images.json | rigs.json | frames.json | pose_graph.json | summary.json | points.bin | points_preview.bin | tiles/index.json | dense/index.json | dense/fused.bin.

Reconstruction-level stages (LRO)

Method

Path

Body

Returns

POST

/v1/reconstructions/{rid}/localize

LocalizationRequest{blob_sha, sift?}

202 + JobAccepted

POST

/v1/reconstructions/{rid}/georegister

Sim3

202 + JobAccepted

POST

/v1/reconstructions/{rid}/mesh

MeshRequest{method, options?}

202 + JobAccepted

POST

/v1/reconstructions/{rid}/dense

202 + JobAccepted

POST

/v1/reconstructions/{rid}:to_cubemap

202 + JobAccepted

Similarity

Method

Path

Body

Returns

GET

/v1/datasets/{did}/similarity

?image_id=&k=&strategy=&include_self=

SimilarityQueryResponse

POST

/v1/datasets/{did}/similarity:build

?strategy=dhash|vlad&force=

200 (dhash, optional image-processing extra) or 202 (vlad)

One-shot (bytes-in / typed-result-out)

For “right now” use cases that don’t need a Project/Dataset row.

Method

Path

Body

Returns

POST

/v1/oneshot/features

image bytes (Content-Type: image/...)

OneShotFeaturesResponse

POST

/v1/oneshot/localize

image bytes + ?recon_id=

OneShotLocalizeResponse

Bytes are tempfile’d then deleted; no DB row is created. Capped at SFMAPI_ONESHOT_MAX_REQUEST_BYTES (50 MiB default).

Jobs

Method

Path

Body

Returns

GET

/v1/jobs

?page_token=&page_size=&status=

Page<JobOut>

GET

/v1/jobs/{jid}/artifacts

?page_token=&page_size=&kind=&task_id=&name=

Page<StageArtifact>

GET

/v1/jobs/{jid}

JobDetail

GET

/v1/jobs/{jid}/progress

JobProgressOut

POST

/v1/jobs/{jid}:cancel

?force=true (optional)

JobOut

POST

/v1/jobs/{jid}:resume

202 + JobOut

GET

/v1/jobs/{jid}/events

Last-Event-ID (optional)

SSE stream of ProgressEvent

GET

/ws/v1/jobs/{jid}

WebSocket upgrade

bidirectional events

?status= filters on the closed JobStatus set: pending | running | succeeded | failed | cancelled | cancelled_dirty.

Progress snapshots

GET /v1/jobs/{jid}/progress is a compact polling view over durable task rows plus the latest persisted ProgressEvent records. It is intended for CLIs and dashboards that do not want to hold an SSE connection open.

curl -s "$BASE/v1/jobs/$JOB_ID/progress" \
  | jq '{status, progress, current_task_kind, current_phase}'

Important fields:

Field

Meaning

progress

Best-effort fraction from 0.0 to 1.0; terminal tasks count as complete

task_counts

Count of tasks by lifecycle status

current_task_id / current_task_kind

Running task when present, otherwise next pending task

current_phase

Latest reported phase, such as matching or global_positioning

latest_event_id

Durable cursor shared with the SSE stream

tasks[]

Per-task status, progress, latest event kind, elapsed time, and optional current / total

Progress is telemetry. Clients should not use exact percentages for scheduling decisions; use status to decide whether work is terminal.

Admin

/v1/admin/api-keys are operator routes. They are not tenant-scoped and are not protected by sfmapi’s tenant API-key dependency, so production deployments must restrict them with an external admin-only control-plane layer.

Method

Path

Body

Returns

POST

/v1/admin/api-keys

{tenant_id, name?}

IssueKeyResponse (raw_key returned once)

GET

/v1/admin/api-keys

[ApiKeyOut]

DELETE

/v1/admin/api-keys/{kid}

ApiKeyOut (revoked)

GET

/v1/admin/plugins

?query=&page_token=&page_size=

Page<PluginRegistryItem>

GET

/v1/admin/plugins/detect-tools

local external-tool detection

GET

/v1/admin/plugins/entry-points

?load=false

installed Python entry points

GET

/v1/admin/plugins/{plugin_id}

plugin manifest + local state

POST

/v1/admin/plugins/{plugin_id}:install

{method, github_url?, ref?, package_name?, dry_run?, allow_unsafe_execution?}

install plan or result

POST

/v1/admin/plugins/{plugin_id}:enable

plugin state

POST

/v1/admin/plugins/{plugin_id}:disable

plugin state

POST

/v1/admin/plugins/{plugin_id}:doctor

diagnostics

POST

/v1/admin/routing/profiles

{name, routes}

routing state

POST

/v1/admin/routing/default

{profile}

routing state

POST

/v1/admin/routing/projects/{project_id}

{profile}

routing state

POST

/v1/admin/routing/workspaces

{profile}

routing state

Plugin installation is an operator action. method="uv" creates a direct-reference command such as uv pip install "sfmapi-colmap-cli @ git+https://github.com/SFMAPI/sfmapi_colmap_cli.git@main"; mutable refs produce warnings. docker and external_tool modes are planned or recorded as runtime choices and are never invoked by normal job submission. Backend packages can expose [project.entry-points."sfmapi.backends"]; sfmapi plugins entry-points --load and sfmapi check-backend --load-entry-points validate those contracts.

ProgressEvent

ProgressEvent v1 — discriminated union for the SSE stream.

Workers emit events to events.jsonl (one JSON line per event); the API serves them via SSE with Last-Event-ID resume.

Schema is versioned: a schema_version literal is pinned per kind. New kinds extend the union; old kinds never change shape.

ProgressEvent kinds

ProgressEvent is a tagged union discriminated on kind:

  • phase_started / phase_progress / phase_completed — per-phase lifecycle (phase is one of Phase).

  • metric — scalar telemetry sample (key + value).

  • snapshot_available — a sealed snapshot is now readable (snapshot_seq + summary).

  • log_line / warning / error — opaque message channels.

Forward-compatibility: SDKs MUST treat unknown kind values as unknown and surface them through a generic catch-all rather than crashing the stream. New kinds are additive (new kind literal plus new arm of the union); existing kinds never change shape. schema_version is pinned to 1 across every variant — bump it in lock-step if the wire shape ever has to break, never per-kind.

class app.schemas.progress_event.ErrorEvent(*, schema_version=1, ts, job_id, task_id=None, seq, kind='error', error_class, message, detail=None)[source]

Bases: _Base

Parameters:
  • schema_version (Literal[1])

  • ts (datetime)

  • job_id (str)

  • task_id (str | None)

  • seq (int)

  • kind (Literal['error'])

  • error_class (str)

  • message (str)

  • detail (dict[str, Any] | None)

kind: Literal['error']
error_class: str
message: str
detail: dict[str, Any] | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.progress_event.LogLine(*, schema_version=1, ts, job_id, task_id=None, seq, kind='log_line', level, message)[source]

Bases: _Base

Parameters:
  • schema_version (Literal[1])

  • ts (datetime)

  • job_id (str)

  • task_id (str | None)

  • seq (int)

  • kind (Literal['log_line'])

  • level (Literal['DEBUG', 'INFO', 'WARNING', 'ERROR'])

  • message (str)

kind: Literal['log_line']
level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR']
message: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.progress_event.Metric(*, schema_version=1, ts, job_id, task_id=None, seq, kind='metric', key, value)[source]

Bases: _Base

Parameters:
  • schema_version (Literal[1])

  • ts (datetime)

  • job_id (str)

  • task_id (str | None)

  • seq (int)

  • kind (Literal['metric'])

  • key (str)

  • value (float)

kind: Literal['metric']
key: str
value: float
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.progress_event.PhaseCompleted(*, schema_version=1, ts, job_id, task_id=None, seq, kind='phase_completed', phase)[source]

Bases: _Base

Parameters:
  • schema_version (Literal[1])

  • ts (datetime)

  • job_id (str)

  • task_id (str | None)

  • seq (int)

  • kind (Literal['phase_completed'])

  • phase (Literal['feature_extraction', 'matching', 'geometric_verification', 'incremental_init', 'incremental_register', 'incremental_ba', 'global_rotation_avg', 'global_positioning', 'global_ba', 'hierarchical_cluster', 'hierarchical_merge', 'panorama', 'spherical', 'bundle_adjust', 'triangulate', 'relocalize', 'pose_graph_optimize', 'segment', 'export', 'vlad_index', 'backend_action', 'artifact_conversion'])

kind: Literal['phase_completed']
phase: Phase
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.progress_event.PhaseProgress(*, schema_version=1, ts, job_id, task_id=None, seq, kind='phase_progress', phase, current, total=None, rate=None)[source]

Bases: _Base

Parameters:
  • schema_version (Literal[1])

  • ts (datetime)

  • job_id (str)

  • task_id (str | None)

  • seq (int)

  • kind (Literal['phase_progress'])

  • phase (Literal['feature_extraction', 'matching', 'geometric_verification', 'incremental_init', 'incremental_register', 'incremental_ba', 'global_rotation_avg', 'global_positioning', 'global_ba', 'hierarchical_cluster', 'hierarchical_merge', 'panorama', 'spherical', 'bundle_adjust', 'triangulate', 'relocalize', 'pose_graph_optimize', 'segment', 'export', 'vlad_index', 'backend_action', 'artifact_conversion'])

  • current (int)

  • total (int | None)

  • rate (float | None)

kind: Literal['phase_progress']
phase: Phase
current: int
total: int | None
rate: float | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.progress_event.PhaseStarted(*, schema_version=1, ts, job_id, task_id=None, seq, kind='phase_started', phase)[source]

Bases: _Base

Parameters:
  • schema_version (Literal[1])

  • ts (datetime)

  • job_id (str)

  • task_id (str | None)

  • seq (int)

  • kind (Literal['phase_started'])

  • phase (Literal['feature_extraction', 'matching', 'geometric_verification', 'incremental_init', 'incremental_register', 'incremental_ba', 'global_rotation_avg', 'global_positioning', 'global_ba', 'hierarchical_cluster', 'hierarchical_merge', 'panorama', 'spherical', 'bundle_adjust', 'triangulate', 'relocalize', 'pose_graph_optimize', 'segment', 'export', 'vlad_index', 'backend_action', 'artifact_conversion'])

kind: Literal['phase_started']
phase: Phase
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.progress_event.SnapshotAvailable(*, schema_version=1, ts, job_id, task_id=None, seq, kind='snapshot_available', snapshot_seq, summary)[source]

Bases: _Base

Parameters:
  • schema_version (Literal[1])

  • ts (datetime)

  • job_id (str)

  • task_id (str | None)

  • seq (int)

  • kind (Literal['snapshot_available'])

  • snapshot_seq (int)

  • summary (dict[str, Any])

kind: Literal['snapshot_available']
snapshot_seq: int
summary: dict[str, Any]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.progress_event.Warning_(*, schema_version=1, ts, job_id, task_id=None, seq, kind='warning', message)[source]

Bases: _Base

Parameters:
  • schema_version (Literal[1])

  • ts (datetime)

  • job_id (str)

  • task_id (str | None)

  • seq (int)

  • kind (Literal['warning'])

  • message (str)

kind: Literal['warning']
message: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Pipeline specs

PipelineSpec — discriminated union of mapping pipelines.

Web layer accepts these. Workers translate them to engine-specific option classes inside the backend implementation — never out here.

PipelineSpec kinds

PipelineSpec is a tagged union discriminated on the kind field:

  • incremental -> IncrementalSpec

  • global -> GlobalSpec

  • hierarchical-> HierarchicalSpec

  • spherical -> SphericalSpec

Forward-compatibility: SDKs MUST treat unknown kind values as unsupported rather than failing the whole response. Add new variants in additive form (new kind literal + new arm of the union); never repurpose an existing kind. Backends advertise the recipes they implement via the pipelines.{kind} capability flags (see GET /v1/capabilities); requests against an unsupported kind return 501 capability_unavailable.

app.schemas.pipeline_spec.BackendOptions

Backend-specific option bag.

Portable fields on the stage specs are the stable sfmapi contract. backend_options is reserved for options discovered from GET /v1/backend/config-schemas and interpreted by the selected backend provider.

alias of dict[str, Any]

class app.schemas.pipeline_spec.BundleAdjustmentSpec(*, version=1, mode='standard', provider=None, refine_focal_length=True, refine_principal_point=False, refine_extra_params=True, max_num_iterations=100, loss_kernel='squared', loss_threshold=1.0, backend_options=<factory>)[source]

Bases: BaseModel

Standalone bundle-adjustment spec.

mode selects the algorithm:
  • standard: a single ceres / baxx solve over all registered cameras + 3D points (capability ba.standard).

  • two_stage: a two-pass refinement (capability ba.two_stage).

  • featuremetric: Pixel-Perfect SfM-style refinement that minimizes a CNN-feature error, not raw reprojection (capability ba.featuremetric). Requires a backend with learned-feature support.

loss_kernel chooses the robust loss applied to per-residual cost: squared (no robustification), huber, cauchy, soft_l1, tukey. loss_threshold is the kernel scale (in pixels for reprojection loss).

Parameters:
  • version (Literal[1])

  • mode (Literal['standard', 'two_stage', 'featuremetric'])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • refine_focal_length (bool)

  • refine_principal_point (bool)

  • refine_extra_params (bool)

  • max_num_iterations (int)

  • loss_kernel (Literal['squared', 'huber', 'cauchy', 'soft_l1', 'tukey'])

  • loss_threshold (float)

  • backend_options (dict[str, Any])

version: Literal[1]
mode: Literal['standard', 'two_stage', 'featuremetric']
provider: str | None
refine_focal_length: bool
refine_principal_point: bool
refine_extra_params: bool
max_num_iterations: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

loss_kernel: Literal['squared', 'huber', 'cauchy', 'soft_l1', 'tukey']
loss_threshold: float
backend_options: BackendOptions
app.schemas.pipeline_spec.FeatureType

Canonical names for local feature extractors. The capability flag for an extractor is features.extract.{type}; the colmap_mod backend advertises features.extract.sift and (when pycolmap exposes it) features.extract.aliked.

alias of Literal[‘sift’, ‘superpoint’, ‘aliked’, ‘disk’, ‘r2d2’, ‘d2net’]

class app.schemas.pipeline_spec.FeaturesSpec(*, version=1, type='sift', provider=None, max_num_features=8192, use_gpu=True, seed=0, backend_options=<factory>, input_artifacts=<factory>)[source]

Bases: BaseModel

Type-tagged feature extractor request.

Backends report which type values they support via the features.extract.{type} capability flags. Unsupported types return 501 with the canonical capability name.

Backend-specific extractor controls belong in backend_options.

Parameters:
  • version (Literal[1])

  • type (Literal['sift', 'superpoint', 'aliked', 'disk', 'r2d2', 'd2net'])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • max_num_features (int)

  • use_gpu (bool)

  • seed (int)

  • backend_options (dict[str, Any])

  • input_artifacts (dict[str, ArtifactRef])

version: Literal[1]
type: FeatureType
provider: str | None
max_num_features: int
use_gpu: bool
seed: int
backend_options: BackendOptions
input_artifacts: ArtifactInputMap
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.pipeline_spec.GlobalSpec(*, version=1, provider=None, seed=0, max_runtime_seconds=None, snapshot_frames_freq=50, backend_options=<factory>, input_artifacts=<factory>, kind='global', backend='AUTO', formulation='AUTO', use_incremental_quality_fallback=True)[source]

Bases: _SpecBase

Parameters:
  • version (Literal[1])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • seed (int)

  • max_runtime_seconds (int | None)

  • snapshot_frames_freq (int | None)

  • backend_options (dict[str, Any])

  • input_artifacts (dict[str, ArtifactRef])

  • kind (Literal['global'])

  • backend (Literal['AUTO', 'BAXX', 'CERES'])

  • formulation (Literal['AUTO', 'EXPLICIT_SCALE', 'ELIMINATED_SCALE'])

  • use_incremental_quality_fallback (bool)

kind: Literal['global']
backend: Literal['AUTO', 'BAXX', 'CERES']
formulation: Literal['AUTO', 'EXPLICIT_SCALE', 'ELIMINATED_SCALE']
use_incremental_quality_fallback: bool
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.pipeline_spec.HierarchicalSpec(*, version=1, provider=None, seed=0, max_runtime_seconds=None, snapshot_frames_freq=50, backend_options=<factory>, input_artifacts=<factory>, kind='hierarchical', cluster_max_size=100, cluster_overlap=25)[source]

Bases: _SpecBase

Parameters:
  • version (Literal[1])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • seed (int)

  • max_runtime_seconds (int | None)

  • snapshot_frames_freq (int | None)

  • backend_options (dict[str, Any])

  • input_artifacts (dict[str, ArtifactRef])

  • kind (Literal['hierarchical'])

  • cluster_max_size (int)

  • cluster_overlap (int)

kind: Literal['hierarchical']
cluster_max_size: int
cluster_overlap: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.pipeline_spec.ImagePairRef(*, image_name1, image_name2)[source]

Bases: BaseModel

One explicit pair of dataset image names.

Parameters:
  • image_name1 (Annotated[str, MinLen(min_length=1), MaxLen(max_length=2048)])

  • image_name2 (Annotated[str, MinLen(min_length=1), MaxLen(max_length=2048)])

image_name1: str
image_name2: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.pipeline_spec.IncrementalSpec(*, version=1, provider=None, seed=0, max_runtime_seconds=None, snapshot_frames_freq=50, backend_options=<factory>, input_artifacts=<factory>, kind='incremental', init_image_pair=None, multiple_models=True, max_num_models=50, min_num_matches=15, ba_global_use_pba=True, extract_colors=True)[source]

Bases: _SpecBase

Parameters:
  • version (Literal[1])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • seed (int)

  • max_runtime_seconds (int | None)

  • snapshot_frames_freq (int | None)

  • backend_options (dict[str, Any])

  • input_artifacts (dict[str, ArtifactRef])

  • kind (Literal['incremental'])

  • init_image_pair (tuple[str, str] | None)

  • multiple_models (bool)

  • max_num_models (int)

  • min_num_matches (int)

  • ba_global_use_pba (bool)

  • extract_colors (bool)

kind: Literal['incremental']
init_image_pair: tuple[str, str] | None
multiple_models: bool
max_num_models: int
min_num_matches: int
ba_global_use_pba: bool
extract_colors: bool
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.pipeline_spec.MatcherSpec(*, version=1, type='nn-mutual', provider=None, use_gpu=True, cross_check=True, max_ratio=0.8, max_distance=0.7, backend_options=<factory>, input_artifacts=<factory>)[source]

Bases: BaseModel

Per-pair feature matcher.

nn-mutual is the COLMAP default (mutual nearest-neighbor). nn-ratio adds Lowe’s ratio test. superglue / lightglue are learned matchers. loftr is semi-dense (no separate extractor — set FeaturesSpec.type to a placeholder).

Parameters:
  • version (Literal[1])

  • type (Literal['nn-mutual', 'nn-ratio', 'superglue', 'lightglue', 'loftr', 'mast3r'])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • use_gpu (bool)

  • cross_check (bool)

  • max_ratio (float)

  • max_distance (float)

  • backend_options (dict[str, Any])

  • input_artifacts (dict[str, ArtifactRef])

version: Literal[1]
type: MatcherType
provider: str | None
use_gpu: bool
cross_check: bool
max_ratio: float
max_distance: float
backend_options: BackendOptions
input_artifacts: ArtifactInputMap
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

app.schemas.pipeline_spec.MatcherType

Per-pair matching algorithm. The capability flag is matchers.{type}. Backends pick the subset they implement.

alias of Literal[‘nn-mutual’, ‘nn-ratio’, ‘superglue’, ‘lightglue’, ‘loftr’, ‘mast3r’]

app.schemas.pipeline_spec.PairStrategy

How to pick which image pairs to match.

  • exhaustive: every pair (O(N²)).

  • sequential: consecutive pairs within overlap window (video / time-ordered).

  • spatial: pairs within a metric distance (needs GPS or pose priors).

  • vocabtree: COLMAP vocabulary tree retrieval.

  • retrieval: pairs from a similarity index (VLAD / NetVLAD / custom). Requires the dataset to have a built similarity index.

  • from_poses: pairs whose camera centers are within overlap_distance_m AND whose principal axes are within max_angle_deg.

  • explicit: use exactly the image-name pairs supplied inline or through an uploaded pair-list blob.

alias of Literal[‘exhaustive’, ‘sequential’, ‘spatial’, ‘vocabtree’, ‘retrieval’, ‘from_poses’, ‘explicit’]

class app.schemas.pipeline_spec.PairsSpec(*, version=1, strategy='exhaustive', provider=None, overlap=10, vocab_tree_path=None, retrieval_strategy='vlad', retrieval_k=20, overlap_distance_m=None, max_angle_deg=None, image_pairs=None, pairs_blob_sha=None, pairs_blob_format='image_name_pairs_txt', backend_options=<factory>, input_artifacts=<factory>)[source]

Bases: BaseModel

Pair-selection strategy. Decoupled from the matcher so the standard supports “select pairs with hloc-style retrieval, then match with any local-feature matcher” workflows.

Capability flag is pairs.{strategy}.

Parameters:
  • version (Literal[1])

  • strategy (Literal['exhaustive', 'sequential', 'spatial', 'vocabtree', 'retrieval', 'from_poses', 'explicit'])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • overlap (int)

  • vocab_tree_path (str | None)

  • retrieval_strategy (Literal['dhash', 'vlad', 'netvlad'])

  • retrieval_k (int)

  • overlap_distance_m (float | None)

  • max_angle_deg (float | None)

  • image_pairs (list[ImagePairRef] | None)

  • pairs_blob_sha (Annotated[str | None, MinLen(min_length=64), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[0-9a-f]{64}$')])

  • pairs_blob_format (Literal['image_name_pairs_txt'])

  • backend_options (dict[str, Any])

  • input_artifacts (dict[str, ArtifactRef])

version: Literal[1]
strategy: PairStrategy
provider: str | None
overlap: int
vocab_tree_path: str | None
retrieval_strategy: Literal['dhash', 'vlad', 'netvlad']
retrieval_k: int
overlap_distance_m: float | None
max_angle_deg: float | None
image_pairs: list[ImagePairRef] | None
pairs_blob_sha: str | None
pairs_blob_format: Literal['image_name_pairs_txt']
backend_options: BackendOptions
input_artifacts: ArtifactInputMap
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.pipeline_spec.SphericalSpec(*, version=1, provider=None, seed=0, max_runtime_seconds=None, snapshot_frames_freq=50, backend_options=<factory>, input_artifacts=<factory>, kind='spherical', panorama=True)[source]

Bases: _SpecBase

Parameters:
  • version (Literal[1])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • seed (int)

  • max_runtime_seconds (int | None)

  • snapshot_frames_freq (int | None)

  • backend_options (dict[str, Any])

  • input_artifacts (dict[str, ArtifactRef])

  • kind (Literal['spherical'])

  • panorama (bool)

kind: Literal['spherical']
panorama: bool
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class app.schemas.pipeline_spec.VerifySpec(*, version=1, provider=None, use_gpu=True, min_inlier_ratio=0.25, backend_options=<factory>, input_artifacts=<factory>)[source]

Bases: BaseModel

Parameters:
  • version (Literal[1])

  • provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])

  • use_gpu (bool)

  • min_inlier_ratio (float)

  • backend_options (dict[str, Any])

  • input_artifacts (dict[str, ArtifactRef])

version: Literal[1]
provider: str | None
use_gpu: bool
min_inlier_ratio: float
backend_options: BackendOptions
input_artifacts: ArtifactInputMap
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Binary points format

Content-Type: application/x-sfm-points-v1. Fixed 32-byte header, 26 bytes per point.

Binary points wire format — application/x-sfm-points-v1.

Header (44 bytes, little-endian):

magic: 8 bytes = b”SFMP3Dx00x00” version: uint32 = 1 count: uint64 bbox_min: 3 x float32 (12 B) bbox_max: 3 x float32 (12 B)

Each record (26 bytes, little-endian):

xyz: 3 x float32 (12) rgb: 3 x uint8 (3) _pad: 1 x uint8 (1) track_len: uint16 (2) point3d_id: uint64 (8)

Records are written in ascending point3d_id order so that HTTP Range requests can treat the file as a fixed-stride array.

class app.schemas.points_binary.Point3DRecord(point3d_id: 'int', xyz: 'tuple[float, float, float]', rgb: 'tuple[int, int, int]', track_len: 'int')[source]

Bases: object

Parameters:
  • point3d_id (int)

  • xyz (tuple[float, float, float])

  • rgb (tuple[int, int, int])

  • track_len (int)

point3d_id: int
xyz: tuple[float, float, float]
rgb: tuple[int, int, int]
track_len: int
app.schemas.points_binary.write_header(fh, *, count, bbox_min, bbox_max)[source]
Parameters:
  • fh (BinaryIO)

  • count (int)

  • bbox_min (tuple[float, float, float])

  • bbox_max (tuple[float, float, float])

Return type:

None

app.schemas.points_binary.write_record(fh, rec)[source]
Parameters:
  • fh (BinaryIO)

  • rec (Point3DRecord)

Return type:

None

app.schemas.points_binary.read_header(fh)[source]
Parameters:

fh (BinaryIO)

Return type:

tuple[int, tuple[float, float, float], tuple[float, float, float]]

app.schemas.points_binary.read_record(buf, offset=0)[source]
Parameters:
  • buf (bytes)

  • offset (int)

Return type:

Point3DRecord

app.schemas.points_binary.encode_all(records, *, bbox_min, bbox_max)[source]
Parameters:
  • records (Iterable[Point3DRecord])

  • bbox_min (tuple[float, float, float])

  • bbox_max (tuple[float, float, float])

Return type:

bytes

app.schemas.points_binary.decode_records(buf)[source]
Parameters:

buf (bytes)

Return type:

tuple[list[Point3DRecord], tuple[float, float, float], tuple[float, float, float]]

Records are written in ascending point3d_id order so HTTP Range requests treat the file as a fixed-stride array. points_preview.bin is the same format, decimated.