API reference¶
The interactive reference lives in its own standalone page, openapi/index.html, so it can be opened directly without running mkdocs serve. It renders openapi/openapi.yaml; download that file to generate a client or import it into Postman.
No authentication
This API has no authentication of its own and is not behind a reverse proxy. Deploy it only on a network whose access you control, and do not expose it directly to the public internet.
Try it out needs a proxy or same-origin setup
The server sends no CORS headers, so Swagger UI's Try it out fails against a running instance from this docs page. The curl examples below work instead.
Request ids¶
Every response carries an X-Request-ID header. Send your own id in that header (up to 128 letters, digits, ., _ or -) to have it reused; otherwise the server generates one. Quote it when reporting a problem with a request.
Workflow¶
| Step | Endpoint | Purpose |
|---|---|---|
| 1 | GET /api/v1/coverage |
Cheap check: does this bbox already have extracted data? |
| 2 | POST /api/v1/runs |
If not, trigger extraction. Returns a run_id immediately. |
| 3 | GET /api/v1/runs/{id} |
Poll until status is completed, no_data, or failed. |
| 4 | GET /api/v1/buildings |
Building + surface attributes, once data exists. |
| 5 | GET /api/v1/geometry |
Footprint geometry, only if something needs to render it. |
buildings and geometry are separate endpoints because a calculation consumer needs no geometry. Splitting them keeps geometry out of responses that will not use it.
Health check¶
Returns {"status": "ok"} if the process is up. It is not authoritative for any one country, because it does not open a database connection.
Checking coverage¶
curl "http://localhost:5000/api/v1/coverage?country=germany&xmin=8.79&ymin=53.14&xmax=8.82&ymax=53.16"
Returns {"count": N}, the number of already PyLovo-linked buildings in that bbox. count: 0 means trigger a run before reading buildings.
sequenceDiagram
participant C as Caller
participant S as City2TABULA API
participant DB as Country database
C->>S: GET /api/v1/coverage?country&bbox
S->>DB: count building_link rows intersecting bbox
DB-->>S: count
S-->>C: 200 {count}
Triggering a run¶
curl -X POST http://localhost:5000/api/v1/runs \
-H 'Content-Type: application/json' \
-d '{"country":"germany","xmin":8.79,"ymin":53.14,"xmax":8.82,"ymax":53.16}'
The pipeline (import, feature extraction, PyLovo linking) runs in the background; the request returns immediately with a run_id to poll.
sequenceDiagram
participant C as Caller
participant S as City2TABULA API
participant P as Pipeline (import, extract, link)
participant DB as Country database
C->>S: POST /api/v1/runs {country, bbox, bbox_mode}
S->>S: validate request, create Run (status: pending)
S-->>C: 202 Accepted {run_id, status: pending}
S->>P: executeRun(run_id) in a background goroutine
P->>DB: import 3D data scoped to bbox (create or incremental)
P->>DB: extract features (scripts 01-08)
P->>DB: link PyLovo buildings (IoU join into building_link)
P->>DB: count building_link rows in bbox
alt buildings found
P->>S: set Run status: completed
else no source data in bbox
P->>S: set Run status: no_data
else pipeline error
P->>S: set Run status: failed, error
end
Polling a run¶
{
"run_id": "0b003631-88c4-4907-a89e-3db1466eb47f",
"country": "germany",
"status": "running",
"stage": "extract_features",
"stage_label": "Extracting building features",
"stage_index": 2,
"stage_count": 3,
"created_at": "2026-10-15T12:00:00Z",
"started_at": "2026-10-15T12:00:01Z",
"updated_at": "2026-10-15T12:00:04Z"
}
no_data means the pipeline ran successfully but found no source data for that bbox, which is not an error. failed means a real error; check error in the response.
A pending run is waiting for another run over an overlapping area to finish. It has no stage fields and no started_at. Once running, stage names the step the run is in:
stage |
stage_index |
stage_label |
|---|---|---|
import |
1 | Importing 3D building data |
extract_features |
2 | Extracting building features |
link_buildings |
3 | Linking to grid buildings |
A failed run keeps the stage it failed in; a completed or no_data run ends on link_buildings. The first run in a country also creates its database during import, so that step takes longer than on later runs.
sequenceDiagram
participant C as Caller
participant S as City2TABULA API
loop until status is completed, no_data, or failed
C->>S: GET /api/v1/runs/{id}
S-->>C: 200 {run_id, status, stage, error}
Note over C: wait, then poll again
end
Reading buildings¶
By bbox, independent of any PyLovo link:
curl "http://localhost:5000/api/v1/buildings?country=germany&xmin=8.79&ymin=53.14&xmax=8.82&ymax=53.16"
By PyLovo OSM id, once a link exists:
By City2TABULA object id, linked or not:
curl "http://localhost:5000/api/v1/buildings?country=germany&object_ids=DEBY_LOD2_7595822,DEBY_LOD2_7672149"
The response is {"buildings": [...], "attributions": [...]}. Each building's surfaces array carries per-face area, azimuth, tilt, horizontal extent, slope extent and height, and its dataset_id names the source dataset. The Surface schema in openapi.yaml defines how each surface value is measured, with references. attributions holds the credit of every dataset in buildings, plus TABULA's when any building has a tabula_variant_code.
Show the credits
The source datasets' licences require their credit wherever data derived from them is shown or passed on. Display each entry's credit (linked to credit_url when present) with the data, and state changes.
Tilt convention
tilt here is 0=vertical wall, 90=flat roof, the opposite of the common building-energy convention (0=horizontal roof, 90=vertical wall). Invert before feeding it to a consumer that expects that convention.
sequenceDiagram
participant C as Caller
participant S as City2TABULA API
participant DB as Country database
alt osm_ids given (PyLovo-linked buildings)
C->>S: GET /api/v1/buildings?country&osm_ids=a,b,c
S->>DB: join building_link + lod2_building on osm_id
else bbox given (every building, linked or not)
C->>S: GET /api/v1/buildings?country&xmin&ymin&xmax&ymax
S->>DB: select lod2_building intersecting bbox
end
DB-->>S: building rows
S->>DB: batch-fetch surfaces for the returned object_ids
DB-->>S: surface rows
S->>DB: credits for the datasets returned
DB-->>S: dataset_attribution rows
S-->>C: 200 {buildings: [{object_id, dataset_id, osm_id, ..., surfaces}], attributions: [...]}
When several modes are present in the query string, osm_ids wins, then object_ids, then the bbox.
Reading geometry¶
Add srid=4326 for WGS84 longitude and latitude instead of the dataset's storage CRS:
sequenceDiagram
participant C as Caller
participant S as City2TABULA API
participant DB as Country database
C->>S: GET /api/v1/geometry?country&object_ids=a,b,c
S->>DB: select footprint geometry for object_ids
DB-->>S: geometry rows (GeoJSON)
S->>DB: credits for the datasets returned
DB-->>S: dataset_attribution rows
S-->>C: 200 {buildings: [{object_id, dataset_id, footprint_geojson}], attributions: [...]}