Skip to content

Upload a GeoTIFF to create a grid

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

This guide creates a grid resource from a GeoTIFF you supply. Use it whenever you have a raster — fuels, topography, a land-cover classification, anything that fits a 2D georeferenced image — that you want to attach to a domain so the rest of the FastFuels API can operate on it.

The endpoint is POST /domains/{domain_id}/grids/upload/geotiff and the workflow is three calls:

  1. Create the grid — the API returns a 201 Created and a short-lived signed URL.
  2. Upload the file — PUT the GeoTIFF to that signed URL; GCS returns 200 OK.
  3. Poll the grid — GET it until status is completed (or failed).
  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. All grids hang off a domain. If you do not have one 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 GeoTIFF that meets these constraints:

    • Format: single- or multi-band GeoTIFF (.tif / .tiff).
    • CRS: must match the domain CRS exactly.
    • Extent: must cover the domain bbox. Pixels outside the bbox (or outside the bbox expanded by num_buffer_cells × native_pixel_size) are clipped on ingest.
    • Size: ≤ 1 GB.

Submit a POST to /domains/{{DOMAIN_ID}}/grids/upload/geotiff. The body declares the bands of the GeoTIFF you are about to upload: bands[i] maps 1-to-1 to GeoTIFF band i + 1. Each band’s key becomes the variable name on the resulting grid, and its quantity sets the band’s type and unit (see Band reference).

Two shapes cover most uploads:

A single band — categorical (a classification raster, e.g. fuel-model codes) or continuous (e.g. an elevation surface). The band is declared by its key alone.

Create grid — single-band GeoTIFF
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/upload/geotiff' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"bands": [
{ "key": "fbfm" }
],
"name": "Custom FBFM40"
}'

The response fields you need for the next steps:

  • grid.id — record this; you’ll need it to poll status.
  • upload.url — the signed URL you PUT your GeoTIFF to. Single use, valid for one hour.
  • upload.headers — the request headers to replay verbatim on the PUT: Content-Type (always image/tiff) and x-goog-content-length-range (0,<max_size_bytes>). GCS rejects the request with 400 Bad Request if either is missing or altered. (upload.content_type and upload.max_size_bytes carry the same two values individually.)

PUT the file directly to the signed URL. The request does not go through the FastFuels API — it goes to Google Cloud Storage. Send no api-key header. Replay the two headers from upload.headers exactly — both are required:

  • Content-Type: image/tiff
  • x-goog-content-length-range: 0,<max_size_bytes>
PUT GeoTIFF to signed URL
curl -X 'PUT' \
'paste-signed-url-from-201-response' \
-H 'Content-Type: image/tiff' \
-H 'x-goog-content-length-range: 0,1073741824' \
--data-binary '@/path/to/your.tif' \
-o /dev/null -w 'HTTP %{http_code}\n'

A successful upload returns an empty body with HTTP 200 OK. The uploader service is then triggered automatically.

Step 3 — Poll the grid until it is completed

Section titled “Step 3 — Poll the grid until it is completed”

The uploader opens the GeoTIFF, validates the CRS against the domain, clips to the domain extent, and writes the result into the grid’s storage. While that runs, GET the grid to watch its status.

GET grid status
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-grid-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'

Status walks pending → running → completed (or → failed). Only the last two are terminal — keep polling on the first two.

{
"id": "your-grid-id",
"domain_id": "your-domain-id",
"name": "Custom FBFM40",
"description": "",
"status": "pending",
"progress": null,
"created_on": "2026-05-19T16:32:41.567669Z",
"modified_on": "2026-05-19T16:32:41.567669Z",
"source": {
"bands": [{ "key": "fbfm", "unit": null, "type": "categorical" }],
"num_buffer_cells": 0,
"format": "geotiff",
"name": "upload",
"object_name": "grids/your-grid-id/upload.tif"
},
"modifications": [],
"bands": [{ "key": "fbfm", "type": "categorical", "unit": null, "index": 0 }],
"georeference": null,
"error": null,
"chunks": null,
"tags": []
}

Keep polling. The status sits at pending until the uploader picks up the file, then moves to running while it validates CRS, clips to the domain, and writes the result. Typical end-to-end time is a few seconds for small rasters, up to a minute or two for files near the 1 GB ceiling.

By default, the uploader keeps exactly the pixels that fall inside the domain bbox. If your raster is larger, the extra pixels are clipped away silently. If it only partially overlaps the domain, the upload still succeeds but the resulting grid covers only the overlap (there is no full-coverage check). The upload fails with NO_OVERLAP only when the raster doesn’t intersect the domain at all.

Two knobs control how forgiving this is:

  • num_buffer_cells in the create request (default 0). Setting this to e.g. 2 keeps two native-resolution cells of padding around the domain on each side, which is useful when downstream operations (resampling, focal filters) are edge-sensitive.
  • The domain extent itself. If you want FastFuels to ingest a region larger than your domain, expand the domain — domains define what “in scope” means for every grid hanging off them.

The expression that must hold is:

GeoTIFF extent ⊇ domain bbox expanded by (num_buffer_cells × native_pixel_size)

Each entry in the bands array describes one raster band:

FieldRequiredDescription
keyyes<quantity> or <quantity>.<label>, with one or more lowercase .label parts (e.g. elevation, bulk_density.foliage, fuel_moisture.live.shrub). The quantity comes from the table below. Becomes the variable name on the grid. Must be unique within the upload.
typeoptional"continuous" or "categorical". Set by the key’s quantity; when given, it must match.
unitoptionalUDUNITS-2 canonical ASCII with ** for exponents (e.g. kg/m**3, 1/m, %). Set by the key’s quantity; when given, it must match.

A type or unit that differs from the key’s quantity is rejected with 422 Unprocessable Entity. A key whose quantity is not in the table is also rejected.

Create grid — type does not match the key
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/upload/geotiff' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"bands": [
{ "key": "fbfm", "type": "continuous" }
],
"name": "Custom FBFM40"
}'

Quantities, grouped by the type and unit they set:

TypeUnitQuantities
categoricalnoneannual_disturbance, fbfm, fbfm13, fccs, plt_cn, spcd, tm_id, tree_id
continuousmcbh, chm, duff_depth, elevation, fuel_depth
continuouskg/m**3bulk_density, cbd
continuouskg/m**2cfl, fuel_load
continuous%cc, curing, fuel_moisture
continuousdegaspect, slope
continuous1/mleaf_area_density, savr
continuouskJ/kgheat_content
continuousnoneirradiance, volume_fraction
  • CRS_MISMATCH after step 2 succeeds. The PUT returns 200 even if the uploaded file’s CRS does not match the domain. The mismatch surfaces in step 3 as status: "failed". Reproject with gdalwarp -t_srs <domain_crs> and re-create the upload.
  • Unknown quantity in key. Rejected with 422 Unprocessable Entity. Start the key with a quantity from the table in the Band reference.
  • Duplicate key in the create request. Rejected with 422 Unprocessable Entity. Each band key must be unique within a single upload.
  • Wrong band order. bands[i] is GeoTIFF band i + 1. Re-check with gdalinfo your.tif before submitting.
  • Treating the upload URL as reusable. It is signed for a single PUT within one hour. If the file changes, request a fresh URL.