Skip to content

Upload a GeoJSON to create a layerset

You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.

This guide creates a Layerset feature resource from a flat GeoJSON FeatureCollection of fuelbed polygons. Use it whenever you want to attach a custom set of surface-fuel inputs to a domain — each Feature’s properties block carries one fuelbed’s row of inputs for downstream rasterization.

The endpoint is POST /domains/{domain_id}/features/layerset/geojson, and unlike the GeoTIFF and netCDF upload workflows this one is a single synchronous call:

  1. Create the layerset — the API validates the GeoJSON, uploads it to storage, computes the union bounds, and returns 201 Created with status: "completed" immediately. There is no signed URL, no PUT step, and no polling.

The body is the GeoJSON itself. Most of what’s interesting on this page is the per-feature properties contract — what each Feature must declare and what’s optional — plus the CRS requirement.

  1. An API key. Create one in the FastFuels web app under your account settings. Every request below sends it in the api-key header — set it once here: my-api-key. It will propagate to every code block on the page.

  2. A domain in a projected CRS. All features hang off a domain. The domain’s CRS effectively anchors any feature you upload to it, so pick one whose CRS matches the coordinate units of your GeoJSON. If you do not have a domain yet, see the Create a domain how-to guide, then come back here. Plug your domain’s id in once: your-domain-id.

  3. A flat GeoJSON FeatureCollection in your domain’s CRS. The FeatureCollection must:

    • Declare its CRS on the top-level crs block. Both "EPSG:32612" and the URN form "urn:ogc:def:crs:EPSG::32612" are accepted. If you omit crs, the server falls back to EPSG:4326 per the GeoJSON spec, which is geographic and is therefore rejected.
    • Contain at least one Feature. Empty FeatureCollections are rejected with 422 because there’s nothing to rasterize.
    • Use Polygon or MultiPolygon geometries. Standard tooling (QGIS, GDAL, geopandas) emits the right shape automatically.
    • Carry the per-feature properties on every Feature.

    geopandas.GeoDataFrame.to_file("layerset.geojson", driver="GeoJSON") is the conventional way to author one of these in Python — make sure your GeoDataFrame’s .crs is set to your domain’s projected CRS first.

POST the body to /domains/{{DOMAIN_ID}}/features/layerset/geojson. The body is the GeoJSON FeatureCollection — type is "FeatureCollection", the CRS goes on the top-level crs block, and the fuelbeds go in features. Optional resource-level metadata sits alongside them at the top level:

  • name, description, tags — optional metadata that shows up on the returned Feature.

Two shapes cover most cases:

The smallest payload that exercises the contract — two fuelbeds, each with only the required properties columns, no resource-level metadata.

Create layerset — minimal body
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/features/layerset/geojson' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": { "name": "urn:ogc:def:crs:EPSG::32612" }
},
"features": [
{
"type": "Feature",
"properties": {
"fuel_type": "shrub",
"fuel_loading": 1.2,
"fuel_height": 0.6,
"percent_cover": 35,
"distribution": "homogeneous"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[294120.0, 5199000.0],
[294520.0, 5199000.0],
[294520.0, 5199400.0],
[294120.0, 5199400.0],
[294120.0, 5199000.0]
]]
}
},
{
"type": "Feature",
"properties": {
"fuel_type": "herb",
"fuel_loading": 0.4,
"fuel_height": 0.3,
"percent_cover": 60,
"distribution": "uniform_random"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[294520.0, 5199000.0],
[294760.0, 5199000.0],
[294760.0, 5199400.0],
[294520.0, 5199400.0],
[294520.0, 5199000.0]
]]
}
}
]
}'

Three fields on the response matter for downstream calls:

  • id — the new feature’s identifier. Record it: your-feature-id. You’ll use it for any subsequent GET/PATCH/DELETE against this layerset.
  • status — already "completed" on the 201. There is no separate polling step.
  • georeference.bounds — the union bounding box across every Feature geometry, expressed in the GeoJSON’s own CRS.

Every Feature in the FeatureCollection must carry a properties block. The required fields mirror the rasterizer’s input columns:

FieldTypeRequiredDescription
fuel_typestringFree-form category label (e.g. "shrub", "herb", "litter").
fuel_loadingfloatFuel loading for the fuelbed.
fuel_heightfloatFuel height for the fuelbed.
percent_coverfloatFractional cover in percent — must be in [0, 100].
distributionenumOne of homogeneous, uniform_random, random_clusters.
patch_sizefloat⚠️Required when distribution == "random_clusters"; ignored otherwise.
strata_fbstringoptionalTraceability label for your own records (e.g. "Shrub1_52").
live_fuel_moisturefloatoptionalLive fuel moisture; produces a NaN output band when omitted.
dead_fuel_moisturefloatoptionalDead fuel moisture; produces a NaN output band when omitted.
heat_of_combustionfloatoptionalHeat of combustion; produces a NaN output band when omitted.
patch_std_devfloatoptionalCluster-size spread; only meaningful for random_clusters.

Geometries must be Polygon or MultiPolygon. Other geometry types (Point, LineString, GeometryCollection) are rejected by the GeoJSON validator.

The server reads CRS from the FeatureCollection’s optional top-level crs block:

{
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": { "name": "urn:ogc:def:crs:EPSG::32612" }
},
"features": [ ... ]
}

Both forms are accepted: bare "EPSG:32612" and the URN form "urn:ogc:def:crs:EPSG::32612" (the form geopandas.to_file emits).

If the crs block is missing, the server falls back to EPSG:4326 per the GeoJSON spec. Because EPSG:4326 is geographic, that fallback is then rejected with 422:

{
"detail": "Layerset CRS is geographic (EPSG:4326). Rasterization requires a projected CRS so cell sizes are in meters. Reproject the GeoJSON to a UTM (or other projected) CRS and declare it on the FeatureCollection's `crs` block."
}

The fix is to reproject the GeoJSON to a projected CRS — typically the UTM zone covering your domain — and declare it on the crs block before uploading:

gdf = gdf.to_crs("EPSG:32612")
gdf.to_file("layerset.geojson", driver="GeoJSON")

The upload endpoint validates synchronously, so most failures surface as 422 on the POST itself with a detail string explaining what went wrong. The cases worth knowing about:

  • Geographic CRS — the crs block resolves to a geographic CRS (e.g. EPSG:4326, the GeoJSON default when crs is omitted). Reproject to a projected CRS and declare it explicitly. Captured response shown above.
  • Unparseable CRS — the crs.properties.name string isn’t recognised by pyproj. Use the bare EPSG:<code> form or the URN form urn:ogc:def:crs:EPSG::<code>.
  • Missing patch_size for random_clusters — a Feature has "distribution": "random_clusters" but no patch_size. The error surfaces as a pydantic validation error pointing at the offending Feature. Either set patch_size, or switch the distribution to homogeneous or uniform_random.
  • Empty FeatureCollectionfeatures is []. The validator rejects this because there’s nothing to rasterize. Include at least one Feature.
  • Wrong geometry type — a Feature carries a Point, LineString, or GeometryCollection. Convert to Polygon / MultiPolygon before uploading.
  • percent_cover out of range — values must be in [0, 100], not [0, 1]. The validator rejects values outside that range.

When a 422 returns, no resource is created — there’s nothing to clean up and no need to retry against a fresh URL. Fix the body and re-POST.