API Guide¶
Landscape Decision Theatre exposes a REST API on the same port as the web UI. Application endpoints are prefixed with /api/; tiles, fonts, static data and the documentation site are served from their own prefixes.
internal/api/handler.go and internal/server/server.go. Add or remove a route and this redraws on the next docs build. Unauthenticated by design — review before exposing
No endpoint requires authentication, and several are destructive (POST /api/datapack/install removes the data directory before extracting). The server currently binds all interfaces, and deployments/nginx.conf proxies every path without a denylist.
Do not expose this API to an untrusted network until the hardening tickets are resolved: Unauthenticated datapack install destroys the data directory, handleFileDialog opens a native OS modal in response to an HTTP request, HTTP server binds 0.0.0.0 while the code claims it binds localhost, No request body size limits on any JSON handler.
Expected to change
Responses are not compressed today, and valuesOnly responses use a per-feature GeoJSON wrapper with null geometry. Both are being changed — compression will be negotiated via Accept-Encoding, and valuesOnly is expected to move to a columnar {ids, values} shape.
Tickets: No HTTP compression on any API response, Full-dataset stat fetches ship 16.1 MB of feature wrappers, twice, concurrently.
Server Information¶
GET /api/health¶
Liveness check. Returns 200 with a minimal JSON body.
GET /api/info¶
Returns the current server status and available features. The frontend uses this on startup to decide whether to show the Setup Guide.
Response:
| Field | Type | Description |
|---|---|---|
version | string | Application version |
tiles_loaded | boolean | Whether MBTiles data was found and opened |
geo_loaded | boolean | Whether GeoPackage scenario data is available |
Tilesets¶
GET /api/tilesets¶
Returns the list of available tileset names.
GET /api/tilesets/{name}/metadata¶
Returns the MBTiles metadata for one tileset as JSON.
Response:
{
"name": "africa",
"format": "pbf",
"minzoom": "2",
"maxzoom": "15",
"bounds": "-17.5,-34.8,63.5,37.4",
"center": "22.977,1.258,4"
}
Metadata and Columns¶
GET /api/columns¶
The authoritative list of attribute columns present in the GeoPackage. Sourced from the data, not from metadata.csv.
GET /api/scenarios¶
Returns the available scenario names.
GET /api/metadata/{key}¶
Fifteen routes, each returning one lookup map parsed from metadata.csv, keyed by column name:
| Route | Contents |
|---|---|
colors | Preferred display colour per column |
details | Human-readable descriptions |
variabletypes | Highest-level grouping per column |
inputs | User-input flags |
targetinputs | Target-input flags |
targetranges | Acceptable target ranges |
canmap | MapthisYN — eligible for map/dial/table view |
cangraph | graphthisYN — eligible for chart view |
axislabels | Y-axis labels |
xaxislabels | X-axis tick labels |
units | Display units |
charttypes | typeofgraph — line, boxplot, dial |
groupingvariables | Grouping variable per column |
groupingvalues | Values available per grouping variable |
dial0middle | Whether the dial centres on zero |
See Datapack Format for column semantics, and Architecture for how these drive the selectors.
Scenario and Catchment Data¶
GET /api/scenario/{scenario}/{attribute}¶
Returns attribute values for all catchments in a scenario.
| Parameter | Type | Description |
|---|---|---|
scenario | path | reference, current or future |
attribute | path | Attribute column name |
Returns a map of catchment IDs (HYBAS_ID) to values:
GET /api/catchment/{id}¶
Returns all attribute values for one catchment across all loaded scenarios. Returns 404 if the ID is not found.
GET /api/choropleth¶
Returns a GeoJSON FeatureCollection of catchment polygons with the requested attribute in each feature's properties. Used below the zoom range covered by the catchment vector tiles, where the server returns grid-aggregated cells rather than catchments; above it the map uses /api/catchment-values against the tiled geometry instead.
| Query parameter | Description |
|---|---|
scenario | reference, current or future |
attribute | Attribute column name |
minx, miny, maxx, maxy | Viewport bounding box |
zoom | Current map zoom; selects the server-side aggregation tier |
siteId | Optional; applies site-specific ideal overrides |
valuesOnly | 1 bypasses zoom aggregation and returns every catchment's raw value with null geometry |
GET /api/catchment-values¶
The values half of the vector-tile choropleth: every catchment's value for one attribute within a viewport, with no geometry at all. Geometry comes from the tile pipeline and is reused across attributes, so this is the only thing a pan or an indicator change has to move over the wire. The response is two index-aligned arrays rather than one object per catchment, which at these sizes is roughly an order of magnitude smaller than the equivalent FeatureCollection scaffolding.
| Query parameter | Description |
|---|---|
scenario | reference, current or future |
attribute | Attribute column name |
minx, miny, maxx, maxy | Viewport bounding box |
siteId | Optional; applies site-specific ideal overrides (future only) |
{
"scenario": "current",
"attribute": "rainfall_mm",
"ids": [1120000010, 1120000011],
"values": [812.4, 903.1],
"domain_min": 0,
"domain_max": 2400
}
The domain_min/domain_max are resolved exactly as /api/choropleth resolves them, so the colour scale does not shift when the map crosses between the two transports.
GET /api/aggregate¶
Area-weighted aggregate values for the requested extent and columns.
GET /api/precalculate/full¶
Precomputed full-domain area-weighted means for all attributes, for reference and current. Cached server-side after the first computation.
GET /api/compare¶
Comparison data between two scenarios.
GET /api/catchments/bounds¶
Returns the bounding box of the catchment dataset.
GET /api/catchments/geometry/{id}¶
Returns the geometry for a single catchment.
POST /api/catchments/in-bbox¶
Returns catchments intersecting a bounding box.
Request body:
Sites¶
A site is a saved study area: a boundary, its view layout, and its indicator values. In the WebView runtime these are JSON files under data/sites/; in the browser runtime they live in localStorage and these endpoints are largely bypassed.
GET /api/sites¶
Returns all sites, sorted by creation date, newest first.
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Munywana Conservancy",
"description": "Catchments across the conservancy",
"thumbnail": "/data/images/550e8400-e29b-41d4-a716-446655440000.jpg",
"createdAt": "2026-02-03T10:15:30Z",
"updatedAt": "2026-02-03T14:22:15Z"
}
]
GET /api/sites/{id}¶
Returns a single site. 404 if not found.
POST /api/sites¶
Creates a site.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Site title |
description | string | No | Site description |
geometry | GeoJSON | Yes | Site boundary |
creationMethod | string | Yes | shapefile, geojson, drawn or catchments |
catchmentIds | string[] | No | Present when created from catchment selection |
thumbnail | string | No | Base64 data URI, saved to data/images/ |
PUT / PATCH /api/sites/{id}¶
Updates a site. Accepts the same fields as POST.
Input validation gap
A thumbnail value that is not a data:image URI is currently stored verbatim and is later joined onto the data directory and deleted when the site is deleted. Ticket: Thumbnail path traversal allows arbitrary file deletion.
DELETE /api/sites/{id}¶
Deletes a site and its thumbnail. 204 No Content on success.
POST /api/sites/dissolve-catchments¶
Dissolves a set of catchments into a single boundary. This is the correct pattern for deriving geometry from the study area — the client sends identifiers and receives geometry.
Request body:
Response:
area is always zero
The area field is currently hardcoded to 0 upstream in DissolveCatchments and has never returned a real value. Do not rely on it. Ticket: Dissolved catchment area is always reported as zero.
Site Indicators¶
GET /api/sites/{id}/indicators¶
Returns the site's aggregated indicator values.
POST /api/sites/{id}/indicators¶
Extracts indicators for the site from its constituent catchments. In the browser runtime the request body carries the site object, since the server has no stored copy.
PATCH /api/sites/{id}/indicators¶
Updates user-set indicator values (ideal values and their bounds) and triggers ecological recalculation.
Expected to change
This handler currently reads, mutates and writes the site with no per-site lock, so two concurrent updates lose one of the results. Ticket: Concurrent indicator saves lose updates, and site writes are not atomic.
POST /api/sites/{id}/indicators/reset¶
Resets ideal values back to current.
GET / POST /api/sites/{id}/catchments¶
Returns the per-catchment breakdown for a site, including areaKm2 and aoiFraction. POST accepts a site body for browser-runtime sites the server has never stored.
GET / POST /api/sites/{id}/whiskers¶
Returns whisker (upper/lower bound) values for the site.
Boundary Editing¶
POST /api/sites/{id}/boundary/union/{catchmentId}¶
Adds a catchment to the site boundary and returns the updated geometry.
POST /api/sites/{id}/boundary/difference/{catchmentId}¶
Removes a catchment from the site boundary and returns the updated geometry.
Both return:
Data Pack Management¶
GET /api/datapack/status¶
Returns the current install state — idle, installing (with progress), or failed.
POST /api/datapack/install¶
Installs a data pack from a local archive path.
Destructive and unauthenticated
This handler accepts an arbitrary filesystem path, validates only the .zip/.7z suffix, and recursively deletes the data directory before extracting. It must not be reachable from an untrusted network. Ticket: Unauthenticated datapack install destroys the data directory.
POST /api/dialog/open-file¶
Opens a native file-picker dialog on the host and returns the chosen path. Intended for the desktop runtime only.
Desktop-only endpoint, currently always registered
In server mode this opens a window on the host's desktop and blocks a request goroutine until a human responds. Ticket: handleFileDialog opens a native OS modal in response to an HTTP request.
Browser Downloads¶
These endpoints serve the distributable datapack archive and platform executables for direct browser download. They are only relevant in browser runtime (server deployment); the desktop app installs the data pack via the setup guide from a local file.
All paths are configured in settings.json. See Server Deployment — Enabling Browser Downloads for full setup instructions.
GET /api/datapack/download-info¶
Returns metadata about the configured downloadable archive. The frontend uses this endpoint on page load to decide whether to show the download button.
Response when an archive is configured:
Response when no archive is configured:
| Field | Type | Description |
|---|---|---|
available | boolean | true if data_pack_download_path is set and the file exists |
filename | string | Base filename of the archive (omitted when available is false) |
size_bytes | integer | File size in bytes (omitted when available is false) |
GET /api/datapack/download¶
Streams the configured archive file to the client as an attachment.
Response headers:
| Header | Value |
|---|---|
Content-Disposition | attachment; filename="<archive-filename>" |
Content-Type | application/octet-stream |
Content-Length | File size in bytes |
Error responses:
| Status | Condition |
|---|---|
404 Not Found | data_pack_download_path is not configured or the file does not exist |
500 Internal Server Error | The file exists but cannot be opened |
GET /api/executables/info¶
Returns availability metadata for each platform executable. The frontend uses this on the Download page to decide which platform cards to show as downloadable.
Response:
{
"windows": { "available": true, "filename": "decision-theatre-v1.0.0-windows.exe", "size_bytes": 45678901 },
"linux": { "available": true, "filename": "decision-theatre-linux-amd64-v1.0.0.tar.gz", "size_bytes": 23456789 },
"macos": { "available": false }
}
Each platform object:
| Field | Type | Description |
|---|---|---|
available | boolean | true if the path is set in settings.json and the file exists |
filename | string | Base filename (omitted when available is false) |
size_bytes | integer | File size in bytes (omitted when available is false) |
GET /api/executables/download/{platform}¶
Streams the executable for the requested platform as a file attachment.
Path parameters:
| Parameter | Values | Description |
|---|---|---|
platform | windows, linux, macos | Target platform |
Response headers:
| Header | Value |
|---|---|
Content-Disposition | attachment; filename="<filename>" |
Content-Type | application/octet-stream |
Content-Length | File size in bytes |
Error responses:
| Status | Condition |
|---|---|
400 Bad Request | platform is not one of windows, linux, macos |
404 Not Found | Path not configured in settings.json or file does not exist |
500 Internal Server Error | File exists but cannot be opened |
Vector Tiles¶
GET /tiles/{name}/{z}/{x}/{y}.pbf¶
Serves Mapbox Vector Tiles from the named MBTiles tileset.
| Parameter | Type | Description |
|---|---|---|
name | path | Tileset name, e.g. africa |
z | path | Zoom level |
x | path | Tile column |
y | path | Tile row |
Response: Protocol Buffer (application/x-protobuf) with Content-Encoding: gzip and Access-Control-Allow-Origin: *. Returns 404 if the tile does not exist.
Up to three auxiliary listeners on the following ports serve this same route, to work around per-origin connection limits in grid view. See Architecture.
Style, TileJSON and Fonts¶
GET /data/style.json¶
Returns the MapLibre GL style used to render the vector tiles, read from data/mbtiles/style.json (currently falling back to the resources directory — a fallback that is being removed) with tile URLs rewritten to match the running host and port.
GET /data/tiles.json¶
Returns TileJSON for the loaded tileset.
GET /fonts/{fontstack}/{range}.pbf¶
Proxies MapLibre font glyphs, fetching from the upstream CDN once and serving locally thereafter. This avoids repeated external requests from each map instance in grid view.
Static Data¶
| Prefix | Serves from | Contents |
|---|---|---|
/data/images/ | data/images/ | Site thumbnails |
/data/walkthroughs/ | data/walkthroughs/ | Read-only demo site JSON |
/data/demo/ | data/demo/ | Demo assets used by the guided tours |
/docs/ | embedded | This documentation site |
Static Assets¶
All other routes serve the embedded React SPA. The server implements SPA routing by returning index.html for any path not matching the above patterns.