Landscaping
Landscape takeoffs end to end. Measure lawns, beds and trees on aerial imagery, count plants on a landscape plan, read irrigation, and change the results the way the editor does.
View as MarkdownA landscape takeoff is a takeoff on a job that measures landscaping. There are two kinds, the same two the app has:
- Aerial (
"type": "aerial"): areas and trees measured on aerial imagery. Areas are by class (lawn, mulch beds, hedges, gravel, ponds, decks and more) in square feet; trees, shrubs, stumps and fountains are counted. - Plan (
"type": "blueprint"): plants counted on a landscape plan PDF. Each plant has a quantity, plant beds and other ground covers are measured as layers, and irrigation is counted from the irrigation sheets.
Everything here runs the app's own logic as the person who created the API key: the same plan gates, the same billing, the same save rules and the same revision history as the takeoff editor. A takeoff you start here opens in the editor (editor_url), and a takeoff your team draws there can be read and changed here.
Typical aerial flow:
POST /v1/jobs/{job_id}/landscape-takeoffswith{"type": "aerial"}. Keep the takeoffid.POST /v1/landscape-takeoffs/{id}/detectwith the site outline. Keep the runid.- Poll
GET /v1/landscape-runs/{run_id}untilstatusiscompleted. GET /v1/landscape-takeoffs/{id}for the quantities. Add, reclassify or remove items as needed.
Typical plan flow, the same steps as the plan editor:
POST /v1/jobs/{job_id}/landscape-takeoffswith{"type": "blueprint", "file_url": "..."}, then poll the run it returns for the takeoffid.- Say where the plant schedule is:
PUT /v1/landscape-takeoffs/{id}/schedule(or let the first run use the sheet's own). POST .../find-plantsto dot every plant symbol and callout tag on a sheet,POST .../group-plantsto sort them into species and name them,POST .../read-calloutsto read the tags' quantities. OrPOST .../advanced-runfor a close read of a whole sheet.POST .../irrigationwith"apply": trueon irrigation sheets.GET /v1/landscape-takeoffs/{id}for plants and their quantities, plant areas and irrigation. Check a sheet withGET .../sheets/{page}/image.- Rename, re-count, merge or remove plants, and fix the plant schedule. Undo any run with
POST /v1/landscape-runs/{run_id}/undo.
Note
Partner keys add firm_id to name the workspace: in the query string, or in the JSON body on writes. See Partner API.
Classes#
GET /v1/landscape/classes
Permission: Read measurements
The area classes and the tree and object classes aerial measuring uses, with their ids. detected is true for the classes the landscape model finds by itself; the others (road, parking area) are drawn by hand or added through the API.
{
"data": {
"areas": [
{"id": "lawn", "name": "Lawn", "color": "#22d3ee", "unit": "SF", "detected": true},
{"id": "mulch", "name": "Mulch / Beds", "color": "#7c3aed", "unit": "SF", "detected": true}
],
"objects": [
{"id": "deciduous", "name": "Deciduous", "color": "#00e676", "unit": "EA", "tree": true, "detected": true},
{"id": "shrub", "name": "Shrub", "color": "#b2ff59", "unit": "EA", "tree": false, "detected": true}
]
}
}
Takeoffs#
List landscape takeoffs#
GET /v1/landscape-takeoffs
Permission: Read measurements
Most recently changed first. Filters: job_id, type (aerial or blueprint), limit (default 50, at most 200), offset. Each row has id, name, type, job_id, created_at, updated_at; aerial rows add mode (auto or manual), plan rows add editor (plan, or standalone for takeoffs from the older standalone plan tool, which are read only here).
Start a landscape takeoff#
POST /v1/jobs/{job_id}/landscape-takeoffs
Permission: Write measurements
| Name | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | aerial or blueprint. |
name |
string | No | Up to 200 characters. Default "Auto Aerial", "Manual Aerial", or "<job name> Blueprint Takeoff". |
manual |
boolean | No | Aerial only. true makes a Manual Aerial takeoff: drawing only, no AI measuring. Needs Manual Aerial on the plan. |
file_url |
string | Plan: one of the two | A public https:// link that returns the PDF, up to 100 MB. |
file_id |
integer | Plan: one of the two | A PDF already on a job (Photos & files). |
pages |
array | No | Plan only. Which PDF pages to keep, for example [1, 3, 4]. Default all. |
curl -X POST https://trutec.ai/api/v1/jobs/7012/landscape-takeoffs \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"type": "blueprint", "file_url": "https://example.com/plans/L-101.pdf", "pages": [1, 2]}'
An aerial takeoff returns 201 with the takeoff in the shape of Get a takeoff, its quantities still empty.
A plan takeoff returns 202 with a run: the PDF is fetched and checked right away, then stored one sheet per page the way the plan editor stores it, which can take a few minutes for a big plan set. Poll GET /v1/landscape-runs/{run_id}; when it is completed, result.takeoff_id and result.takeoff are the new takeoff. It opens in the plan editor in landscape mode.
Billing. Starting an aerial takeoff is free; its first detection uses a credit (see Measure an aerial takeoff). A plan takeoff uses one takeoff credit once its pages are stored, as starting an AI Blueprint takeoff does in TruTec. A bad link, a file that isn't a readable PDF, or a PDF that can't be stored costs nothing. In a workspace on landscape.trutec.ai, plan takeoffs are free, as they are in that app. Plan takeoffs need Blueprint takeoffs on the plan (Gold and above).
| Status | error |
|---|---|
| 400 | type must be "aerial" or "blueprint", Send file_url or file_id, Page 9 is not in this PDF, Unknown fields |
| 402 | No takeoff credits remaining, an inactive subscription, Manual Aerial is available on Gold and above. |
| 403 | API key lacks measurement write permission, Blueprint Takeoffs require a Gold plan or above. |
| 404 | Job not found, File not found |
| 422 | Could not fetch file_url, PDF too large, Not a readable PDF (not a PDF, or password protected) |
| 429 | Too many runs: the workspace already has 3 landscape runs going |
Get a takeoff's quantities#
GET /v1/landscape-takeoffs/{takeoff_id}
Permission: Read measurements
unit (sf, sy, sm or acres, default sf) sets the unit of the area values; area_sqft is always square feet.
An aerial takeoff:
{
"data": {
"id": 41022,
"name": "Front lawns",
"type": "aerial",
"mode": "auto",
"ai_detection": true,
"job_id": 7012,
"unit": "SF",
"quantities": {
"areas": [
{"class": "lawn", "label": "Lawn", "count": 4, "area_sqft": 18250.4, "area": 18250.4},
{"class": "mulch", "label": "Mulch / Beds", "count": 9, "area_sqft": 3120.0, "area": 3120.0}
],
"objects": [
{"class": "deciduous", "label": "Deciduous", "count": 31},
{"class": "shrub", "label": "Shrub", "count": 112}
],
"total_area_sqft": 21370.4,
"total_area": 21370.4,
"tree_count": 31,
"object_count": 143
},
"editor_url": "https://trutec.ai/measurements/41022/edit"
}
}
A plan takeoff:
{
"data": {
"id": 41090,
"name": "Maple Ridge Blueprint Takeoff",
"type": "blueprint",
"editor": "plan",
"page_count": 4,
"unit": "SF",
"quantities": {
"plants": [
{"id": "cat_lsc_ai_ar_red_maple", "name": "AR — Red Maple", "code": "AR", "common_name": "Red Maple",
"kind": "plant", "unit": "EA", "quantity": 14, "quantity_source": "callouts", "dot_count": 6, "pages": [1]},
{"id": "cat_lsc_co_sod", "name": "Sod", "kind": "callout", "unit": "SF", "quantity": 12400,
"quantity_source": "callouts", "dot_count": 3, "pages": [1, 2]}
],
"plant_total": 14,
"areas": [
{"id": "bed_1", "name": "Shrub beds", "count": 7, "area_sqft": 2210.5, "area": 2210.5,
"quantity_sqft": 2210.5, "quantity": 2210.5, "quantity_source": "drawn"}
],
"lines": [{"id": "cat_fence", "name": "Fence", "linear_feet": 412.0}],
"other_counts": [],
"irrigation": {
"heads_and_valves": [
{"id": "cat_lsc_irr_sym_rotor", "name": "Rotor", "unit": "EA", "quantity": 38, "quantity_source": "dots",
"dot_count": 38, "pages": [3], "plan_quantity": 38}
],
"pipes": [{"id": "cat_lsc_irr_pipe_lateral_1", "name": "Lateral 1\"", "linear_feet": 1840.2}],
"zones": [{"zone": "A1", "valve_size": "1\"", "gpm": 14, "heads": {"Rotor": 6}, "lateral_lf": {"1\"": 210}}]
}
},
"schedule": {"status": "boxed", "page": 1, "box": [2210, 140, 3020, 980], "rows": [], "rows_ignored": false},
"editor_url": "https://trutec.ai/measurements/blueprint?job_id=7012&measurement_id=41090&ai=1&mode=landscape"
}
}
| Field | Description |
|---|---|
plants[].quantity |
What the takeoff counts for the plant: the amounts its callouts state ("(4) AR" = 4), else one per dot; or a total someone typed in. |
plants[].quantity_source |
callouts, dots or typed. |
plants[].kind |
plant (plant symbols), callout (an item read from callout text, which can be SF or LF), callout_tag (callout tags not read yet), other. |
plants[].code, common_name |
Split from names like "AR — Red Maple". |
areas[] |
Layers drawn on the plan (plant beds, sod, mulch), with quantity_sqft = a typed total when there is one. |
irrigation |
Heads, valves and equipment counted by the irrigation tool (plan_quantity = what the sheet's legend says), pipe linear feet, and valve zones. |
Items#
List items#
GET /v1/landscape-takeoffs/{takeoff_id}/items
Permission: Read measurements
Paged with limit (default 50, at most 200) and offset.
- Aerial: each area (
id,class,label,kind: "area",area_sqft,ringsof [latitude, longitude]) and each tree or object (kind: "object",lat,lng). Filter withclass. - Plan: each plant dot (
id,plant_id,plant_name,kind,page,x,yin sheet pixels at 144 dpi,quantity,callout_quantitywhen a callout states one). Filter withplant_id,kindorpage.
Add areas and trees (aerial)#
POST /v1/landscape-takeoffs/{takeoff_id}/items
Permission: Write measurements
items is a list of up to 200: {"class": "mulch", "polygon": [[lat, lng], ...]} for an area class, {"class": "palm", "point": [lat, lng]} for a tree or object class, each with an optional phase ("Phase 2"; default Phase 1). Coordinates are [latitude, longitude]. Returns 201 with the items added and the takeoff's new quantities. Plant dots on plans are placed in the editor.
Change an item#
PATCH /v1/landscape-takeoffs/{takeoff_id}/items/{item_id}
Permission: Write measurements
- Aerial:
class(an area stays an area class, a tree or object stays an object class) and/orphase(nullclears it). - Plan:
plant_idmoves the dot to another plant;quantitysets the amount the dot stands for, as a callout does ("(4) AR");nullclears it.
Totals follow the change.
Remove an item#
DELETE /v1/landscape-takeoffs/{takeoff_id}/items/{item_id}
Permission: Write measurements and Delete records
Returns 204.
Every change is saved the way the editor saves: the takeoff's other items are untouched, its totals follow, and the version it replaced is kept in the takeoff's history in TruTec. If someone saves the takeoff in the editor while the API is changing it, the API answers 409 (Takeoff changed): read it again and retry. A takeoff sent to TruTec for cleanup can't be changed until the cleanup is done (403).
Measure an aerial takeoff#
POST /v1/landscape-takeoffs/{takeoff_id}/detect
Permission: Write measurements
Runs the landscape model inside the outline you send and puts the result on the takeoff, the way Run does in the editor.
| Name | Type | Required | Description |
|---|---|---|---|
polygon |
array | One of three | One outline: [latitude, longitude] points. Find parcels gives a parcel's outline. |
polygons |
array | One of three | Up to 20 outlines. |
bounds |
object | One of three | {north, south, east, west}. Up to 1,000 acres. |
area_classes |
array | No | Only these area classes. |
include_areas |
boolean | No | Default true. |
include_objects |
boolean | No | Trees, shrubs and objects. Default true. |
replace_existing |
boolean | No | true: replace the classes the model found (the editor's Replace). false (default): add alongside what is there. |
curl -X POST https://trutec.ai/api/v1/landscape-takeoffs/41022/detect \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"polygon": [[30.40112, -97.72231], [30.40112, -97.72102], [30.39991, -97.72102], [30.39991, -97.72231]]}'
Returns 202 with the run (id, status: "pending") and poll_url. It takes 30 seconds to a few minutes.
As in the editor: new areas never cover road or parking area already on the takeoff; with "add", they are also clipped to everything already there and trees within a meter of one already counted are skipped. The run is refused before it starts when the takeoff is Manual Aerial, when AI auto-measure is turned off for the person who created the key, or when the outline is too far from the takeoff's site.
Billing. A takeoff's first model run uses one takeoff credit from the plan, as in the editor. Runs on the same takeoff after that are free, so measure more of a site by running again on its takeoff, not by starting a new one. A refused run costs nothing. A workspace can have 3 landscape runs going at once.
Get a run#
GET /v1/landscape-runs/{run_id}
Permission: Read measurements
status is pending, processing, completed or failed. A completed detection's result has added (areas, objects), removed (items a Replace took off), duplicates_skipped and the takeoff's new quantities. A failed run has error.
Plants (plan takeoffs)#
Change a plant#
PATCH /v1/landscape-takeoffs/{takeoff_id}/plants/{plant_id}
Permission: Write measurements
| Name | Type | Description |
|---|---|---|
name |
string | Up to 120 characters, for example "AR — Red Maple 2.5\" cal.". The editor's built-in items keep their names (422). |
quantity |
number or null | A typed total: it stays until cleared, as in the editor. null goes back to the counted total. |
Returns 200 with the plant.
Merge plants#
POST /v1/landscape-takeoffs/{takeoff_id}/plants/{plant_id}/merge with {"into_plant_id": "..."}
Permission: Write measurements
Moves every dot of plant_id to into_plant_id and removes plant_id: for a species the grouping split in two. Both must count in the same unit. The kept plant's total follows its dots unless it has a typed total.
Remove a plant#
DELETE /v1/landscape-takeoffs/{takeoff_id}/plants/{plant_id}
Permission: Write measurements and Delete records
Removes the plant and all its dots. Returns 204.
Plant schedule#
GET /v1/landscape-takeoffs/{takeoff_id}/schedule and PUT the same path.
Permission: Read measurements to read; Write measurements to change
The plant schedule is the table of plant codes and names on the plan. Grouping plants in the editor reads it to name each group. status is boxed (an area of a sheet), on_page, no_schedule (the plan has none) or not_set; rows are the rows read from it (row, code, name).
PUT takes one of:
{"page": 2}or{"page": 2, "box": [x0, y0, x1, y1]}: where the schedule is (box in sheet pixels at 144 dpi; find text "PLANT SCHEDULE" gives a position). A new place starts without rows.{"no_schedule": true}: the plan has no schedule.{"rows": [{"row": 3, "code": "LAV", "name": "Lavender"}, {"row": 5, "remove": true}]}: fix rows already read.409 No schedule rows yetuntil the editor has read them.
Counting plants on a plan#
These runs do what the plan editor's plant tools do, on one sheet at a time. Each starts a run (202); poll GET /v1/landscape-runs/{run_id}. They are not billed (only starting a plan takeoff uses a credit) and need Blueprint takeoffs on the plan.
| Endpoint | Editor tool | What it does |
|---|---|---|
POST .../find-plants |
Find plants / Find callouts | A dot on every plant symbol (item "Plants") and callout tag (item "Plant callouts"). lanes: both, plants or callouts. region: an outline to scan. |
POST .../group-plants |
Group plants, then Name classes | Sorts the sheet's unsorted plant dots into species ("Type 1", "Type 2"…), then names each from the plant schedule ("AR — Red Maple"). species_count fixes how many species. name_groups: false stops after grouping. |
POST .../name-plants |
Name classes | Names the groups still called "Type N". |
POST .../read-callouts |
Read callouts | Reads each unread callout tag and moves it to its item (cat_lsc_co_*) with the amount it states: "(4) AR" counts 4, "1,200 SF SOD" counts 1,200 SF. |
POST .../advanced-run |
Advanced run | A close read of the sheet or a region, tile by tile. mode: "plants" reads the drawn symbols with name, code and condition; mode: "text" reads plant labels written as text. Takes a few minutes. |
POST .../read-schedule |
(the schedule read naming uses) | The schedule's plant codes, names and stated quantities. Changes nothing. |
curl -X POST https://trutec.ai/api/v1/landscape-takeoffs/41090/find-plants \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"page": 1}'
A completed run's result:
{
"takeoff_id": 41090,
"page": 1,
"summary": {"plants_found": 212, "callouts_found": 48, "already_marked": 0},
"items": [
{"id": "cat_lsc_plant", "added": 212},
{"id": "cat_lsc_callout", "added": 48}
],
"applied": true
}
Runs only add, and can be undone#
A run never changes what is already on the plan unless you send "replace_existing": true:
- find-plants only adds: a symbol or tag that already has a dot is skipped, so running it again fills in what the last run missed.
- group-plants sorts only the dots still in "Plants". With
replace_existingit re-sorts every plant dot on the sheet, as the editor's Group does. - name-plants names only groups still called "Type N". With
replace_existingit renames every group on the sheet. - read-callouts reads only tags not read yet and removes nothing: tags the reader says are not callouts, or are duplicates, come back in
flaggedfor you to check. Withreplace_existingit re-reads every tag, removes the false ones and clears the items' typed totals, as the editor does. - advanced-run only adds. It adds the clear proposals; proposals that are unclear, possible duplicates, already dotted or existing plants are left out unless you send
include_flagged, and then they are marked for review in the editor. - irrigation with
applyrefuses a sheet that already has reader results unless you sendreplace_existing(it then replaces the reader's own earlier results; anything drawn by hand stays).
Every applied run can be undone with POST /v1/landscape-runs/{run_id}/undo. Undo removes the dots the run added, moves back the dots it moved, and restores the names, quantities and settings it changed. Anything that was changed again since then (by the editor or another run) is left alone.
To review before anything changes, send "apply": false. The run's result then shows what it would do (items; every proposals entry for an Advanced run, with selected, needs_review, already_on_plan and possible_duplicate). Apply it with POST /v1/landscape-runs/{run_id}/apply. For an Advanced run, proposal_ids picks which proposals to add. Anything changed on the takeoff since the run is skipped and counted in conflicts.
Check a sheet#
GET /v1/landscape-takeoffs/{takeoff_id}/sheets/{page}/image returns the sheet as a JPEG, with every dot and drawn area in its item's color (style=annotated, the default) or plain. crop=left,top,right,bottom (fractions of the sheet) zooms in. POST .../sheets/{page}/image-link returns a link that opens the same picture for 24 hours without a key; anyone with the link can see the sheet.
Server renders#
The editor renders sheets in your browser. These runs render them on TruTec's servers at the same scales and send the models the same crops, tiles and schedule pictures. The two renderers are different programs, so a server render can differ slightly from the editor's, for example in line weights and text smoothing. Results can therefore differ a little from a run in the editor; check them with the sheet picture or in the editor. Sheets stored as pictures, not PDF pages, can only be read in the editor.
Irrigation#
POST /v1/landscape-takeoffs/{takeoff_id}/irrigation with {"page": 3}
Permission: Read measurements
Reads one irrigation sheet with the editor's irrigation tool: heads, valves and equipment by type with counts, pipe linear feet by class and size, and valve zones, each checked against the sheet's own legend (plan_quantity, plan_linear_feet, check). mode is all (default), symbols or pipes. Returns 202 with a run; poll GET /v1/landscape-runs/{run_id}. Not billed.
By default nothing on the takeoff changes. Send "apply": true (needs Write measurements) to put the result on the takeoff the way Find irrigation does in the editor:
- a count item per legend row (
cat_lsc_irr_sym_*), one dot per head, valve or piece of equipment; - the valve tags, with zone, valve size and GPM, which feed the zone quantities;
- a line item per pipe type and size (
cat_lsc_irr_pipe_*), its footage measured at the sheet's scale; - the legend's totals, for the editor's legend check;
- the sheet's printed scale, when the sheet has no scale yet.
If the sheet already has reader results, send "replace_existing": true to replace them; otherwise it answers 409. A read run without apply can also be applied later with POST /v1/landscape-runs/{run_id}/apply.
{
"data": {
"id": "5e2b1c9a-7d3f-4a10-8b6e-0c4d9f2a1e77",
"type": "irrigation",
"status": "completed",
"takeoff_id": 41090,
"result": {
"page": 3,
"sheet_kind": "plan",
"scale": {"ft_per_in": 20, "source": "printed"},
"symbols": [{"name": "Rotor", "kind": "head", "count": 38, "plan_quantity": 38, "check": "ok"}],
"pipes": [{"name": "Lateral", "kind": "lateral", "size": "1\"", "linear_feet": 1840.2, "plan_linear_feet": 1900, "runs": 41, "check": "ok"}],
"pipe_classes": [{"name": "Lateral", "kind": "lateral", "plan_linear_feet": 1900, "found_linear_feet": 1840.2, "status": "ok"}],
"zones": [{"zone": "A1", "valve_size": "1\"", "gpm": 14, "heads": {"Rotor": 6}}],
"notes": [],
"saved_on_takeoff": false
}
}
}
A sheet that is a scan, with no drawing lines, may not be readable: the run then fails with a message saying so.
Find text on a plan#
GET /v1/landscape-takeoffs/{takeoff_id}/sheet-text
Permission: Read measurements
The editor's Find text. q with no page searches the text of every sheet (the first 60). page reads one sheet: mode=text (default) is the PDF's own text; mode=full also reads sheets that have none (scans, outlined text), which can take several calls: repeat while complete is false. Each match has page, text, and x, y (where the text starts, in sheet pixels at 144 dpi).
curl "https://trutec.ai/api/v1/landscape-takeoffs/41090/sheet-text?q=plant%20schedule" \
-H "X-API-Key: sk_your_api_key_here"
What stays in the editor#
Drawing by hand on the plan sheet: plant beds and other layers, painting plants into classes, and dots placed one by one. Correcting the schedule's row boxes also happens there; through the API you can fix a row's code and name. On aerial takeoffs the editor's road and parking-stall lanes stay in the editor; measure pavement with Measure a site.