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 flow is three calls: create the inventory (and get a signed upload URL), PUT your file to that URL, then poll until it’s processed.

  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 columns (in the domain CRS) plus whatever 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 in step 1.

The whole flow in one script:

Upload a tree inventory (create + PUT + poll)
import time
import requests
API_KEY = "my-api-key"
DOMAIN_ID = "your-domain-id"
CSV_PATH = "trees.csv"
BASE = "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app"
HEADERS = {"api-key": API_KEY}
# 1. Create the inventory + get a signed upload URL. `columns` maps your
# file's column names onto the canonical fields.
created = requests.post(
f"{BASE}/domains/{DOMAIN_ID}/inventories/tree/upload",
headers=HEADERS,
json={
"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",
},
},
).json()
inventory_id = created["inventory"]["id"]
upload = created["upload"]
# 2. PUT the file to the signed URL (no api-key — this goes to GCS).
with open(CSV_PATH, "rb") as f:
put = requests.put(
upload["url"],
data=f,
headers={
"Content-Type": upload["content_type"],
"x-goog-content-length-range": f"0,{upload['max_size_bytes']}",
},
)
put.raise_for_status()
# 3. Poll the inventory until it is processed.
while True:
inv = requests.get(
f"{BASE}/domains/{DOMAIN_ID}/inventories/{inventory_id}", headers=HEADERS
).json()
if inv["status"] in ("completed", "failed"):
break
time.sleep(5)
print(inventory_id, inv["status"]) # -> <inventory id> completed

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 (here tree_xx, dbh_cmdbh, and so on).

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 the content_type and max_size_bytes you must echo on the upload. Set it 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 (x, y, dbh, height, crown_ratio, fia_species_code, fia_status_code) — your mapped fields, 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.
  • 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.