52 KiB
API Contracts v1
This document freezes the first API shape. Codex may add implementation details but must not rename these routes without updating this file and the frontend API client.
API principles
- Base path:
/api/v1. - JSON by default.
- GeoJSON accepted for geometries where possible.
- Long processing tasks return a job or analysis run record instead of blocking.
- Error responses use the shared
ApiErrorschema.
Shared schemas
ApiError
{
"error": "string",
"message": "human readable message",
"details": {},
"request_id": "optional string"
}
GeoJsonGeometry
Any valid GeoJSON geometry object. V1 primarily expects Polygon and MultiPolygon for areas.
BoundingBox
{
"min_x": 0.0,
"min_y": 0.0,
"max_x": 0.0,
"max_y": 0.0,
"crs": "EPSG:4326"
}
Health
GET /health
Returns service status.
{
"status": "ok",
"service": "geointel-backend",
"version": "0.1.0"
}
GET /api/v1/system/capabilities
Returns enabled feature flags and tool availability.
{
"postgis": true,
"rasterio": true,
"geopandas": true,
"yolo": false,
"sam": false,
"grb": "planned",
"sentinel": "planned",
"providers": [
{
"provider_name": "grb",
"display_name": "GRB",
"authority_level": "authoritative",
"supported_layers": ["buildings", "roads", "parcels"],
"supported_geometry_types": ["Polygon", "MultiPolygon", "LineString", "MultiLineString"],
"supported_query_modes": ["area"],
"fetch_signature": "POST /api/v1/external/grb/fetch",
"configured": false,
"status": "not_configured",
"limitation_message": "GRB live WFS/download integration is not configured in Sprint 7B.",
"attribution": "Digitaal Vlaanderen - Basiskaart Vlaanderen (GRB)",
"license_note": "Use must follow Digitaal Vlaanderen open data and attribution terms.",
"not_configured_reason": "Provider integration is not configured yet"
}
]
}
Projects
GET /api/v1/projects
Returns all projects.
POST /api/v1/projects
Request:
{
"name": "Geel building detection demo",
"description": "Detect buildings and validate against GRB",
"region": "Kempen"
}
Response: ProjectRead.
GET /api/v1/projects/{project_id}
Returns one project with summary counts.
PATCH /api/v1/projects/{project_id}
Updates name/description/region.
DELETE /api/v1/projects/{project_id}
Soft-delete in V1 preferred. Hard-delete only if storage cleanup is also implemented.
Areas
GET /api/v1/projects/{project_id}/areas
Returns areas for a project. Area responses include persisted AOI geometry as GeoJSON so the frontend can display the selected area in the map workbench.
{
"id": "uuid",
"project_id": "uuid",
"name": "Geel Centrum AOI",
"original_crs": "EPSG:4326",
"area_m2": 1234.5,
"created_at": "timestamp",
"geometry_type": null,
"geometry": {
"type": "MultiPolygon",
"coordinates": []
}
}
POST /api/v1/projects/{project_id}/areas
Request:
{
"name": "Geel Centrum AOI",
"geometry": {"type": "Polygon", "coordinates": []},
"crs": "EPSG:4326"
}
Backend responsibilities:
- Validate geometry.
- Repair trivial polygon issues if safe.
- Store geometry in PostGIS.
- Calculate area in square meters using projected CRS.
- Store bbox.
GET /api/v1/projects/{project_id}/areas/{area_id}
Returns one project area. The payload uses the same AreaRead shape as the
area list endpoint and includes persisted GeoJSON geometry for map display.
PATCH /api/v1/projects/{project_id}/areas/{area_id}
Updates the area name and/or geometry. Geometry updates follow the same validation, repair and metric-calculation rules as area creation.
Datasets
POST /api/v1/projects/{project_id}/datasets/upload
Multipart upload.
Fields:
file: dataset file.dataset_type:vector,geojson(legacy),raster.source: free text, e.g.user_upload,grb,osm.dataset_role:source,derived, orreference(defaultsource).source_name: optional source identity, e.g.manual,grb,osm; reference uploads default tomanualwhen omitted.reference_layer_name: optional reference layer label, e.g.buildings; only retained for reference datasets.area_id: optional.
Response: DatasetRead with extracted metadata if supported.
Vector uploads remain stored as original files and are also persisted into vector_features as queryable PostGIS state.
GET /api/v1/projects/{project_id}/datasets/orthophoto/products
Return the governed Digitaal Vlaanderen orthophoto product allowlist in the canonical envelope. Every product reports its key, display/observation label, temporal granularity, native resolution, colour mode, catalogue URL, limitations and whether current configured-YOLO detection is allowed.
POST /api/v1/projects/{project_id}/datasets/orthophoto/acquire
Explicitly acquire a bounded orthophoto selection from a governed official Digitaal Vlaanderen WMS product. Arbitrary WMS URLs and layer names are not accepted.
{
"bbox": {"min_x": 5.10, "min_y": 51.17, "max_x": 5.11, "max_y": 51.18, "crs": "EPSG:4326"},
"area_id": "optional-project-area-uuid",
"product_key": "most_recent",
"force_refresh": false
}
The canonical envelope contains a synchronous Job. Its output_dataset_id
identifies the raster Dataset; result_json contains provider, layer, pixel
dimensions, EPSG:4326/EPSG:31370 bounds, sampling resolution, attribution,
cache reuse and limitation text. Historical products also persist their
observation/validity period and a spatially scoped temporal-series key.
Safety contract:
- every side must measure between 128 m and 1,024 m in EPSG:31370;
- an optional
area_idmust belong to the project and cover at least 99% of the rectangle; - defaults are 1 m/pixel, a 32 MiB response limit and 24-hour exact-request reuse;
- WMS bytes are georeferenced to EPSG:31370 and persisted only through
DatasetService; no fetch runs on startup; - only
most_recentcan enter the current configured-YOLO plus GRB-QA path; historical products are visual evidence and are never validated against the current GRB state; - product periods such as
1979_1990remain explicitly multi-year and are not presented as exact annual observations.
GET /api/v1/projects/{project_id}/datasets
List datasets.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}
Return metadata.
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/metadata/refresh
Re-extract metadata.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/inspect
Return a wrapped vector inspection payload with metadata, storage summary and feature summary.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/summary
Return vector summary data only.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/metadata
Return raster metadata profile for supported raster uploads.
If raster processing is unavailable:
code: RASTER_PROCESSING_UNAVAILABLE
message: Raster processing unavailable. Install rasterio and GDAL-compatible drivers to enable raster metadata extraction.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/inspect
Return raster inspect wrapper payload.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/stats
Return raster band statistics payload.
If raster processing dependencies are unavailable:
- code:
RASTER_PROCESSING_UNAVAILABLE - message: dependency-specific unavailable message.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/preview
Preview readiness for raster layers.
If preview dependencies are unavailable:
- code:
RASTER_PROCESSING_UNAVAILABLE - message:
Raster preview unavailable...
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/image
Return a persisted orthophoto Dataset as a bounded browser-safe PNG. This is an explicit binary non-envelope endpoint used by the MapLibre image source. It accepts only ready datasets from the governed orthophoto provider and never reads arbitrary filesystem paths.
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/clip
Clip raster by selected area. Returns a 202-style accepted job payload through the job wrapper (jobs create/read flow).
If raster processing dependencies are unavailable:
- code:
RASTER_PROCESSING_UNAVAILABLE - message:
Raster processing unavailable. Install rasterio and GDAL-compatible drivers to enable raster processing operations.
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/reproject
Reproject raster dataset to another CRS.
Input:
target_crs(default:EPSG:31370)resampling(nearest,bilinear,cubic; defaultnearest)output_name
Returns a job payload with derived dataset id in result.output_dataset_id.
Failure modes:
- code:
INVALID_PARAMETERSfor bad CRS or resampling - code:
INVALID_DATASET_CRSwhen source raster CRS is missing - code:
RASTER_PROCESSING_UNAVAILABLEwhen rasterio is unavailable
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/indices/ndvi
Compute NDVI from raster band pairs.
Input:
nir_band(positive integer, 1-based)red_band(positive integer, 1-based)output_name(optional)
Returns a job payload with derived dataset id in result.output_dataset_id.
Failure modes:
- code:
INVALID_PARAMETERSfor non-positive/non-integer band indices - code:
INVALID_PARAMETERSfor band index outside source band count - code:
INVALID_DATASET_TYPEwhen source is not raster - code:
RASTER_PROCESSING_UNAVAILABLEwhen rasterio or numpy is unavailable
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/indices/ndwi
Compute NDWI from raster band pairs.
Input:
nir_band(positive integer, 1-based)green_band(positive integer, 1-based)output_name(optional)
Returns a job payload with derived dataset id in result.output_dataset_id.
Failure modes:
- code:
INVALID_PARAMETERSfor non-positive/non-integer band indices - code:
INVALID_PARAMETERSfor band index outside source band count - code:
INVALID_DATASET_TYPEwhen source is not raster - code:
RASTER_PROCESSING_UNAVAILABLEwhen rasterio or numpy is unavailable
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/indices/ndbi
Compute NDBI from raster band pairs.
Input:
nir_band(positive integer, 1-based)swir_band(positive integer, 1-based)output_name(optional)
Returns a job payload with derived dataset id in result.output_dataset_id.
Failure modes:
- code:
INVALID_PARAMETERSfor non-positive/non-integer band indices - code:
INVALID_PARAMETERSfor band index outside source band count - code:
INVALID_DATASET_TYPEwhen source is not raster - code:
RASTER_PROCESSING_UNAVAILABLEwhen rasterio or numpy is unavailable
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/tile
Generate raster tiles and a manifest for downstream processing. Returns a job payload with tile_set_id and manifest metadata.
If raster processing dependencies are unavailable:
- code:
RASTER_PROCESSING_UNAVAILABLE - message:
Raster processing unavailable. Install rasterio and GDAL-compatible drivers to enable raster processing operations.
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/clip
Clip vector dataset to selected area.
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/buffer
Apply buffer distance to vector features.
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/intersect
Intersect source vector dataset with another vector dataset.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/stats
Return vector stats (feature counts and geometry summary).
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/bbox
Return vector bounds and feature count.
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select
Read-only spatial selection over persisted vector_features.
Request:
{
"bbox": {
"min_x": 5.0,
"min_y": 51.0,
"max_x": 5.1,
"max_y": 51.1,
"crs": "EPSG:4326"
},
"area_id": "optional persisted area UUID",
"limit": 250
}
Response:
{
"data": {
"selection_bbox": {
"min_x": 5.0,
"min_y": 51.0,
"max_x": 5.1,
"max_y": 51.1,
"crs": "EPSG:4326"
},
"selection_area_id": "present when area_id was requested",
"feature_count": 2,
"total_feature_count": 2,
"limit": 250,
"truncated": false,
"geojson": {
"type": "FeatureCollection",
"features": []
},
"summary": {
"metric_label": "Wateroppervlakte",
"metric_value": 5.25,
"metric_unit": "ha",
"aggregation_method": "intersection_area",
"primary_metric_key": "water_area",
"feature_count": 23,
"is_estimate": false,
"warning": "Watervolume is niet berekenbaar zonder betrouwbare diepte- of bathymetrische gegevens.",
"metrics": [
{
"metric_key": "water_area",
"metric_label": "Wateroppervlakte",
"metric_value": 5.25,
"metric_unit": "ha",
"aggregation_method": "intersection_area",
"is_estimate": false
},
{
"metric_key": "watercourse_length",
"metric_label": "Lengte waterlopen",
"metric_value": 12.75,
"metric_unit": "km",
"aggregation_method": "intersection_length",
"is_estimate": false
},
{
"metric_key": "feature_count",
"metric_label": "Waterobjecten",
"metric_value": 23,
"metric_unit": "objecten",
"aggregation_method": "feature_count",
"is_estimate": false
}
]
}
}
}
Rules:
- Only vector/GeoJSON datasets are supported.
- Coordinates are EPSG:4326 longitude/latitude.
area_idis optional and must belong to the route project. When present, the bbox remains the bounded preview extent but PostGIS filtering and configured aggregations use the persisted Area geometry exactly. This prevents a municipal or regional full-work-area query from counting objects in the surrounding bbox corners.- Results are generated from persisted PostGIS
vector_features, not from client-side map data. feature_countis the number of GeoJSON features returned in the bounded preview.total_feature_countis the exact number of persisted rows intersecting the requested bbox or persisted Area geometry.summarykeeps one backwards-compatible primary metric and exposes all relevant measurements inmetrics. Known themes use metric PostGIS calculations: building/forest/water/parcel surfaces in hectares, road and watercourse lengths in kilometres, population in inhabitants and intersecting feature counts as supporting evidence.- Area and length calculations transform geometry to Belgian Lambert 72 (
EPSG:31370); they are never calculated in geographic degrees. - Water volume is not inferred from 2D GRB geometry. It remains unavailable until a source provides reliable depth or bathymetry with compatible spatial coverage and provenance.
- The response is capped by
limitand returnstruncated=truewhentotal_feature_countexceeds the returned preview. limitis bounded to1..1000. Municipality-scale clients must page spatially by viewport instead of requesting an unbounded municipality FeatureCollection.- The Map workspace uses this existing endpoint for vector datasets above 5,000 features. It starts delivery at zoom level 14, debounces
moveendrequests and explicitly reportstruncated=trueas a request to zoom further in. This is a client delivery policy, not a second API or persistence path.
POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select/derive
Persists a bbox selection as a new derived vector dataset and indexes the
selected output into vector_features.
Request:
{
"bbox": {
"min_x": 5.0,
"min_y": 51.0,
"max_x": 5.1,
"max_y": 51.1,
"crs": "EPSG:4326"
},
"limit": 250,
"output_name": "selected-buildings"
}
Response: DatasetRead in the canonical API envelope.
Rules:
- Only vector/GeoJSON datasets are supported.
- Coordinates are EPSG:4326 longitude/latitude.
- The new dataset uses
dataset_role="derived",source="operation:selection",source_name="map_selection"andderived_from_dataset_idpointing to the source dataset. - The persisted GeoJSON properties retain source provenance as
source_dataset_idandsource_vector_feature_id. - Empty selections return
VECTOR_OPERATION_EMPTY_RESULTand do not create a dataset.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/content
Returns stored vector dataset content through the canonical API envelope. Vector content is returned as GeoJSON/JSON payload data. Raster content is not served through this endpoint.
Jobs
POST /api/v1/projects/{project_id}/jobs
Create a job.
GET /api/v1/projects/{project_id}/jobs
List jobs.
GET /api/v1/projects/{project_id}/jobs/{job_id}
Read job detail.
GET /api/v1/projects/{project_id}/jobs/{job_id}/status
Read simplified job status payload.
Provider registry
GET /api/v1/external/providers
Returns all configured provider capability descriptors.
GET /api/v1/external/providers/capabilities
Compatibility alias for listing provider capability descriptors.
GET /api/v1/external/providers/{provider_name}
Returns one provider capability descriptor.
GET /api/v1/external/providers/{provider_name}/layers
Returns the supported provider layers.
GET /api/v1/external/providers/{provider_name}/status
Returns configured/status/limitation fields.
POST /api/v1/external/providers/{provider_name}/import
Defines the future provider import contract. Sprint 7B does not perform live imports or write datasets.
Request:
{
"project_id": "uuid-or-local-id",
"area_id": "optional uuid-or-local-id",
"layers": ["buildings"],
"dataset_role": "optional source|reference"
}
GRB/OSM response:
{
"provider_name": "grb",
"status": "not_configured",
"message": "No live GRB import is configured in Sprint 7B.",
"requested_layers": ["buildings"],
"dataset_id": null,
"dataset_role": "reference",
"source_name": "grb"
}
Manual and fixture providers point callers to existing upload/fixture flows. No provider writes directly to vector_features; all future provider output must flow through DatasetService and VectorFeatureService.
External data fetchers
POST /api/v1/external/osm/fetch
Request:
{
"project_id": "uuid",
"area_id": "uuid",
"layers": ["buildings", "roads", "water", "green"]
}
POST /api/v1/external/grb/fetch
Request:
{
"project_id": "uuid",
"area_id": "uuid",
"layers": ["buildings"]
}
V1 may initially implement this as a service interface with a clear not_configured response until the exact WFS endpoint is wired.
Sprint 7B provider contract responses expose capabilities only. Providers must report:
{
"provider_name": "osm",
"display_name": "OpenStreetMap",
"authority_level": "contextual",
"supported_layers": ["buildings", "roads", "water", "landuse"],
"supported_geometry_types": ["Polygon", "MultiPolygon", "LineString", "MultiLineString"],
"supported_query_modes": ["area"],
"configured": false,
"status": "not_configured",
"limitation_message": "OSM live Overpass/download integration is not configured in Sprint 7B.",
"attribution": "OpenStreetMap contributors",
"license_note": "OpenStreetMap data is available under ODbL; attribution is required."
}
No GRB WFS, OSM Overpass or provider downloads are implemented in Sprint 7B.
Demo workflow
POST /api/v1/demo/workflow
Seeds an explicit offline demo workflow from local fixture files. This endpoint does not fetch live GRB/OSM data and does not run AI inference. It creates or returns:
- one demo project
- one demo AOI
- one fixture reference building dataset
- one fixture candidate/predicted building dataset
- one persisted QA/QC result with metric rows
The endpoint is idempotent for the named demo project.
Response:
{
"project_id": "uuid",
"area_id": "uuid",
"reference_dataset_id": "uuid",
"candidate_dataset_id": "uuid",
"quality_check_id": "uuid",
"metric_count": 6,
"status": "ready",
"message": "Demo workflow seeded from explicit local fixtures.",
"created": true
}
Analysis
Detection Lab
Sprint 8 implements Detection Lab foundation only. YOLO/PyTorch real inference is not enabled, no model is downloaded, and fixture detections require explicit fixture mode.
Guided browser orchestration
The current frontend offers one guided building-analysis action, but does not add a parallel backend workflow endpoint. It deliberately composes the canonical contracts in this order:
- optional explicit
POST /api/v1/projects/{project_id}/datasets/uploadfor a georeferenced GeoTIFF; POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/tilewith 512 px tiles and 64 px overlap;GET /api/v1/detection/yolo/preflightwith the returned manifest and selected local model asset;POST /api/v1/detection/runonly after successful preflight;- persisted run, Detection list and Detection GeoJSON reads;
- optional persisted reference QA through the existing detection QA endpoint.
The strict POST /api/v1/detection/run contract still requires tile_manifest_path for configured YOLO. The frontend does not create fake tiles, bypass tile limits, fetch external imagery or download model weights.
GET /api/v1/detection/models
Returns object-detection model capability descriptors.
{
"models": [
{
"model_id": "yolo-placeholder",
"display_name": "YOLO detector placeholder",
"framework": "ultralytics/pytorch",
"task_type": "object_detection",
"supported_classes": ["building", "road", "water", "landuse"],
"configured": false,
"status": "not_configured",
"limitation_message": "YOLO/PyTorch inference is not configured in Sprint 8; no model is downloaded or executed.",
"version": null
},
{
"model_id": "yolo-configured",
"display_name": "Configured YOLO detector",
"framework": "ultralytics/pytorch",
"task_type": "object_detection",
"supported_classes": ["building", "road", "water", "landuse"],
"configured": false,
"status": "not_configured",
"limitation_message": "YOLO is disabled. Set YOLO_ENABLED=true and YOLO_MODEL_PATH to a local model file to enable inference.",
"version": null
}
]
}
GET /api/v1/detection/model-assets
Returns local runtime model files discovered in the configured model directory. This is a read-only catalog. GeoIntel never downloads, creates, mutates or deletes model weights from this endpoint.
The backend scans YOLO_MODELS_DIR (default /app/models) and reports
supported local model files such as .pt, .onnx and .engine. The active
model is the file matching YOLO_MODEL_PATH.
Response data:
{
"items": [
{
"model_asset_id": "building-detector-pt",
"filename": "building-detector.pt",
"display_name": "building-detector",
"model_path": "/app/models/building-detector.pt",
"suffix": ".pt",
"framework": "ultralytics/pytorch",
"task_type": "object_detection",
"size_bytes": 123456,
"sha256": "sha256hex",
"active": true,
"status": "available",
"limitation_message": "Local runtime model asset. GeoIntel will not download or mutate model weights.",
"will_download_models": false
}
],
"total": 1,
"model_directory": "/app/models"
}
GET /api/v1/detection/yolo/preflight
Returns a canonical envelope with read-only configured-YOLO runtime preflight state. Optional query parameters:
tile_manifest_path: existing raster tile manifest path to validate.model_asset_id: optional local model asset ID fromGET /api/v1/detection/model-assets; when supplied, preflight validates that asset path instead of the defaultYOLO_MODEL_PATH.check_model_load: defaultfalse; whentrue, explicitly loads only the configured local model file for compatibility smoke. It never downloads weights and never runs inference.
Response data:
{
"model_id": "yolo-configured",
"model_asset_id": null,
"model_path": null,
"tile_manifest_path": null,
"status": "not_configured",
"message": "YOLO_MODEL_PATH is not set. GeoIntel will not download model weights automatically.",
"checks": {
"enabled": true,
"dependencies_available": true,
"model_path_set": false,
"model_file_exists": null,
"model_load_requested": false,
"model_load_ok": null,
"manifest_path_set": null,
"manifest_valid": null,
"tile_paths_exist": null,
"tile_limit_ok": null
},
"runtime": {
"dependencies_assumed": false,
"model_directory": null,
"yolo_config_dir": "/app/storage/ultralytics",
"torch_version": "2.12.1",
"ultralytics_version": "8.4.88",
"cuda_available": false
},
"tile_count": 0,
"max_tiles": 100,
"will_download_models": false,
"will_run_inference": false
}
POST /api/v1/detection/run
Creates a detection job and detection analysis run. If the requested model is unavailable, the job and analysis run are marked failed with DETECTION_MODEL_UNAVAILABLE or DETECTION_DEPENDENCY_UNAVAILABLE.
Request:
{
"project_id": "uuid",
"dataset_id": "uuid",
"model_id": "yolo-placeholder",
"model_asset_id": null,
"confidence_threshold": 0.5,
"class_filter": ["building"],
"tile_manifest_path": null,
"parameters_json": {}
}
Sprint 8B configured YOLO mode uses model_id: "yolo-configured". It requires:
YOLO_ENABLED=trueYOLO_MODEL_PATHpointing to an existing local model file- backend optional AI dependencies installed with
geointel-backend[ai] tile_manifest_pathpointing to an existing raster tile manifest generated by the raster tile operation
model_asset_id may be supplied with model_id: "yolo-configured" to select a
specific local model file from the read-only model asset catalog. The backend
resolves the ID to a file inside the configured model directory and persists the
asset ID, path and SHA-256 in the job and analysis-run parameters for
reproducibility. Clients must not submit arbitrary model paths.
GeoIntel does not download model weights automatically. Configured YOLO runs read existing tile files from the manifest, convert YOLO pixel-space boxes to EPSG:4326 detection polygons and persist detections as first-class records.
Unavailable model response:
{
"analysis_run_id": "uuid",
"job_id": "uuid",
"project_id": "uuid",
"dataset_id": "uuid",
"model_id": "yolo-placeholder",
"status": "failed",
"detection_count": 0,
"error_code": "DETECTION_MODEL_UNAVAILABLE",
"message": "YOLO/PyTorch inference is not configured in Sprint 8; no model is downloaded or executed."
}
Validation errors:
INVALID_DATASET_TYPEwhen the dataset is not raster.DETECTION_MODEL_NOT_FOUNDwhen the model id is unknown.DETECTION_MODEL_ASSET_NOT_FOUNDwhenmodel_asset_idis not present in the configured model directory.FIXTURE_MODE_REQUIREDwhenmanual-fixture-detectoris requested withoutparameters_json.fixture_mode=true.DETECTION_TILE_MANIFEST_REQUIREDwhenyolo-configuredis requested withouttile_manifest_path.DETECTION_TILE_MANIFEST_NOT_FOUNDwhen the provided manifest path does not exist.DETECTION_TILE_MANIFEST_INVALIDwhen the manifest cannot be parsed or lacks tile metadata.DETECTION_TILE_LIMIT_EXCEEDEDwhen the manifest exceedsYOLO_MAX_TILES.- Configured YOLO inference forwards
YOLO_MAX_DETECTIONSto Ultralyticsmax_detand defaults to1000so dense building AOIs are not silently limited by the upstream default of 300 detections before persisted QA/QC. - Configured YOLO applies cross-tile duplicate suppression after pixel boxes are
converted to EPSG:4326 geometries and before
Detectionrows are persisted. Same-class candidates are confidence-sorted and lower-confidence candidates with geometry IoU greater than or equal toYOLO_DUPLICATE_IOU_THRESHOLDare suppressed. The default is0.5;0disables this GeoIntel-side post-processing for debugging. DETECTION_DEPENDENCY_UNAVAILABLEwhen YOLO dependencies are not installed.DETECTION_MODEL_LOAD_FAILEDwhen the local model file exists but cannot be loaded.
Fixture detector mode is test/demo-only. It persists only explicit parameters_json.fixture_detections entries and is never invoked automatically.
GET /api/v1/detection/runs/{analysis_run_id}
Returns one detection analysis run.
GET /api/v1/detection/runs
Returns detection analysis runs, optionally filtered by project_id and dataset_id.
GET /api/v1/detection/runs/{analysis_run_id}/detections
Returns persisted detections for a detection analysis run. Optional filters:
dataset_idclass_namemin_confidence
GET /api/v1/detection/datasets/{dataset_id}/detections
Returns persisted detections for a raster dataset. Optional filters:
analysis_run_idclass_namemin_confidence
GET /api/v1/detection/detections/{detection_id}
Returns one persisted detection.
GET /api/v1/detection/runs/{analysis_run_id}/geojson
Returns persisted detections for a run as a GeoJSON FeatureCollection. Geometry comes from persisted PostGIS detection geometry in EPSG:4326.
Each feature includes:
detection_idclass_nameconfidencemodel_namemodel_versionanalysis_run_iddataset_idjob_idsource_tile_pathbbox_json
GET /api/v1/detection/datasets/{dataset_id}/geojson
Returns persisted detections for a dataset as a GeoJSON FeatureCollection. Optional filters match the detection list endpoint.
POST /api/v1/detection/runs/{analysis_run_id}/qa/reference
Compares persisted detection geometries from an analysis run against persisted vector_features from a reference vector dataset.
Request:
{
"reference_dataset_id": "uuid",
"iou_threshold": 0.5,
"class_name": "building",
"min_confidence": 0.5
}
Response persists a quality_check and metrics rows through the existing QA/QC persistence architecture and returns:
precisionrecallf1_scoremean_ioufalse_positivesfalse_negativesquality_check_id
Configured-YOLO QA automatically reads tile_manifest_path from the persisted
AnalysisRun.parameters_json. Candidate and reference geometries are clipped
to the union of the manifest's tile bounds after explicit CRS transformation to
EPSG:4326. Before reference geometries are materialized, the service applies
that coverage with an indexed PostGIS ST_Intersects predicate. The full
dataset count is retained separately so raw/evaluated/excluded counts remain
auditable without transferring a regional reference dataset to Python. The
response additionally returns:
candidate_feature_count_rawandreference_feature_count_raw;coverage, including raw/evaluated/excluded/boundary-clipped population counts, tile count, source CRS values and coverage mode;box_to_footprint_diagnostics, which compares candidate boxes with reference envelopes at the same IoU threshold.
The canonical precision, recall, F1 and mean IoU always remain based on
candidate geometry versus the persisted reference footprint. Envelope results
are explicitly diagnostic_only and are persisted in
quality_checks.findings_json; they never replace or inflate canonical metrics.
Configured-YOLO QA fails closed with DETECTION_QA_COVERAGE_UNAVAILABLE when
manifest provenance is absent, DETECTION_QA_COVERAGE_MISMATCH when it belongs
to another raster, DETECTION_QA_COVERAGE_INVALID when bounds/CRS are invalid,
or REFERENCE_FEATURES_OUTSIDE_COVERAGE when no reference polygons overlap the
actual inference coverage. Explicit fixture/legacy runs without a manifest keep
the documented unbounded comparison behavior.
If the reference dataset has no persisted vector features, the endpoint returns REFERENCE_FEATURES_NOT_FOUND. It does not calculate fake QA metrics.
Future analysis route: /api/v1/analysis/building-stats
Not implemented in the active API surface. Future input is expected to combine an area with a vector building layer.
Future analysis route: /api/v1/analysis/object-detection
Request:
{
"project_id": "uuid",
"area_id": "uuid",
"dataset_id": "uuid",
"model_id": "optional uuid",
"classes": ["building"],
"confidence_threshold": 0.35,
"tile_size": 640,
"overlap": 64
}
Response: AnalysisRunRead.
Future analysis route: /api/v1/analysis/segmentation
Same pattern as object detection, but output includes masks and polygonized geometries.
Segmentation Lab
Sprint 9 implements Segmentation Lab foundation only. Real SAM and YOLO-seg inference are not enabled, no model is downloaded, and fixture segmentations require explicit fixture mode.
GET /api/v1/segmentation/models
Returns segmentation model capability descriptors:
segmentation-placeholder:not_configuredfixture-segmenter: configured for explicit test/demo fixtures onlyyolo-seg-configured:not_configuredsam-configured:not_configured
POST /api/v1/segmentation/run
Creates a segmentation job and segmentation analysis run. If the requested model is unavailable, the job and analysis run are marked failed with SEGMENTATION_MODEL_UNAVAILABLE.
Request:
{
"project_id": "uuid",
"dataset_id": "uuid",
"model_id": "segmentation-placeholder",
"confidence_threshold": 0.5,
"class_filter": ["vegetation"],
"tile_manifest_path": null,
"parameters_json": {}
}
Fixture segmenter mode is test/demo-only. It persists only explicit parameters_json.fixture_segmentations entries when parameters_json.fixture_mode=true; it is never invoked automatically and does not represent production inference.
Validation errors:
INVALID_DATASET_TYPEwhen the dataset is not raster.SEGMENTATION_MODEL_NOT_FOUNDwhen the model id is unknown.FIXTURE_MODE_REQUIREDwhenfixture-segmenteris requested withoutparameters_json.fixture_mode=true.INVALID_FIXTURE_SEGMENTATIONSwhen fixture payloads are not a list.INVALID_FIXTURE_GEOMETRYwhen fixture geometry is empty, invalid or not Polygon/MultiPolygon.
GET /api/v1/segmentation/runs
Returns segmentation analysis runs, optionally filtered by project_id and dataset_id.
GET /api/v1/segmentation/runs/{analysis_run_id}
Returns one segmentation analysis run.
GET /api/v1/segmentation/runs/{analysis_run_id}/segmentations
Returns persisted segmentation records for a segmentation analysis run. Optional filters:
dataset_idclass_namemin_confidence
GET /api/v1/segmentation/datasets/{dataset_id}/segmentations
Returns persisted segmentation records for a raster dataset. Optional filters:
analysis_run_idclass_namemin_confidence
GET /api/v1/segmentation/segmentations/{segmentation_id}
Returns one persisted segmentation record.
GET /api/v1/segmentation/runs/{analysis_run_id}/geojson
Returns persisted segmentations for a run as a GeoJSON FeatureCollection. Geometry comes from persisted PostGIS segmentation geometry in EPSG:4326.
Each feature includes:
segmentation_idclass_nameconfidencearea_m2model_namemodel_versionanalysis_run_iddataset_idjob_idsource_tile_pathtile_indexmask_pathbbox_jsonprovenance_json
GET /api/v1/segmentation/datasets/{dataset_id}/geojson
Returns persisted segmentations for a dataset as a GeoJSON FeatureCollection. Optional filters match the segmentation list endpoint.
POST /api/v1/segmentation/runs/{analysis_run_id}/qa/reference
Compares persisted segmentation geometries from an analysis run against persisted vector_features from a reference vector dataset.
Request:
{
"reference_dataset_id": "uuid",
"iou_threshold": 0.5,
"class_name": "vegetation",
"min_confidence": 0.5
}
Response persists a quality_check and metrics rows through the existing QA/QC persistence architecture and returns precision, recall, F1, mean IoU and false positive/negative counts.
If the segmentation run has no persisted geometries, the endpoint returns SEGMENTATIONS_NOT_FOUND. If the reference dataset has no persisted vector features, it returns REFERENCE_FEATURES_NOT_FOUND. It does not calculate fake QA metrics.
POST /api/v1/analysis/change-detection
Compares two persisted vector datasets in the same project and returns a synchronous job envelope. This is a lightweight V1 foundation for added/removed object review, not a temporal run-history engine.
Request:
{
"source_dataset_id": "uuid",
"target_dataset_id": "uuid",
"iou_threshold": 0.8,
"include_unchanged": true
}
Response is a canonical API envelope containing a JobRead payload. On success,
result_json contains:
{
"source_dataset_id": "uuid",
"target_dataset_id": "uuid",
"source_feature_count": 2,
"target_feature_count": 2,
"added_count": 1,
"removed_count": 1,
"unchanged_count": 1,
"iou_threshold": 0.8,
"warnings": [],
"generated_at": "2026-06-16T00:00:00Z",
"geojson": {
"type": "FeatureCollection",
"features": []
}
}
Change detection prefers persisted vector_features. If an older vector dataset
has no persisted vector rows, it falls back to the stored GeoJSON artifact and
adds a warning to result_json.warnings. Supported comparable geometry types are
Polygon and MultiPolygon; point/line geometries return UNSUPPORTED_GEOMETRY.
GeoJSON feature properties include:
change_type:added,removedorunchangedsource_dataset_idtarget_dataset_idsource_feature_idtarget_feature_idiou
Limitations:
- No live GRB/OSM/Sentinel fetching.
- No fake object lifecycle classification.
- No
changedclassification without durable object ids/versioning. - No first-class change table yet; the current output is stored in job
result_jsonand rendered in the frontend map.
QA/QC
POST /api/v1/qa/detections-vs-reference
Request:
{
"candidate_dataset_id": "uuid",
"reference_dataset_id": "uuid",
"iou_threshold": 0.5,
"area_id": "optional uuid"
}
Response is wrapped in the job envelope. On success, result_json includes precision, recall, F1, mean IoU, false positives, false negatives and quality_check_id.
Sprint 111 also includes feature-level evidence arrays for map/review handoff:
match_evidence: matched candidate/reference feature ids with IoU.false_positive_evidence: unmatched candidate feature ids.false_negative_evidence: unmatched reference feature ids.
These arrays are derived from the same persisted/source geometries used for IoU matching. They are not separate QA records yet; they are persisted inside quality_checks.findings_json.
Sprint 7A persists the QA/QC result as:
jobs: execution state.quality_checks: domain result.metrics: individual measurements.
Future Detection and Segmentation flows may add an analysis_run_id path without replacing persisted quality checks.
GET /api/v1/projects/{project_id}/quality-checks
Lists persisted QA/QC quality checks for a project with metric rows.
Response:
{
"items": [
{
"id": "uuid",
"project_id": "uuid",
"job_id": "uuid-or-null",
"analysis_run_id": "uuid-or-null",
"candidate_dataset_id": "uuid-or-null",
"reference_dataset_id": "uuid",
"check_type": "demo_candidate_vs_reference",
"status": "ok",
"score": 0.5,
"parameters_json": {},
"findings_json": {
"matches": 1,
"false_positives": 1,
"false_negatives": 1,
"match_evidence": [
{
"candidate_feature_id": "candidate-feature-id",
"reference_feature_id": "reference-feature-id",
"iou": 0.83
}
],
"false_positive_evidence": [
{
"candidate_feature_id": "candidate-extra-id"
}
],
"false_negative_evidence": [
{
"reference_feature_id": "reference-missing-id"
}
]
},
"metrics": [
{
"metric_key": "precision",
"metric_value": 0.5
}
]
}
],
"total": 1,
"limit": 50,
"offset": 0
}
GET /api/v1/projects/{project_id}/quality-checks/{quality_check_id}/evidence/geojson
Returns a canonical envelope containing a read-only QA/QC evidence overlay for a
persisted quality check. The endpoint reads feature ids from
quality_checks.findings_json.match_evidence,
false_positive_evidence and false_negative_evidence, resolves them against
persisted candidate/reference geometries and returns a GeoJSON FeatureCollection.
Supported resolution paths:
- dataset QA candidate/reference geometries from
vector_features; - detection QA candidate geometries from persisted
detections; - segmentation QA candidate geometries from persisted
segmentations; - reference geometries from persisted
vector_features.
Response:
{
"data": {
"quality_check_id": "uuid",
"project_id": "uuid",
"candidate_dataset_id": "uuid-or-null",
"reference_dataset_id": "uuid",
"analysis_run_id": "uuid-or-null",
"feature_count": 4,
"warnings": [],
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "match_candidate:feature-id",
"geometry": {},
"properties": {
"qa_evidence_role": "match_candidate",
"quality_check_id": "uuid",
"candidate_feature_id": "candidate-feature-id",
"reference_feature_id": "reference-feature-id",
"iou": 0.83
}
}
]
}
}
}
qa_evidence_role is one of match_candidate, match_reference,
false_positive or false_negative. Missing persisted feature ids are reported
in warnings; no fake geometries are produced.
Detection-backed candidate evidence also exposes provenance read from the
persisted detections row: detection_id, job_id, confidence,
model_name, model_version, source_tile_path and bbox_json. Existing
properties_json fields such as tile_index remain present. Segmentation-backed
candidate evidence exposes the equivalent persisted model/source fields plus
segmentation_id, mask_path and area_m2. These are additive GeoJSON
properties; the canonical envelope and endpoint path are unchanged.
For detection QA, false-positive and false-negative evidence properties also
include review_decision, review_notes, reviewed_by and reviewed_at.
Missing review rows are represented as review_decision=unreviewed. Evidence
resolution is bounded to identifiers stored by the selected quality check.
GET /api/v1/projects/{project_id}/quality-checks/{quality_check_id}/reviews
Returns the paginated operator review queue for a persisted
detections_vs_reference quality check. Optional query parameters are
evidence_role=false_positive|false_negative, decision, reviewed=true|false,
limit (1-200) and offset. Items derive only from persisted QA evidence.
{
"data": {
"items": [{
"id": null,
"project_id": "uuid",
"quality_check_id": "uuid",
"analysis_run_id": "uuid",
"evidence_role": "false_positive",
"evidence_feature_id": "detection-uuid",
"detection_id": "detection-uuid",
"decision": "unreviewed",
"confidence": 0.62,
"class_name": "building"
}],
"total": 1,
"limit": 50,
"offset": 0,
"summary": {
"total": 73,
"reviewed": 0,
"remaining": 73,
"false_positive_total": 17,
"false_negative_total": 56,
"decision_counts": {"unreviewed": 73}
}
}
}
POST /api/v1/projects/{project_id}/quality-checks/{quality_check_id}/reviews
Creates or updates one durable operator decision. The evidence id must belong to the quality check and resolve to the persisted Detection or reference VectorFeature. False-positive and false-negative roles accept only their role-specific decisions.
{
"evidence_role": "false_positive",
"evidence_feature_id": "detection-uuid",
"decision": "qa_alignment_mismatch",
"notes": "The detection box overlaps the irregular GRB footprint.",
"reviewed_by": "operator"
}
Allowed decisions are confirmed_model_false_positive,
confirmed_model_false_negative, reference_gap_or_change,
qa_alignment_mismatch, imagery_obscured_or_uncertain, uncertain and
unreviewed. Invalid role/decision combinations return
INVALID_DETECTION_REVIEW_DECISION.
Exports
POST /api/v1/exports/geojson
Export detections, segmentations or vector layer to GeoJSON.
Dataset vector export request:
{
"export_kind": "dataset",
"dataset_id": "uuid",
"name": "optional-basename"
}
Map vector selection export request:
{
"export_kind": "vector_selection",
"dataset_id": "uuid",
"bbox": {
"min_x": 5.0,
"min_y": 51.0,
"max_x": 5.1,
"max_y": 51.1,
"crs": "EPSG:4326"
},
"limit": 250,
"name": "optional-basename"
}
Detection run export request:
{
"export_kind": "detection_run",
"analysis_run_id": "uuid",
"name": "optional-basename"
}
Segmentation run export request:
{
"export_kind": "segmentation_run",
"analysis_run_id": "uuid",
"name": "optional-basename"
}
Response persists an exports row and writes a deterministic JSON artifact:
{
"export_id": "uuid",
"path": "storage/exports/{project_id}/datasets/{target}/{name}.geojson",
"status": "ready",
"export_type": "dataset_geojson",
"metadata_json": {
"source": "dataset",
"feature_count": 0
}
}
Vector dataset exports use the stored dataset GeoJSON. Detection and
segmentation exports use persisted first-class geometry records and the
existing Detection/Segmentation GeoJSON conversion services. Vector selection
exports query persisted PostGIS vector_features with the supplied EPSG:4326
bbox, write the selected FeatureCollection as a vector_selection_geojson
artifact, and persist bbox/feature-count metadata in the export record. Raster
datasets are rejected for dataset and selection GeoJSON export.
POST /api/v1/exports/metadata
Exports project metadata JSON for projects, datasets, persisted QA/QC summary rows and existing export history.
{
"project_id": "uuid",
"name": "optional-basename"
}
GET /api/v1/exports/projects/{project_id}/exports
Lists persisted export records for a project.
GET /api/v1/exports/{export_id}
Returns one persisted export record.
GET /api/v1/exports/{export_id}/content
Returns the stored JSON artifact content through the standard API envelope.
HTML report artifacts are intentionally download-only through /download.
Calling /content for project_report_html returns:
code: EXPORT_CONTENT_UNSUPPORTED
message: Export content preview is only available for JSON and GeoJSON artifacts. Download HTML report artifacts instead.
GET /api/v1/exports/{export_id}/download
Downloads the stored JSON/GeoJSON/HTML export artifact as a raw file response
with a Content-Disposition attachment filename. JSON and GeoJSON artifacts
use application/json; HTML report artifacts use text/html. This endpoint
intentionally does not use the JSON envelope because it is a browser/file-download
path; callers that need canonical API JSON should use /content for JSON/GeoJSON
artifacts.
Future export route: /api/v1/exports/yolo
Export annotations/detections to YOLO format.
POST /api/v1/exports/report
Creates a lightweight HTML project report artifact from persisted project, dataset, V1 readiness summary, QA/QC summary, known limitations and export history state. This does not create a PDF and does not introduce a report designer.
{
"project_id": "uuid",
"name": "optional-basename"
}
Response persists an exports row with export_type: project_report_html. Download the report through:
GET /api/v1/exports/{export_id}/download
PDF/report-designer functionality can be added after core GeoAI workflows work.
Temporal datasets and area evolution
Temporal metadata describes the source observation, not API run history. A
snapshot is a normal persisted dataset grouped by temporal_series_key and
ordered by observed_at. Optional validity uses valid_from and valid_to;
temporal_granularity is snapshot, day, month, year or period.
Dataset upload accepts those temporal fields plus source_version. When a
dataset declares source_metadata.selection_aggregation, the vector bbox
selection response also contains a summary with metric label/value/unit,
aggregation method, feature count, estimate status and an optional warning.
Supported PostGIS aggregations are feature count, intersection area,
intersection length, numeric sum and area-weighted numeric sum. Area and length
are measured after transformation to EPSG:31370.
PATCH /api/v1/projects/{project_id}/datasets/{dataset_id}/temporal
Updates the temporal provenance of an existing dataset. Series key and observation date are required together. It does not alter features or manufacture a historical observation.
GET /api/v1/projects/{project_id}/datasets/{dataset_id}/versions
Lists immutable storage/provenance versions. Uploads and derived datasets create version 1 in the same persistence transaction.
GET /api/v1/projects/{project_id}/temporal/series
Returns dated project series in the canonical envelope. Each item contains its source/layer identity, first and last observations and ordered datasets.
POST /api/v1/projects/{project_id}/temporal/compare
{
"earlier_dataset_id": "uuid",
"later_dataset_id": "uuid",
"bbox": {"min_x": 5.0, "min_y": 51.0, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326"},
"area_id": "optional persisted Area uuid",
"preview_limit": 500
}
Both datasets must belong to the project and the same temporal series, with
the earlier observation preceding the later one. The response contains source
snapshot references, selection bbox, earlier/later metric values,
absolute/percentage change, estimate status, warnings and GeoJSON evidence.
metric remains the backwards-compatible primary measurement. metrics
contains every aggregation that is compatible between both snapshots and
timeline contains the same persisted metric for every dated snapshot in the
series. When area_id is supplied it must belong to the project and the exact
persisted Area geometry is used; the bbox remains only the bounded map extent.
Added/removed/modified object changes are calculated only when source
provenance declares stable feature identities; otherwise
object_changes.available=false and no object history is inferred.
Local GeoIntel assistant
The assistant is an optional read-only language interface over persisted GeoIntel measurements. The browser never connects to Ollama directly and does not choose an arbitrary provider URL.
GET /api/v1/assistant/status
Returns configured, not_configured or unavailable, the configured default
model and the number of locally installed models. It never downloads a model.
GET /api/v1/assistant/models
Returns the models reported by Ollama GET /api/tags in the canonical
envelope. A chat request can only select a model from this list.
POST /api/v1/projects/{project_id}/assistant/query
{
"question": "Hoe evolueerde de bosoppervlakte?",
"model": "qwen3.5:9b",
"bbox": {"min_x": 5.0, "min_y": 51.0, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326"},
"area_id": "optional persisted Area uuid",
"history": []
}
The backend validates project/Area ownership, calculates current semantic
metrics from PostGIS and includes dated observations only for persisted
temporal series. Geometry is not sent to Ollama. The response contains the
answer, used model, scope label, context metrics, discovered temporal series,
source dataset ids and warnings. Missing measurements remain unavailable;
specifically, no water volume is inferred from 2D water geometry. The backend
sets an explicit Ollama context window and returns
OLLAMA_RESPONSE_TRUNCATED instead of accepting a response with
done_reason=length as a complete answer.