Quickstart

Stand up sfmapi on a single host with no Docker, no Redis, no Postgres. Allow about five minutes. The defaults give you SQLite on disk, filesystem blob storage, and an in-process worker — perfect for development, embedded use, and trying the wire surface.

To run real reconstructions you will additionally register a backend — that step is optional and orthogonal to everything below.

Prerequisites

  • Python 3.12

  • uv (or pip if you prefer)

  • A few hundred MB of disk for the Python wheel cache

1. Install

git clone https://github.com/sfmapi/sfmapi
cd sfmapi
uv venv
uv pip install -e ".[dev]"
cp .env.example .env
uv run alembic upgrade head

alembic upgrade head creates sfmapi.db (SQLite) in the working directory using the schema. The .env carries the standalone defaults (queue=inline, blobs=fs, sqlite db).

The base API reads image dimensions with header-only parsing. Install the image-processing extra only for optional thumbnail rendering and dhash similarity:

uv pip install -e ".[image-processing]"

2. Start the server

uv run uvicorn sfmapi.runtime:create_app --factory --reload

Verify:

curl http://localhost:8080/healthz       # {"status":"ok"}
curl http://localhost:8080/version       # {"sfmapi":"0.0.1","backend":null}
curl http://localhost:8080/v1/spec       # spec metadata + openapi.json link

backend is null because no SfM engine is registered. The wire surface is fully responsive; SfM-specific operations will return 501 CapabilityUnavailableError until you register a backend.

3. Drive the API end-to-end

# Create a project
curl -sX POST http://localhost:8080/v1/projects \
    -H 'Content-Type: application/json' \
    -d '{"name": "vacation-2026"}' | tee /tmp/proj.json
PID=$(jq -r .project_id /tmp/proj.json)

# Upload an image (chunked, content-addressed)
UID=$(curl -sX POST http://localhost:8080/v1/uploads \
    -H 'Content-Type: application/json' \
    -d '{"expected_size": '$(stat -c%s img1.jpg)'}' | jq -r .upload_id)
curl -sX PATCH http://localhost:8080/v1/uploads/$UID \
    -H "Content-Range: bytes 0-$(($(stat -c%s img1.jpg)-1))/$(stat -c%s img1.jpg)" \
    --data-binary @img1.jpg
SHA=$(curl -sX POST http://localhost:8080/v1/uploads/$UID:finalize \
    -H 'Content-Type: application/json' -d '{}' | jq -r .blob_sha)

# Create a dataset that references the uploaded blob
curl -sX POST http://localhost:8080/v1/projects/$PID/datasets \
    -H 'Content-Type: application/json' \
    -d '{"name": "trip", "source": {"kind": "upload",
         "entries": [{"name": "img1.jpg", "blob_sha": "'$SHA'"}]}}' | jq .

4. Use a typed SDK (Python or TypeScript)

uv pip install ../sfmapi-sdk/python/sfmapi_client_gen
from sfmapi_client_gen import Client
from sfmapi_client_gen.api.projects import projects_create
from sfmapi_client_gen.models import ProjectCreate

with Client(base_url="http://localhost:8080") as c:
    project = projects_create.sync(client=c, body=ProjectCreate(name="vacation-2026"))
    print(project.project_id)

For TypeScript:

cd ../sfmapi-sdk/typescript && npm install && npm run build
import { createSfmApiClient } from "@sfmapi/client/generated";

const c = createSfmApiClient({ baseUrl: "http://localhost:8080" });
const { data: project } = await c.raw.POST("/v1/projects", {
  body: { name: "vacation-2026" },
});

Where to go next

  • Implement a backend — make the SfM endpoints actually do reconstructions.

  • Architecture — why the boundaries are where they are.

  • Curl tour — the same flow as above with every shape laid out.

  • API reference — full endpoint list.

  • Deployment — multi-host scale-out, Postgres, Redis-backed ARQ workers, observability.