Skip to content

Modify an inventory

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

You don’t have to regenerate a tree inventory to edit it. POST .../modifications applies rules to an inventory’s current stored trees and writes the result back under the same id — no re-running the point process, no new resource. Use it to remove trees by attribute, by a multi-attribute expression, or by location, or to scale a numeric attribute, once the inventory already exists.

The rules you submit are appended to the inventory’s list and applied on top of whatever it already holds. To instead apply the same rules as you first build an inventory, see Remove trees with features — same rule shape, applied at create time. For the concepts — conditions, actions, the inventory-only remove action and expression conditions, and the create-time-vs-in-place distinction — see About modifications.

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

  2. A domain in a projected CRS: your-domain-id. See Create a domain.

  3. A completed tree inventory: your-inventory-id. See Generate a tree inventory from TreeMap.

  4. Optional — a completed road and water feature in the same domain, for the spatial recipes. See Create road and water features from OpenStreetMap and record your-road-feature-id and your-water-feature-id.

Removing small trees in one script:

Modify an inventory (POST + poll)
import time
import requests
API_KEY = "my-api-key"
DOMAIN_ID = "your-domain-id"
# A completed tree inventory (e.g. from inventories/tree/pim).
INVENTORY_ID = "your-inventory-id"
BASE = "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app"
HEADERS = {"api-key": API_KEY}
def poll(inventory_id: str) -> dict:
while True:
r = requests.get(
f"{BASE}/domains/{DOMAIN_ID}/inventories/{inventory_id}",
headers=HEADERS,
).json()
if r["status"] in ("completed", "failed"):
return r
time.sleep(5)
def rows(inventory_id: str) -> int:
return requests.get(
f"{BASE}/domains/{DOMAIN_ID}/inventories/{inventory_id}/data/metadata",
headers=HEADERS,
).json()["total_rows"]
before = rows(INVENTORY_ID)
# Remove every tree under 5 cm DBH. `remove` must be the only action in its rule.
inventory = requests.post(
f"{BASE}/domains/{DOMAIN_ID}/inventories/{INVENTORY_ID}/modifications",
headers=HEADERS,
json={
"modifications": [
{
"conditions": [
{"attribute": "dbh", "operator": "lt", "value": 5}
],
"actions": [{"modifier": "remove"}],
}
]
},
).json()
print(inventory["status"]) # -> pending
inventory = poll(INVENTORY_ID)
print(inventory["status"], len(inventory["modifications"])) # -> completed 1
print(before, "->", rows(INVENTORY_ID)) # fewer trees

The rest of this page walks the individual recipes. The inventory below starts with 81,908 trees.

Match trees by a single attribute, delete them. Here every tree under 5 cm DBH: an attribute condition plus a remove action. remove must be the only action in its rule — you can’t both drop a tree and edit it.

POST inventories/{id}/modifications
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/your-inventory-id/modifications' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"modifications": [
{
"conditions": [
{ "attribute": "dbh", "operator": "lt", "value": 5 }
],
"actions": [
{ "modifier": "remove" }
]
}
]
}'

The 200 comes back pending with a freshly rotated checksum. The modifications array is still empty here; the submitted rule lands there once the job completes (see Poll to completion). This inventory has a lot of saplings — the rule removes 31,825 trees (81,908 → 50,083).

An expression condition tests several attributes at once — something a single attribute condition can’t do, and something only inventories support. Here, trees that are both thin and short: dbh < 5 and height < 2.

Remove trees by a compound expression
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/your-inventory-id/modifications' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"modifications": [
{
"conditions": [
{ "expression": "dbh < 5 and height < 2" }
],
"actions": [
{ "modifier": "remove" }
]
}
]
}'

The and inside the expression is the same ANDing as stacking conditions — it narrows. Where the dbh < 5 rule above removed 31,825 trees, adding height < 2 cuts that to just 8 (81,908 → 81,900): almost every thin stem is still taller than 2 m. Expressions accept dbh, height, and crown_ratio, always in native units (cm, m, fraction).

To drop trees wherever they fall near a road or near water, use two rules — one per feature. Each is a within test against the feature, widened by a buffer_m, with a remove action.

Remove trees within 5 m of roads or water
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/your-inventory-id/modifications' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"modifications": [
{
"conditions": [
{ "source": "feature", "operator": "within", "feature_id": "your-road-feature-id", "buffer_m": 5 }
],
"actions": [
{ "modifier": "remove" }
]
},
{
"conditions": [
{ "source": "feature", "operator": "within", "feature_id": "your-water-feature-id", "buffer_m": 5 }
],
"actions": [
{ "modifier": "remove" }
]
}
]
}'

Together the two rules remove 3,613 trees (81,908 → 78,295) — those in the road and water corridors plus the 5 m tolerance.

Not every edit removes trees. A non-remove action edits an attribute on the matching trees and leaves the tree count unchanged. Here, tall trees (height > 25 m) have their dbh scaled to 90% — a multiply action gated by a condition.

Scale DBH of tall trees to 90%
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/your-inventory-id/modifications' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"modifications": [
{
"conditions": [
{ "attribute": "height", "operator": "gt", "value": 25 }
],
"actions": [
{ "attribute": "dbh", "modifier": "multiply", "value": 0.9 }
]
}
]
}'

The tree count is unchanged (81,908 → 81,908) — this rewrites dbh on the matching trees rather than deleting anything. multiply, divide, add, and subtract all edit in place; only remove deletes.

The checksum rotates the instant you POST, but the trees are rewritten and the modifications list is updated only when the job completes. Poll the inventory:

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'

Status walks pending → running → completed. When it lands, the submitted rule appears in modifications:

{
"id": "your-inventory-id",
"domain_id": "your-domain-id",
"type": "tree",
"name": "TreeMap tree inventory",
"description": "",
"status": "completed",
"progress": {
"percent": 100,
"message": "Complete"
},
"created_on": "2026-07-09T19:28:23.911399Z",
"modified_on": "2026-07-09T19:29:35.631889Z",
"checksum": "d58341841ef34b2589132168caf44ae4",
"source": {
"name": "pim",
"point_process": "inhomogeneous_poisson",
"seed": 42,
"source_pim_grid_id": "your-pim-grid-id",
"source_pim_grid_checksum": "f3d2608693f8440b9ee4234d11c37e72"
},
"modifications": [
{
"conditions": [
{
"attribute": "dbh",
"operator": "lt",
"value": 5,
"unit": null
}
],
"actions": [
{
"modifier": "remove"
}
]
}
],
"treatments": [],
"columns": [
{
"key": "x",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 717157.6880869393,
"max": 718695.9982627264,
"mean": 718365.8836694711,
"std": 252.44197956672994
}
},
{
"key": "y",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 5326540.000592403,
"max": 5328249.990101517,
"mean": 5327485.855720593,
"std": 559.5766552337327
}
},
{
"key": "fia_species_code",
"type": "categorical",
"unit": null,
"summary": {
"type": "categorical",
"count": 50083,
"null_count": 0,
"unique_count": 14
}
},
{
"key": "fia_status_code",
"type": "categorical",
"unit": null,
"summary": {
"type": "categorical",
"count": 50083,
"null_count": 0,
"unique_count": 2
}
},
{
"key": "dbh",
"type": "continuous",
"unit": "cm",
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 5.08,
"max": 72.898,
"mean": 14.479704051274885,
"std": 9.153580443050032
}
},
{
"key": "height",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 2.7432,
"max": 41.452799999999996,
"mean": 10.530476976219477,
"std": 5.662240505216165
}
},
{
"key": "crown_ratio",
"type": "continuous",
"unit": null,
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 0.0,
"max": 0.97,
"mean": 0.5807293892139049,
"std": 0.22081371525657967
}
}
],
"forestry_metrics": {
"type": "tree",
"tree_count": 50083,
"basal_area_per_area": 9.375725095011157,
"tree_density": 37.79318312050426,
"quadratic_mean_diameter": 6.744224242220545,
"dominant_species_groups": [
{
"spgrpcd": 2,
"name": "Douglas-fir",
"basal_area_share": 0.45768974649180144
},
{
"spgrpcd": 4,
"name": "Pine",
"basal_area_share": 0.20342386693258668
},
{
"spgrpcd": 1,
"name": "Cedar/larch",
"basal_area_share": 0.15923405289259357
},
{
"spgrpcd": 3,
"name": "True fir/hemlock",
"basal_area_share": 0.10696149796976803
},
{
"spgrpcd": 5,
"name": "Spruce",
"basal_area_share": 0.036172433028793964
}
]
},
"georeference": {
"crs": "EPSG:32611",
"bounds": [717140.0, 5326524.0, 718696.0, 5328250.0]
},
"error": null,
"tags": []
}

The rotated checksum is the staleness signal for anything derived from this inventory — a voxelization, an export. To keep the pre-edit inventory around, branch it first.

In-place modification edits the inventory under its own id — the pre-edit trees are gone. When you’d rather keep the original and explore a variant stand, duplicate first, then modify the copy.

POST .../duplicate makes a byte-for-byte clone: it copies the stored parquet (it does not re-run the point process), carrying the inventory’s source, modifications, treatments, and checksum over verbatim.

POST inventories/{id}/duplicate
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/your-inventory-id/duplicate' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Inventory with small trees removed (scenario B)",
"tags": ["scenario-b"]
}'

The 201 mints a new inventory id — record it: id-of-the-duplicated-inventory. The copy starts pending while the background copy runs, but its checksum already equals the source’s and its modifications are carried over intact. Poll it to completed:

{
"id": "id-of-the-duplicated-inventory",
"domain_id": "your-domain-id",
"type": "tree",
"name": "Inventory with small trees removed (scenario B)",
"description": "",
"status": "completed",
"progress": null,
"created_on": "2026-07-09T19:35:38.055373Z",
"modified_on": "2026-07-09T19:35:38.358291Z",
"checksum": "d58341841ef34b2589132168caf44ae4",
"source": {
"name": "pim",
"point_process": "inhomogeneous_poisson",
"seed": 42,
"source_pim_grid_id": "your-pim-grid-id",
"source_pim_grid_checksum": "f3d2608693f8440b9ee4234d11c37e72"
},
"modifications": [
{
"conditions": [
{
"attribute": "dbh",
"operator": "lt",
"value": 5,
"unit": null
}
],
"actions": [
{
"modifier": "remove"
}
]
}
],
"treatments": [],
"columns": [
{
"key": "x",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 717157.6880869393,
"max": 718695.9982627264,
"mean": 718365.8836694711,
"std": 252.44197956672994
}
},
{
"key": "y",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 5326540.000592403,
"max": 5328249.990101517,
"mean": 5327485.855720593,
"std": 559.5766552337327
}
},
{
"key": "fia_species_code",
"type": "categorical",
"unit": null,
"summary": {
"type": "categorical",
"count": 50083,
"null_count": 0,
"unique_count": 14
}
},
{
"key": "fia_status_code",
"type": "categorical",
"unit": null,
"summary": {
"type": "categorical",
"count": 50083,
"null_count": 0,
"unique_count": 2
}
},
{
"key": "dbh",
"type": "continuous",
"unit": "cm",
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 5.08,
"max": 72.898,
"mean": 14.479704051274885,
"std": 9.153580443050032
}
},
{
"key": "height",
"type": "continuous",
"unit": "m",
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 2.7432,
"max": 41.452799999999996,
"mean": 10.530476976219477,
"std": 5.662240505216165
}
},
{
"key": "crown_ratio",
"type": "continuous",
"unit": null,
"summary": {
"type": "continuous",
"count": 50083,
"null_count": 0,
"min": 0.0,
"max": 0.97,
"mean": 0.5807293892139049,
"std": 0.22081371525657967
}
}
],
"forestry_metrics": {
"type": "tree",
"tree_count": 50083,
"basal_area_per_area": 9.375725095011157,
"tree_density": 37.79318312050426,
"quadratic_mean_diameter": 6.744224242220545,
"dominant_species_groups": [
{
"spgrpcd": 2,
"name": "Douglas-fir",
"basal_area_share": 0.45768974649180144
},
{
"spgrpcd": 4,
"name": "Pine",
"basal_area_share": 0.20342386693258668
},
{
"spgrpcd": 1,
"name": "Cedar/larch",
"basal_area_share": 0.15923405289259357
},
{
"spgrpcd": 3,
"name": "True fir/hemlock",
"basal_area_share": 0.10696149796976803
},
{
"spgrpcd": 5,
"name": "Spruce",
"basal_area_share": 0.036172433028793964
}
]
},
"georeference": {
"crs": "EPSG:32611",
"bounds": [717140.0, 5326524.0, 718696.0, 5328250.0]
},
"error": null,
"tags": ["scenario-b"]
}

Same checksum, same source, same modifications — only the id, name, and tags differ. Now apply any recipe above to id-of-the-duplicated-inventory and only the copy changes; the original keeps the checksum shown here. Two things to watch:

  • Using the copy before it’s completed. The clone is pending until the background copy finishes; its /data endpoints 422 until then. Poll first.

    {
    "detail": "inventories/id-of-the-duplicated-inventory status is 'pending', expected 'completed'."
    }
  • Duplicating a non-completed inventory. The source must be completed — there’s nothing stable to copy until it finishes building.

    {
    "detail": "inventories/your-inventory-id status is 'pending', expected 'completed'."
    }

If processing fails, the inventory lands in status: failed with an error message, and its stored trees are left unchanged — a failed in-place edit never partially rewrites the inventory. The rules you submitted stay queued, so once you’ve addressed the cause you can POST the same (or a corrected) modification again to retry.

  • One rule for two features (AND vs OR). A road condition and a water condition in one rule match only trees inside both — almost none. Use one rule per feature, as in Remove trees under roads or water.

  • remove combined with another action. remove must be the sole action in its rule; pairing it with an attribute edit is a 422. To edit some trees and remove others, use two separate rules.

    {
    "detail": [
    {
    "type": "value_error",
    "loc": ["body", "modifications", 0],
    "msg": "Value error, RemoveAction must be the sole action if present",
    "input": {
    "conditions": [
    {
    "attribute": "dbh",
    "operator": "lt",
    "value": 5
    }
    ],
    "actions": [
    {
    "modifier": "remove"
    },
    {
    "attribute": "height",
    "modifier": "multiply",
    "value": 0.9
    }
    ]
    },
    "ctx": {
    "error": {}
    }
    }
    ]
    }
  • A road feature with no buffer_m. Road features are thin. Trees are points, and a point rarely lands inside a bare road polygon, so a within test with no buffer removes almost nothing. buffer_m gives the feature the width to actually catch trees — set it to the corridor you mean.