Create road and water features from OpenStreetMap
You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.
A Feature is a named, reusable piece of vector geometry attached to a domain. The most common ones come straight from OpenStreetMap: the road network and water bodies within your domain. Once created, a feature is a single source of truth you can reference by id — most often to mask a fuel grid or remove trees where roads and water cross your data.
Creating one is a POST to the OSM extractor plus a short poll while the
service fetches and processes the geometry.
Prerequisites
Section titled “Prerequisites”-
An API key. Set it once — it propagates to every code block: my-api-key.
-
A domain in a projected CRS. Plug its id in once: your-domain-id. If you don’t have one, see Create a domain. The extractor clips OSM data to this domain’s extent and stores the result in the domain’s CRS.
Step 1 — Create the road feature
Section titled “Step 1 — Create the road feature”POST to /domains/{{DOMAIN_ID}}/features/road/osm. The body is minimal —
type and an optional name/description/tags.
curl -X 'POST' \ 'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/features/road/osm' \ -H 'accept: application/json' \ -H 'api-key: my-api-key' \ -H 'Content-Type: application/json' \ -d '{ "type": "road", "name": "OSM road"}'import time
import requests
API_KEY = "my-api-key"DOMAIN_ID = "your-domain-id"BASE = "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app"HEADERS = {"api-key": API_KEY}
def create_osm_feature(kind: str) -> dict: """Create a road or water feature from OpenStreetMap and poll to completion.""" resp = requests.post( f"{BASE}/domains/{DOMAIN_ID}/features/{kind}/osm", headers=HEADERS, json={"type": kind, "name": f"OSM {kind}"}, ) resp.raise_for_status() feature_id = resp.json()["id"]
while True: feature = requests.get( f"{BASE}/domains/{DOMAIN_ID}/features/{feature_id}", headers=HEADERS ).json() if feature["status"] in ("completed", "failed"): return feature time.sleep(5)
road = create_osm_feature("road")water = create_osm_feature("water")print(road["id"], road["status"]) # -> <road id> completedprint(water["id"], water["status"]) # -> <water id> completed{ "id": "your-road-feature-id", "domain_id": "your-domain-id", "type": "road", "name": "OSM road", "description": "", "status": "pending", "progress": null, "created_on": "2026-06-02T19:01:51.770011", "modified_on": "2026-06-02T19:01:51.770011", "source": { "product": "osm", "description": "OpenStreetMap road network", "extent_buffer_m": 0.0 }, "georeference": null, "error": null, "tags": []}The 201 mints a feature id — record it: your-road-feature-id.
Status starts pending; the extractor fetches OSM data asynchronously.
Step 2 — Create the water feature
Section titled “Step 2 — Create the water feature”Identical shape against the water extractor:
curl -X 'POST' \ 'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/features/water/osm' \ -H 'accept: application/json' \ -H 'api-key: my-api-key' \ -H 'Content-Type: application/json' \ -d '{ "type": "water", "name": "OSM water"}'Record this id too: your-water-feature-id.
Step 3 — Poll until completed
Section titled “Step 3 — Poll until completed”curl -X 'GET' \ 'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/features/your-road-feature-id' \ -H 'accept: application/json' \ -H 'api-key: my-api-key'Status walks pending → running → completed. When it lands, the
georeference.bounds are populated:
{ "id": "your-road-feature-id", "domain_id": "your-domain-id", "type": "road", "name": "OSM road", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-02T19:01:51.770011Z", "modified_on": "2026-06-02T19:02:10.308061Z", "source": { "product": "osm", "extent_buffer_m": 0.0, "description": "OpenStreetMap road network" }, "georeference": { "crs": "EPSG:32611", "bounds": [ 717141.6556112946, 5326524.549818624, 718694.8955395881, 5328248.228078655 ] }, "error": null, "tags": []}{ "id": "your-water-feature-id", "domain_id": "your-domain-id", "type": "water", "name": "OSM water", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-02T19:01:52.250230Z", "modified_on": "2026-06-02T19:02:10.314980Z", "source": { "product": "osm", "extent_buffer_m": 0.0, "description": "OpenStreetMap water features" }, "georeference": { "crs": "EPSG:32611", "bounds": [ 717141.6556112946, 5326524.549818624, 718694.8955395881, 5328248.228078655 ] }, "error": null, "tags": []}Here are the two features over the domain — the Bigfork, Montana area at the head of Flathead Lake:
What you get
Section titled “What you get”A completed feature stores its geometry as polygons in the domain CRS — the road extractor widens OSM centerlines into footprint polygons by road class, and water bodies are already areal. Check how many geometries it holds with the data metadata endpoint:
{ "total_features": 112, "partition_count": 1, "partitions": [ { "index": 0, "num_features": 112 } ]}The geometry itself streams from
/domains/{{DOMAIN_ID}}/features/{{ROAD_FEATURE_ID}}/data/{partition_index}
as GeoJSON (application/geo+json) in the domain CRS — iterate
partition_index from 0 to partition_count.
Next steps
Section titled “Next steps”These features are inputs to the feature-based modification guides:
Common pitfalls
Section titled “Common pitfalls”- Zero features back. A
completedfeature withtotal_features: 0means OSM has no roads/water in your domain extent (common for remote areas, especially water). Referencing an empty feature in a modification masks nothing — and an inventory modification against an empty feature fails during processing. Confirm the count viadata/metadatabefore you rely on it, and considerextent_buffer_mif the feature you want sits just outside the domain. - Geographic-CRS domain. Features inherit the domain CRS, and the
extractor needs metric units. Domains are rejected at creation time if
they’re in a geographic CRS (e.g.
EPSG:4326), so use a projected CRS (the appropriate UTM zone) for the domain. - Polling the wrong id. Each extractor call returns its own feature id. Poll road and water separately — they complete independently.