Files
geointel/docs/API_CONTRACTS.md
T
Codex 0fae53a7de
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s
Harden RC7 API response contracts
2026-07-18 05:07:35 +02:00

82 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 ApiError schema.
  • Every successful JSON endpoint has a concrete Pydantic response model and uses the canonical {"data": ...} envelope. Readiness runs an OpenAPI audit that rejects free-form dictionary responses and envelope drift.
  • The only successful non-envelope responses are /health, /health/live, /health/ready, the four documented persisted-raster PNG endpoints and the export artifact download endpoint.

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/live

Returns process liveness only. It never queries PostgreSQL.

{
  "status": "ok",
  "service": "geointel-backend",
  "version": "0.1.0",
  "build_sha": null,
  "build_time": null
}

GET /health

Backward-compatible alias for dependency readiness.

GET /health/ready

Returns dependency readiness. Both readiness routes return HTTP 503 when the database, PostGIS, single migration head or writable storage check is degraded. Docker uses /health/ready.

{
  "status": "ok",
  "service": "geointel-backend",
  "version": "0.1.0",
  "database": "ok",
  "postgis": "ok:3.x",
  "migration": "ok:202607160001",
  "storage": "ok",
  "checks": {
    "database": "ok",
    "postgis": "ok:3.x",
    "migration": "ok:202607160001",
    "storage": "ok"
  }
}

GET /api/v1/system/capabilities

Returns enabled feature flags and tool availability in the canonical data envelope. PostGIS and configured YOLO state are derived at runtime.

{
  "data": {
    "postgis": true,
    "rasterio": true,
    "geopandas": true,
    "yolo": true,
    "yolo_status": "configured",
    "sam": false,
    "grb": "bounded",
    "sentinel": "planned",
    "version": "0.1.0",
    "build_sha": null,
    "providers": []
  }
}

Projects

GET /api/v1/projects

Returns active projects by default with limit/offset pagination. The optional status query accepts active, archived or all; deleted projects are never returned. Optional exact name filtering supports stable lookup of a canonical operational workspace without depending on its position among newer operator or benchmark projects:

GET /api/v1/projects?name=Kempen%20Regional%20Workbench&limit=1
GET /api/v1/projects?status=archived&limit=50

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 or the ordinary lifecycle status. The status can only be active or archived. Archiving keeps datasets, jobs, analyses, quality checks and exports intact while removing the workspace from the default active-project list.

DELETE /api/v1/projects/{project_id}

Marks the project as deleted. This route does not remove storage artifacts or related persistence and is not the ordinary workspace-cleanup path. Operators and the UI use PATCH with status: "archived" for reversible cleanup.

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, or reference (default source).
  • source_name: optional source identity, e.g. manual, grb, osm; reference uploads default to manual when 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.

GET /api/v1/projects/{project_id}/datasets/grb/products

Returns the fixed official GRB vector registry in the canonical envelope. Only buildings (GBG), roads (Wegsegment), water (WTZ/WLAS/WGR) and parcels (ADP) are exposed. Each product reports its exact source collections, supported geometry types, attribution, licence and semantic limitation.

POST /api/v1/projects/{project_id}/datasets/grb/acquire

Acquires one explicitly bounded GRB product from the allowlisted Digitaal Vlaanderen OGC API Features service behind the synchronous Job abstraction:

{
  "bbox": {"min_x": 5.12, "min_y": 51.18, "max_x": 5.17, "max_y": 51.22, "crs": "EPSG:4326"},
  "area_id": "optional-project-area-uuid",
  "product_key": "buildings",
  "force_refresh": false
}

The backend requires EPSG:4326, intersects the rectangle with the optional persisted Area, rejects sides below 10 m or above 20 km, follows only same-host allowlisted collection pagination and fails instead of truncating when page, transfer or feature limits are reached. Every official feature id, request URL, response checksum and artifact checksum remains provenance. Although GRB is stored natively in EPSG:31370, the service explicitly negotiates OGC CRS84 longitude/latitude GeoJSON for both bbox and output before performing the exact geometry intersection.

Output is an ordinary reference Dataset persisted through DatasetService, DatasetVersion and VectorFeatureService; providers never write directly to vector_features. Exact requests are reused for 24 hours unless force_refresh=true. Selection metrics are footprint hectares for buildings, line kilometres for roads, hectares plus supporting line kilometres for water, and hectares for parcels. Water volume is unsupported because GRB contains no depth or bathymetry.

GET /api/v1/projects/{project_id}/datasets/dhmv/products

Returns the fixed official DHMV II product registry in the canonical envelope. The registry contains only dtm_1m (DHMVII_DTM_1m) and dsm_1m (DHMVII_DSM_1m). Each item records native 1 m resolution, EPSG:31370, TAW, the 2013-2015 acquisition period, attribution, catalogue and limitations.

POST /api/v1/projects/{project_id}/datasets/dhmv/acquire

Runs a bounded WCS 2.0.1 GetCoverage request behind the existing synchronous Job abstraction. Arbitrary coverage identifiers are rejected.

{
  "bbox": {"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3, "crs": "EPSG:4326"},
  "area_id": "optional-project-area-uuid",
  "product_key": "dtm_1m",
  "resolution_m": 5.0,
  "force_refresh": false
}

The service extracts the GeoTIFF from the official multipart response, clips to the exact persisted Area when supplied, validates EPSG:31370, one band, resolution, nodata and valid cells, then persists through DatasetService. The default 5 m file is an analysis copy of the retained 1 m source product; both resolutions and all request/response/output checksums remain provenance.

POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/terrain/select

Accepts an EPSG:4326 rectangle and optional Area id. It reads only a governed, ready DHMV Dataset and returns a canonical envelope with valid-cell coverage, mean/min/max/P10/P90 height in m TAW, relief in metres and mean/P90/max slope in degrees. Area geometry is an exact mask, not only a bounding box.

The response always lists water_depth_m and water_volume_m3 under unsupported_metrics. Drainage is not calculated by this endpoint.

POST /api/v1/projects/{project_id}/datasets/raster/terrain/select

Runs the same exact terrain calculation over every persisted municipal DHMV partition intersecting one bounded EPSG:4326 rectangle. The request adds the governed product_key (dtm_1m or dsm_1m) to the ordinary selection bbox and optional Area id. The backend mosaics only the intersecting windows in EPSG:31370, enforces the existing 12-million-cell limit and calculates global cell statistics. The canonical response includes dataset_ids and partition_count; percentiles are calculated from the combined cells and are not averages of municipal summaries.

GET /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/terrain/image

Returns a browser-safe PNG colour relief for the persisted governed DHMV Dataset. This binary MapLibre source never accepts an arbitrary file path.

Safety contract:

  • every side must measure between 128 m and 1,024 m in EPSG:31370;
  • an optional area_id must 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_recent can 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_1990 remain explicitly multi-year and are not presented as exact annual observations.

GET /api/v1/projects/{project_id}/datasets/flood-hazard/products

Returns the fixed twelve-product VMM flood-depth registry in the canonical envelope. Products combine pluviaal/fluviaal, current climate/climate projection 2050 and T10/T100/T1000. Each product keeps the official WCS coverage id, probability class, source unit centimetres, normalized unit metres, publication metadata, attribution and limitation.

POST /api/v1/projects/{project_id}/datasets/flood-hazard/acquire

Acquires one bounded official VMM OGRK WCS 1.1 coverage behind the synchronous Job abstraction. Arbitrary coverage identifiers and service URLs are rejected. When product_key is omitted, the governed default is pluviaal_current_t100; the default is itself present in the fixed product registry.

{
  "bbox": {"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3, "crs": "EPSG:4326"},
  "area_id": "optional-project-area-uuid",
  "product_key": "pluviaal_current_t100",
  "resolution_m": 5.0,
  "force_refresh": false
}

The service tiles municipality-size requests, validates EPSG:31370 and one Float32 depth band, converts positive source centimetres to metres, clips to the exact persisted Area and stores an ordinary raster Dataset and DatasetVersion. Zero/null source cells become transparent nodata. The scenario is not a temporal observation and receives no fabricated observed_at value.

POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/flood-hazard/select

Returns mapped positive-depth area in hectares, share of the selection, mean, P90 and maximum modeled local depth and modelled_max_depth_area_integral_m3. Every result identifies mechanism, climate context, probability class and return period. The integral sums local modeled maximum depth times cell area; it is explicitly not concurrent flood storage, permanent waterbody content, current water level or bathymetry. These unsupported metrics remain listed in the response.

POST /api/v1/projects/{project_id}/datasets/raster/flood-hazard/select

Runs exact bounded analysis over the persisted municipal VMM partitions for one governed product_key. Only partitions intersecting the selection are opened, the normalized metre grids are combined at their common 5 m analysis resolution and the global area/depth metrics are calculated from the combined cells. The canonical response includes every contributing Dataset id in dataset_ids plus partition_count. The existing flood-volume and bathymetry prohibitions are unchanged.

GET /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/flood-hazard/image

Returns a constrained transparent PNG for a persisted governed VMM flood-depth Dataset. It never accepts an arbitrary path or coverage id and is used by the existing MapLibre image-overlay path.

GET /api/v1/projects/{project_id}/datasets/thematic-raster/products

Returns the fixed MercatorNet registry for space_occupation_2025, open_space_2022, population_density_2019, node_value_2022 and service_level_2022. Every item includes the governed WCS coverage id, native resolution, source unit, observation year, legend, attribution and limitation.

POST /api/v1/projects/{project_id}/datasets/thematic-raster/acquire

Acquires one allowlisted official coverage behind the synchronous Job abstraction. The request accepts only an EPSG:4326 bbox, optional project Area, one registry product key and an explicit refresh flag:

{
  "bbox": {"min_x": 5.03, "min_y": 51.15, "max_x": 5.25, "max_y": 51.33, "crs": "EPSG:4326"},
  "area_id": "optional-project-area-uuid",
  "product_key": "population_density_2019",
  "force_refresh": false
}

The backend uses native 10 m or 100 m resolution, splits requests into bounded WCS 1.0 tiles, validates EPSG:31370 and documented source values, masks the exact Area and persists an ordinary raster Dataset and DatasetVersion. It does not accept arbitrary URLs, coverage ids, resolutions or expressions.

The Flanders map workbench orchestrates this existing endpoint only after an explicit municipality or rectangle selection. It requests all five governed products for the same bbox ∩ Area, reuses the checksum-bound Dataset for an identical request and then calls the normal selection endpoint. The browser never contacts WCS directly. A whole-Flanders raster request is intentionally unavailable in the map flow because it exceeds the bounded 60 km/30 million cell guardrail; users must choose a municipality or draw a smaller rectangle. An Area is recorded as coverage_scope=municipality only when its canonical name starts with Gemeente ; a regional Area remains bounded_selection.

POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/thematic/select

Returns source-correct metrics for a bbox and optional exact Area mask:

  • occupied/open hectares, share and valid raster area for binary products;
  • estimated inhabitants plus mean/P90 inhabitants per hectare for the 2019 population raster;
  • mean, P10, median and P90 source score for node value and service level.

The response names estimate status, aggregation method, source unit, observation year, attribution, unsupported metrics and product limitation. Current register population, live public-transport availability and causal interpretations are not produced.

GET /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/thematic/image

Returns a constrained transparent PNG generated from the persisted governed raster. It accepts neither an arbitrary file path nor a provider URL and feeds the existing MapLibre image-overlay path.

GET /api/v1/projects/{project_id}/datasets

List datasets.

GET /api/v1/projects/{project_id}/datasets/source-freshness

Returns one read-only, canonical source-governance report for all persisted project datasets. Datasets are grouped by source_name (falling back to source) and classified as a rolling snapshot, annual release, fixed edition, scenario, historical archive or local artifact.

Each source item reports dataset/version counts, latest import and observation evidence, latest source version, next review date where meaningful, historical series availability and local integrity counts for missing DatasetVersions, checksum mismatches, missing storage files and size mismatches. Status is one of current, due, review_required or local.

This endpoint never contacts an external provider, downloads data, mutates a Dataset or silently refreshes a publication. Fixed editions and scenarios are not marked stale merely because their source date is old. Unknown sources are review_required until an explicit publication policy is defined.

GET /api/v1/projects/{project_id}/datasets/source-catalog-probes

Performs an explicit, read-only release probe for the allowlisted GRB WFS, most-recent orthophoto WMS, Statbel population DCAT catalog and ALZ agricultural-use parcel publication page. For OGC services, the endpoint reads bounded GetCapabilities responses, confirms the expected layers and follows only HTTPS ISO 19139 GetRecordById links on metadata.vlaanderen.be. For Statbel, it reads the bounded official RDF/Turtle catalog and selects the latest uniquely identified Dutch Bevolking per statistische sector release. It requires the official landing page, CC BY 4.0 license and allowlisted distribution identities but never requests a ZIP or XLSX file. For ALZ, it reads only the bounded official HTML release page and accepts only exact www.landbouwvlaanderen.be/bestanden/gis/agpa_<year>_<date>_public.zip link identities. It does not request those archives. Query parameter refresh=true bypasses the short in-memory response cache.

Each provider item returns service reachability, matched/missing layers, the official metadata identifier, title, edition, publication/metadata dates, local source_version and one comparison status: same, different, not_comparable, no_local_data or unavailable. Provider status is available, degraded, unavailable or disabled. A difference means only that an operator should review provenance; it is not an update instruction.

ALZ comparisons use only the latest definitive third snapshot, normalized as <campaign>-v3. A newer first or second snapshot is reported in the item title and message as provisional but cannot mark a local definitive historical edition as outdated. service_type=HTML uses definitive_archive and current_snapshot as evidence markers in the existing expected/matched/missing arrays; no parallel response shape is introduced.

Statbel comparisons use the four-digit population reference year. The 2025 release requires the new REDEGEO TXT/ZIP variant; its old-sector-layout file is reported only as transition evidence. service_type=DCAT uses population_txt_current, landing_page and cc_by_4_0 as evidence markers. A newer statistical-sector geometry edition is not interpreted as a newer population release.

Future Statbel execution remains outside the HTTP request cycle in scripts/manage_statbel_population_release.py. It reuses this read-only catalog response for plan, then separates filesystem-only stage, named human review and checksum-confirmed apply. No additional public endpoint is introduced. Apply delegates to the existing dataset upload contract and creates a new immutable annual snapshot only after all evidence is unchanged.

Future definitive ALZ execution likewise remains outside the HTTP request cycle in scripts/manage_alz_agriculture_release.py. It derives the exact agpa_<campaign>_<publication-date>_public.zip identity from the existing catalog response and accepts only YYYY-v3. stage is filesystem-only, review is a named approval and apply requires exact plan/review hashes, revalidates the current catalog and delegates to the existing Dataset upload contract. No ALZ release endpoint, background task or provider URL parameter is added; v1/v2 campaign snapshots remain non-importable.

Current-orthophoto release preflight also remains outside the HTTP request cycle in scripts/orthophoto_release_preflight.py. It composes the existing product-registry and source-catalog envelopes with allowlisted WMS GetCapabilities, WCS DescribeCoverage and bounded Vliegdagcontour GetFeatureInfo evidence for one 128-1,024 m EPSG:4326 selection. The result reports product variant, official YYYY.NN edition, exact raster-domain containment, sampled flight dates/years, local comparison state and staging_permitted. It performs no pixel request, upload, Job, Dataset write or legacy metadata rewrite. This operator script adds no public API contract.

Governed pixel promotion remains outside the HTTP request cycle in scripts/manage_orthophoto_release.py. plan reruns that read-only preflight; stage makes exactly one allowlisted bounded Ortho GetMap request and writes only checksummed source/raster/preview evidence; review requires a named approval; and apply requires the exact plan/review SHA-256 values. Apply revalidates the remote identity and local comparison state, then delegates to the existing POST .../datasets/upload contract with dataset_type=raster, source_name=digitaal_vlaanderen_orthophoto, the official YYYY.NN source_version and complete source/provenance metadata. No release endpoint, provider URL parameter, Job type or alternate response envelope is added.

The endpoint accepts no arbitrary URL, feature query, area or layer. It does not fetch vector features, raster pixels or models, create jobs/datasets, write to PostGIS or trigger an import. The normal source-freshness endpoint remains local-only and never invokes this probe implicitly.

GET /api/v1/projects/{project_id}/datasets/grb-refresh-plan

Builds a read-only refresh decision for the governed kempen-transport-region GRB snapshot series. Query parameter refresh_catalog=true explicitly bypasses the catalog cache. The response maps the official dated edition to buildings, roads, water and parcels and reports each latest local Dataset, source version, observation date, feature count, artifact size and refresh state.

Layer state is current, update_available, not_loaded, review_required or remote_unavailable. The summary reports how many new immutable Datasets would be created and how many existing snapshots remain retained. The endpoint does not fetch GRB features, stage files, create a Job, write to PostGIS or start an operator process. Exact remote deltas remain unavailable until every municipality partition has been staged and validated.

Regional execution uses scripts/manage_grb_refresh.py outside the request cycle. stage requires the exact official ISO edition, invokes the existing regional GRB operators with --fetch-only, validates all artifact/partition checksums and emits a SHA-256-bound plan. apply requires that exact plan hash, revalidates every staged byte and delegates persistence to DatasetService and VectorFeatureService. It creates new temporal snapshots and never deletes or overwrites an older Dataset.

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; default nearest)
  • output_name

Returns a job payload with derived dataset id in result.output_dataset_id.

Failure modes:

  • code: INVALID_PARAMETERS for bad CRS or resampling
  • code: INVALID_DATASET_CRS when source raster CRS is missing
  • code: RASTER_PROCESSING_UNAVAILABLE when 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_PARAMETERS for non-positive/non-integer band indices
  • code: INVALID_PARAMETERS for band index outside source band count
  • code: INVALID_DATASET_TYPE when source is not raster
  • code: RASTER_PROCESSING_UNAVAILABLE when 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_PARAMETERS for non-positive/non-integer band indices
  • code: INVALID_PARAMETERS for band index outside source band count
  • code: INVALID_DATASET_TYPE when source is not raster
  • code: RASTER_PROCESSING_UNAVAILABLE when 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_PARAMETERS for non-positive/non-integer band indices
  • code: INVALID_PARAMETERS for band index outside source band count
  • code: INVALID_DATASET_TYPE when source is not raster
  • code: RASTER_PROCESSING_UNAVAILABLE when 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_id is optional and must belong to the route project. When present, PostGIS filtering and configured aggregations use bbox ∩ Area. A bbox that encloses the complete Area therefore produces the exact full-work-area result, while a drawn rectangle that crosses a municipality boundary is clipped to that official boundary.
  • Results are generated from persisted PostGIS vector_features, not from client-side map data.
  • feature_count is the number of GeoJSON features returned in the bounded preview. total_feature_count is the exact number of persisted rows intersecting the requested bbox or persisted Area geometry.
  • summary keeps one backwards-compatible primary metric and exposes all relevant measurements in metrics. 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.
  • Governed datasets may declare additional source_metadata.selection_metrics. Each metric may constrain one persisted feature-property to an explicit value allowlist before the same PostGIS aggregation runs. The BWK/Natura 2000 dataset uses this only for official EVAL classes; it does not collapse mixed classes into a made-up score.
  • 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 limit and returns truncated=true when total_feature_count exceeds the returned preview.
  • limit is bounded to 1..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 moveend requests and explicitly reports truncated=true as 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" and derived_from_dataset_id pointing to the source dataset.
  • The persisted GeoJSON properties retain source provenance as source_dataset_id and source_vector_feature_id.
  • Empty selections return VECTOR_OPERATION_EMPTY_RESULT and 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

The legacy Sprint 7 provider registry remains the exact grb|osm|manual|fixture import abstraction. National and maritime source authority is exposed separately so adding official source families cannot silently change that import contract.

GET /api/v1/external/coverage/catalog

Returns the normalized Belgium and Belgian North Sea theme vocabulary, legal coverage zones, allowed availability states and audited source contracts. Every source includes authority, native layers, geometry types, acquisition mode, source URL, attribution, licence and limitation text.

Allowed result states are exactly:

  • operational: a matching ready Dataset is materialized in the project;
  • partial: the governed integration exists but matching project data is absent or incomplete;
  • not_configured: an audited source has no operational adapter;
  • unsupported: no audited contract supports the theme in that zone.

POST /api/v1/external/coverage/resolve

Resolves a user-drawn EPSG:4326 bbox against persisted national and maritime Areas. Cross-region and land/sea selections remain split by zone.

{
  "project_id": "uuid",
  "bbox": {
    "minx": 2.65,
    "miny": 51.05,
    "maxx": 2.85,
    "maxy": 51.20
  },
  "themes": ["buildings", "surface_water", "bathymetry"]
}

The response returns intersected_zones, outside_supported_scope, one item per zone/theme combination, matching source names and IDs of actually materialized Datasets. An empty themes list requests the complete normalized vocabulary. Unknown themes fail with COVERAGE_THEME_UNSUPPORTED.

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

Compatibility contract for provider-specific import flows. It never writes datasets itself.

Request:

{
  "project_id": "uuid-or-local-id",
  "area_id": "optional uuid-or-local-id",
  "layers": ["buildings"],
  "dataset_role": "optional source|reference"
}

GRB response:

{
  "provider_name": "grb",
  "status": "bounded_request_required",
  "message": "Use the governed project GRB acquisition endpoint with an EPSG:4326 bounding box and one supported layer.",
  "requested_layers": ["buildings"],
  "dataset_id": null,
  "dataset_role": "reference",
  "source_name": "grb"
}

OSM still returns not_configured. Manual and fixture providers point callers to existing upload/fixture flows. No provider writes directly to vector_features; the bounded GRB integration and all future provider output 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"]
}

This legacy compatibility endpoint never fetches. It returns bounded_request_required and directs callers to POST /api/v1/projects/{project_id}/datasets/grb/acquire, where bbox, Area, product allowlist, limits and persistence are enforceable.

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 unbounded GRB WFS download or OSM Overpass integration is implemented. Bounded GRB OGC API Features acquisition is documented under Datasets.

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:

  1. optional explicit POST /api/v1/projects/{project_id}/datasets/upload for a georeferenced GeoTIFF;
  2. POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/tile with 512 px tiles and 64 px overlap;
  3. GET /api/v1/detection/yolo/preflight with the returned manifest and selected local model asset;
  4. POST /api/v1/detection/run only after successful preflight;
  5. persisted run, Detection list and Detection GeoJSON reads;
  6. 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 from GET /api/v1/detection/model-assets; when supplied, preflight validates that asset path instead of the default YOLO_MODEL_PATH.
  • check_model_load: default false; when true, 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=true
  • YOLO_MODEL_PATH pointing to an existing local model file
  • backend optional AI dependencies installed with geointel-backend[ai]
  • tile_manifest_path pointing 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_TYPE when the dataset is not raster.
  • DETECTION_MODEL_NOT_FOUND when the model id is unknown.
  • DETECTION_MODEL_ASSET_NOT_FOUND when model_asset_id is not present in the configured model directory.
  • FIXTURE_MODE_REQUIRED when manual-fixture-detector is requested without parameters_json.fixture_mode=true.
  • DETECTION_TILE_MANIFEST_REQUIRED when yolo-configured is requested without tile_manifest_path.
  • DETECTION_TILE_MANIFEST_NOT_FOUND when the provided manifest path does not exist.
  • DETECTION_TILE_MANIFEST_INVALID when the manifest cannot be parsed or lacks tile metadata.
  • DETECTION_TILE_LIMIT_EXCEEDED when the manifest exceeds YOLO_MAX_TILES.
  • Configured YOLO inference forwards YOLO_MAX_DETECTIONS to Ultralytics max_det and defaults to 1000 so 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 Detection rows are persisted. Same-class candidates are confidence-sorted and lower-confidence candidates with geometry IoU greater than or equal to YOLO_DUPLICATE_IOU_THRESHOLD are suppressed. The default is 0.5; 0 disables this GeoIntel-side post-processing for debugging.
  • DETECTION_DEPENDENCY_UNAVAILABLE when YOLO dependencies are not installed.
  • DETECTION_MODEL_LOAD_FAILED when 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_id
  • class_name
  • min_confidence

GET /api/v1/detection/datasets/{dataset_id}/detections

Returns persisted detections for a raster dataset. Optional filters:

  • analysis_run_id
  • class_name
  • min_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_id
  • class_name
  • confidence
  • model_name
  • model_version
  • analysis_run_id
  • dataset_id
  • job_id
  • source_tile_path
  • bbox_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:

  • precision
  • recall
  • f1_score
  • mean_iou
  • false_positives
  • false_negatives
  • quality_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_raw and reference_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_configured
  • fixture-segmenter: configured for explicit test/demo fixtures only
  • yolo-seg-configured: not_configured
  • sam-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_TYPE when the dataset is not raster.
  • SEGMENTATION_MODEL_NOT_FOUND when the model id is unknown.
  • FIXTURE_MODE_REQUIRED when fixture-segmenter is requested without parameters_json.fixture_mode=true.
  • INVALID_FIXTURE_SEGMENTATIONS when fixture payloads are not a list.
  • INVALID_FIXTURE_GEOMETRY when 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_id
  • class_name
  • min_confidence

GET /api/v1/segmentation/datasets/{dataset_id}/segmentations

Returns persisted segmentation records for a raster dataset. Optional filters:

  • analysis_run_id
  • class_name
  • min_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_id
  • class_name
  • confidence
  • area_m2
  • model_name
  • model_version
  • analysis_run_id
  • dataset_id
  • job_id
  • source_tile_path
  • tile_index
  • mask_path
  • bbox_json
  • provenance_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, removed or unchanged
  • source_dataset_id
  • target_dataset_id
  • source_feature_id
  • target_feature_id
  • iou

Limitations:

  • Change detection itself performs no provider fetch. Bounded GRB acquisition is a separate explicit Dataset operation; OSM and Sentinel remain disabled.
  • No fake object lifecycle classification.
  • No changed classification without durable object ids/versioning.
  • No first-class change table yet; the current output is stored in job result_json and 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",
  "area_id": "optional-uuid-used-as-an-official-area-constraint",
  "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. When area_id is present, the export scope is bbox ∩ Area. The frontend sends the active Area for drawn, manual and full-work-area selections so no result can leak outside the chosen official boundary. A bbox enclosing the complete Area still resolves to the exact Area geometry. Raster datasets are rejected for dataset and selection GeoJSON export.

POST /api/v1/exports/map-result

Persists the active map result before opening the Downloads workspace. The backend recomputes the result from persisted data; it never accepts client metrics as authoritative export content.

Current vector or governed raster request:

{
  "project_id": "uuid",
  "mode": "current",
  "dataset_id": "uuid",
  "bbox": {
    "min_x": 5.10,
    "min_y": 51.17,
    "max_x": 5.11,
    "max_y": 51.18,
    "crs": "EPSG:4326"
  },
  "area_id": null,
  "partitioned": false,
  "product_key": null,
  "theme_id": "buildings",
  "name": "buildings-analysis"
}

Historical comparison request:

{
  "project_id": "uuid",
  "mode": "evolution",
  "earlier_dataset_id": "uuid",
  "later_dataset_id": "uuid",
  "bbox": {
    "min_x": 5.10,
    "min_y": 51.17,
    "max_x": 5.11,
    "max_y": 51.18,
    "crs": "EPSG:4326"
  },
  "theme_id": "forest"
}

Vector results reuse the authoritative vector_selection_geojson flow. Governed raster results are persisted as map_analysis_json; temporal comparisons use map_evolution_json. Metadata records the bbox, optional exact Area scope, source datasets, theme and server_recomputed=true.

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. Configured supplemental metrics can use filter_property plus filter_values. Filters are server-owned dataset metadata, not arbitrary client SQL or request expressions.

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.

Governed regional GRB snapshots use the official OGC feature identifier as their object identity. New imports declare the identity scheme and accepted collection prefixes explicitly. Existing operator-managed GRB snapshots are accepted only when their authoritative, complete and area-clipped provenance matches an approved regional operator and every selected identifier is present, unique and uses the expected prefix. Missing, duplicate or unexpected identifiers fail closed to metric-only comparison. The existing 5,000-feature selection limit also remains in force. Differences between daily GRB editions describe changes in the official registration; they do not prove that a physical change happened on the exact publication date.

For reference_layer_name=agriculture, the primary map metric is exact intersected declared-use area in hectares. Supplemental metrics use server-owned filters on the normalized official main-crop group. Annual ALZ Datasets share one scope-specific temporal series, but declare identity_stable=false; their temporal response compares area totals and returns no parcel-level added/removed/modified claims.

For reference_layer_name=building_registry, the primary metric is exact intersected building-footprint area in hectares. Supplemental server-owned metrics expose register building/status counts, aggregate building-unit and address-status counts, and confirmed GRB matches. Filtered feature_count metrics apply their configured property filter in PostGIS just like filtered area/sum metrics. Address labels and house/box numbers are never part of the queryable Feature properties or response contract.

The governed snapshot is scoped to its persisted area_id. The frontend may prefer it over the regional GRB building layer only when that exact Area is active; another municipality or the full region must continue to use the regional GRB Dataset. No new register-specific API endpoint exists: upload, GeoJSON, exact selection and temporal provenance use the existing Dataset contracts.

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.

Bathymetry and VHA profile acquisition

GET /api/v1/projects/{project_id}/datasets/bathymetry/sources

Returns the governed bathymetry source registry in the canonical envelope. VHA inland profiles are operational. MDK Belgian Continental Shelf is probe_only; SPW Walloon bathymetry remains available_not_integrated. Neither source can be acquired until its raster/download and vertical-datum flow passes live validation.

GET /api/v1/projects/{project_id}/datasets/bathymetry/sources/mdk_bcp_bathymetry/readiness

Runs one bounded, read-only WCS 1.0.0 GetCapabilities request with mandatory system TLS verification and a configured response-size limit. Status is one of disabled, invalid_configuration, tls_error, endpoint_unavailable, invalid_capabilities or reachable. A reachable response lists coverage identifiers, advertised formats and CRS values, but always returns acquisition_supported=false. There is no insecure TLS fallback and no GetCoverage request.

POST /api/v1/projects/{project_id}/datasets/bathymetry/profiles/acquire

{
  "bbox": {"min_x": 5.0, "min_y": 51.0, "max_x": 5.3, "max_y": 51.3, "crs": "EPSG:4326"},
  "area_id": "optional persisted Area uuid",
  "force_refresh": false
}

The endpoint creates a synchronous vector.bathymetry_profiles.acquire Job. It queries the official VHA Digital Atlas in bounded pages, intersects every point with the persisted Area when supplied and persists one ordinary reference Dataset plus VectorFeature rows. The result reports exact profile, document, structured-depth/width and watercourse counts and the source measurement-date range.

The acquired Dataset uses source_name=vmm_vha_bathymetry_profiles, dataset_role=reference and reference_layer_name=bathymetry_profiles. Existing vector content and selection endpoints provide GeoJSON and metrics. The contract does not expose a continuous bathymetric surface, current water depth or water volume.

POST /api/v1/projects/{project_id}/datasets/bathymetry/profiles/partitions/finalize

Finalizes a complete municipality-partition manifest. The request supplies a scope key, every expected municipality Area id, one ready VHA Dataset id for each non-empty partition, explicit Area ids with zero source profiles, a SHA-256 manifest identity and observation time. The backend verifies project, Area, provider, Dataset and one-Dataset-per-Area consistency. Only a complete accounting sets partitioned_source_audit=true and regional_partitions_complete=true; partial operator runs remain hidden at regional scope.

POST /api/v1/projects/{project_id}/datasets/bathymetry/profiles/partitions/select

Runs one bounded spatial selection across the latest complete flanders VHA manifest. The request reuses VectorSelectionRequest: EPSG:4326 bbox, optional persisted Area and a result limit of at most 1,000 features.

The backend first rejects incomplete or inconsistent manifests, selects only municipality Dataset partitions whose recorded bounds overlap the request and then queries their persisted vector_features with one PostGIS intersection. When the Area is a municipality, only that exact Area partition is eligible. A regional Area can combine all intersecting partitions without treating one municipality Dataset as representative regional data.

The canonical response extends the ordinary vector-selection result with partition_count, available_partition_count, partition_scope_key, source_name and the contributing dataset_ids. GeoJSON is capped by the request limit, while total_feature_count and all configured depth/width metrics are calculated across the complete spatial result. Empty municipalities return an empty, honest result. No provider request occurs during analysis.

Future provider output continues to use DatasetService and, for vectors, VectorFeatureService. Arbitrary service URLs, browser-side fetches, insecure TLS bypasses and startup downloads remain forbidden.

Governed official nature and soil acquisition

GET /api/v1/projects/{project_id}/datasets/official-vector/products

Returns the fixed official vector registry in the canonical { "data": ... } envelope. The allowlist contains bwk_natura2000_2025 and dov_soil_types; arbitrary collection names or URLs are never accepted.

POST /api/v1/projects/{project_id}/datasets/official-vector/acquire

Request:

{
  "bbox": {
    "min_x": 5.05,
    "min_y": 51.15,
    "max_x": 5.25,
    "max_y": 51.30,
    "crs": "EPSG:4326"
  },
  "area_id": "optional-area-uuid",
  "product_key": "bwk_natura2000_2025",
  "force_refresh": false
}

The synchronous vector.official.acquire Job validates the metric request size, intersects bbox with the persisted Area, retrieves every bounded page, clips polygon geometry in EPSG:31370 and persists EPSG:4326 features through DatasetService. A repeated exact request can reuse the 24-hour cache. Provider errors, unstable or incomplete WFS pagination and safety-limit violations fail without persisting a truncated Dataset.

bwk_natura2000_2025 preserves BWK EVAL, EENH*, HAB* and PHAB* semantics. dov_soil_types preserves mapped soil, texture, drainage, profile and substrate classes and is dated as the 1949-1971 survey period. Neither contract accepts a caller-supplied endpoint.

Governed Landgebruik Vlaanderen forest and agriculture

The existing GET /api/v1/projects/{project_id}/datasets/thematic-raster/products registry also returns forest_land_use_2025 for source class 12 and agricultural_land_use_2025 for source classes 13 and 14. Both use the existing thematic acquisition and selection contracts. Persisted rasters are binary masks; the source class allowlist and original source-value range are retained and validated.