Skip to content

Upload a netCDF 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 CF-conformant netCDF you supply. Use it whenever you have gridded data — a 2D raster of fuel-model codes, a 3D voxelized bulk-density volume, anything that fits one or more data variables on a regular (y, x) or (z, y, x) grid — 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/netcdf 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 netCDF to that signed URL; GCS returns 200 OK.
  3. Poll the grid — GET it until status is completed (or failed).

Unlike the GeoTIFF upload guide, the request body has no bands field. The netCDF’s data-variable names, units, and dtype come from the file itself.

  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 CF-conformant netCDF. The uploader enforces a small subset of the CF Conventions — concretely:

    • Format: netCDF-4 (.nc), readable by h5netcdf.
    • Dimensions: each data variable must have dims exactly ("y", "x") for 2D or ("z", "y", "x") for 3D, in that order. Mixed ranks in the same file are rejected.
    • CRS: a CF grid_mapping (typically spatial_ref) on the data variables. The CRS must match the domain CRS exactly — no auto-reproject.
    • Units: if a data variable has a units attribute, it must be in UDUNITS-2 canonical ASCII with ** for exponents (e.g. kg/m**3, 1/m, %).
    • Z axis (3D only): the z coordinate must carry positive = "up" and be uniformly spaced, with at least two levels.
    • Pixels: square (dx == dy).
    • Extent: must cover the domain bbox (expanded by num_buffer_cells × native_pixel_size if you set the buffer field).
    • Size: ≤ 1 GB.

    rioxarray.Dataset.rio.write_crs() stamps the CRS correctly. ncdump -h your.nc is a quick way to verify dims, coordinate attrs, and grid_mapping before you upload.

Submit a POST to /domains/{{DOMAIN_ID}}/grids/upload/netcdf. The body only carries resource-level metadata — name, description, tags, num_buffer_cells. The bands themselves are derived from the file at process time.

Two shapes cover most uploads:

A 2D netCDF — one or more data variables with dims ("y", "x"). Categorical (e.g. fuel-model codes) or continuous (e.g. an elevation surface). Integer dtypes become categorical bands; floating-point dtypes become continuous bands.

Create grid — 2D netCDF
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/upload/netcdf' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Custom FBFM40"
}'

Four fields on the response matter for the next steps:

  • grid.id — record this; you’ll need it to poll status.
  • upload.url — the signed URL you PUT your netCDF to. Single use, valid for one hour.
  • upload.content_type — must be sent as the Content-Type header on the PUT. Always application/x-netcdf.
  • upload.max_size_bytes — must be echoed as x-goog-content-length-range: 0,<max_size_bytes> on the PUT. GCS rejects the request with 400 Bad Request if this header is missing or doesn’t match.

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. Two request headers are required and both must match the values returned in step 1:

  • Content-Type: application/x-netcdf — from upload.content_type.
  • x-goog-content-length-range: 0,<max_size_bytes> — from upload.max_size_bytes.
PUT netCDF to signed URL
curl -X 'PUT' \
'paste-signed-url-from-201-response' \
-H 'Content-Type: application/x-netcdf' \
-H 'x-goog-content-length-range: 0,1073741824' \
--data-binary '@/path/to/your.nc' \
-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 netCDF from GCS, validates the CF contract against the domain, clips to the domain extent, derives band metadata from the file’s data variables, 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-19T17:57:05.089784Z",
"modified_on": "2026-05-19T17:57:05.089784Z",
"source": {
"num_buffer_cells": 0,
"format": "netcdf",
"name": "upload",
"object_name": "grids/your-grid-id/upload.nc"
},
"modifications": [],
"bands": [],
"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 the CF contract, clips to the domain, and writes the result. bands is [] and georeference is null until the uploader finishes inspecting the file. Typical end-to-end time is a few seconds for small files, 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 file’s extent is smaller than the domain bbox, the upload will fail validation (NO_OVERLAP if the file misses the domain entirely); if it is larger, the extra pixels are clipped away silently.

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 (resample, 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:

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

The netCDF endpoint has no bands field in the request body. Instead, each data variable in the file becomes one band on the resulting grid, populated as follows when the upload finishes:

Band fieldDerived from
keyThe data variable’s name in the netCDF (e.g. fbfm, bulk_density.foliage).
typecategorical if the variable’s dtype is integer; continuous if floating-point.
unitThe variable’s units attribute, or null if unset. Must be UDUNITS-2 canonical ASCII (kg/m**3, 1/m, %) when set.
indexThe position of the variable in the dataset.

The uploader emits one of the following error.code values in the failed response:

  • MISSING_CRS — the dataset has no CF grid_mapping. Call ds.rio.write_crs("EPSG:<code>") before saving the file.
  • CRS_MISMATCH — the netCDF CRS does not match the domain CRS. The uploader does not auto-reproject; reproject in your authoring pipeline before uploading.
  • WRONG_DIMS — at least one data variable has dims other than ("y", "x") or ("z", "y", "x") in that order, or the file mixes 2D and 3D variables. Transpose with da.transpose("y", "x") (or "z", "y", "x") and drop or split out the variables that don’t fit.
  • INVALID_UNITS — a data variable’s units attribute isn’t valid UDUNITS-2 canonical ASCII. Rewrite the attribute (e.g. kg/m**3 instead of kg/m³).
  • MISSING_Z_POSITIVE — the z coordinate doesn’t carry positive = "up". Set ds["z"].attrs["positive"] = "up" before saving.
  • SINGLE_Z_LAYER — a 3D variable has only one z level, so cell thickness cannot be inferred. Either upload as 2D (drop the z dim) or include at least two z levels.
  • NONUNIFORM_Zz coordinates are not uniformly spaced. Resample to a uniform grid before uploading; the API stores z_resolution as a scalar.
  • NON_SQUARE_PIXELSdx ≠ dy. Resample to square pixels (typically with gdalwarp -tr <res> <res> or da.rio.reproject(..., resolution=res)).
  • NO_OVERLAP — the netCDF doesn’t intersect the domain extent. Verify the file covers the domain region in the domain’s CRS.

Two additional notes:

  • The signed URL is single-use. If a PUT fails for any reason, start over from step 1 to get a fresh URL.
  • Variable names become band keys verbatim. Pick names you’d be happy to see in subsequent API responses; dot notation (e.g. bulk_density.foliage) is supported and conventional for grouping related bands.