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 each one is a POST to the OSM extractor plus a short poll while the
service fetches and processes the geometry. Road and water are two separate
features — you create each and poll each. The Python SDK creates both and
waits in a handful of lines; the raw HTTP walkthrough is the same work, one call
at a time.
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.
With the Python SDK
Section titled “With the Python SDK”create_road_feature_from_osm and create_water_feature_from_osm each POST
to the extractor and return a Feature; .wait() blocks until the async
extraction finishes. Create both, wait on both:
import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
domain = ff.Domain.from_id("your-domain-id")
road = ff.features.create_road_feature_from_osm(domain, name="OSM road")water = ff.features.create_water_feature_from_osm(domain, name="OSM water")
road.wait()water.wait()Each call mints a feature id — record them: your-road-feature-id and
your-water-feature-id. They complete independently, which is why each
gets its own .wait().
The raw HTTP flow (curl)
Section titled “The raw HTTP flow (curl)”The same two features as raw calls — a POST per extractor, then a poll per
feature.
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"}'{ "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 comes back as polygons too: lakes and ponds are already areal, while linear waterways (rivers, streams) are buffered by class the way roads are. 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 - 1.
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 fails during processing — with aFEATURE_EMPTYerror for both grid masks and inventory modifications. 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.