Skip to content

Upload your own tree inventory

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

Already have a tree list — a field cruise, a plot inventory, output from another model? Upload it directly instead of generating one.

The two paths below do the same thing, but they don’t have the same number of steps. With the Python SDK it’s one call plus a waitcreate_tree_inventory_from_file creates the inventory and PUTs your file for you. At the raw HTTP level it’s three calls — create the inventory (and get a signed upload URL), PUT your file to that URL, then poll until it’s processed. Pick the path that matches how you’re working; you don’t need both.

  1. An API key: my-api-key.

  2. A domain in a projected CRS: your-domain-id. Your tree coordinates must be in this domain’s CRS (meters).

  3. A tree file — CSV, GeoJSON, or GeoPackage. A CSV needs at least x, y, and height columns (x/y in the domain CRS) plus whatever other attributes you have. Here’s the example used below:

    trees.csv
    tree_x,tree_y,dbh_cm,ht_m,cr,spcd
    720400.0,5190000.0,25.4,14.2,0.45,122
    720520.5,5190120.0,30.1,16.8,0.50,202
    720650.0,5190050.5,12.7,8.1,0.35,122
    720800.0,5190300.0,41.9,21.0,0.60,93
    720950.5,5190450.0,18.3,10.5,0.40,15
    721100.0,5190200.0,52.6,24.7,0.65,202

    The column names are arbitrary — you map them to the canonical fields with columns (SDK) or the columns object in the request body (HTTP).

create_tree_inventory_from_file folds the create and the PUT into a single call; inventory.wait() blocks until processing finishes. That’s the whole flow:

Upload a tree inventory (create + PUT + poll)
import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
domain = ff.Domain.from_id("your-domain-id")
# create_tree_inventory_from_file creates the inventory and PUTs your file to
# the signed URL in one call. `columns` maps your file's column names onto the
# canonical fields; `format` is inferred from the file extension.
inventory = ff.inventories.create_tree_inventory_from_file(
domain,
"trees.csv",
columns={
"x": "tree_x",
"y": "tree_y",
"dbh": "dbh_cm",
"height": "ht_m",
"crown_ratio": "cr",
"fia_species_code": "spcd",
},
name="Field-cruise tree inventory",
)
# Block until processing finishes. The completed inventory carries the
# canonical columns you mapped, normalized.
inventory.wait()
print(inventory.id, inventory.status)

The columns mapping is the one thing to get right: each key is a canonical field, and its value is the column name in your file (here tree_xx, dbh_cmdbh, and so on). The completed inventory carries those canonical columns (x, y, dbh, height, crown_ratio, fia_species_code here), normalized. Add fia_status_code too if your file has a tree-status column.

The same upload as three raw calls — this is what the SDK does for you above. Reach for it when you’re not in Python, or when you want to see the signed-URL handshake.

Step 1 — Create the upload and map columns

Section titled “Step 1 — Create the upload and map columns”

POST to /inventories/tree/upload with the format and a columns mapping. Each key is a canonical field; its value is the column name in your file.

POST inventories/tree/upload
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/tree/upload' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Field-cruise tree inventory",
"format": "csv",
"columns": {
"x": "tree_x",
"y": "tree_y",
"dbh": "dbh_cm",
"height": "ht_m",
"crown_ratio": "cr",
"fia_species_code": "spcd"
}
}'

The response nests two objects:

  • inventory — the new resource (status: "pending"). Record its id: your-inventory-id.
  • upload — the signed PUT URL plus upload.headers, the exact request headers (Content-Type and x-goog-content-length-range) to replay on the upload. Set the URL here: paste-signed-url-from-201-response.

Upload the file directly to GCS — not through the FastFuels API, so send no api-key. The Content-Type and x-goog-content-length-range headers must match the upload block from step 1.

PUT trees.csv to the signed URL
# PUT the CSV directly to the signed URL returned in step 1.
# This goes to Google Cloud Storage, not the FastFuels API — send no api-key.
# Both headers must match the values from the create response.
curl -X 'PUT' \
'paste-signed-url-from-201-response' \
-H 'Content-Type: text/csv' \
-H 'x-goog-content-length-range: 0,524288000' \
--data-binary @trees.csv

A successful upload returns 200 OK with an empty body and triggers processing.

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

The processed inventory carries the canonical columns you mapped, normalized.

  • Coordinates in the wrong CRS. x/y must be in the domain’s projected CRS (meters). Lat/lon or a different projection places trees outside the domain. Reproject before uploading.
  • Unmapped columns. Only the fields you list in columns are read. A typo in a source column name (e.g. dbh_cm vs dbh_CM) silently drops that attribute — check the completed inventory’s columns.
  • Signed URL is single-use and expires. expires_at is one hour out. If the PUT fails or the URL expires, start over from step 1 for a fresh URL. (The SDK handles this for you.)
  • Mismatched upload headers. GCS rejects the PUT with 400/403 if Content-Type or x-goog-content-length-range don’t match what step 1 returned. Echo them verbatim. (Again, only a concern on the raw HTTP path.)