Blueprint takeoffs
Measure a paving job from its PDF plan set — store the sheets, set their scale, run the takeoff models on the site plan, clean up the shapes and read the quantities.
View as MarkdownA blueprint takeoff measures a job from its plan sheets instead of aerial imagery. The API does what a paver does in TruTec's AI plan editor, with the same rules: start the takeoff from a PDF, check each sheet's scale, run the models on the site plan, fix what they found, and read the quantities your estimate uses. The takeoff opens in the plan editor at any point (editor_url), so your team can pick up where the API left off.
Typical flow:
POST /v1/jobs/{job_id}/blueprint-takeoffswith the PDF. Poll the import run it returns.GET /v1/blueprint-takeoffs/{takeoff_id}to see the sheets, their titles and scales.GET /v1/blueprint-takeoffs/{takeoff_id}/sheets/{page}/imageto look at a sheet. Set or confirm its scale withPUT .../sheets/{page}/scale.POST .../sheets/{page}/ai-takeoffwith the part of the sheet that holds the site plan. Poll the run.- Fix the results:
PATCH .../shapes/{shape_id}to move a shape to the right item,POST .../shapesto draw what's missing,POST .../shapes/removeto erase what's wrong. - Read the totals from
GET /v1/blueprint-takeoffs/{takeoff_id}, or the estimate's view fromGET /v1/measurements/{takeoff_id}/items.
Note
Partner keys add firm_id to name the workspace: in the query string, or in the JSON body on writes. See Partner API.
Plan and billing#
Blueprint takeoffs need a plan with blueprint takeoffs (Gold and up). Silver workspaces with an active subscription can use them too, as in the app.
Starting a takeoff uses one takeoff credit, exactly like starting an AI Blueprint takeoff in TruTec. The file is fetched and checked first, so a bad link or an unreadable PDF never uses a credit. Everything after that is free: adding sheets, reading scales and titles, model runs, fills, matches, counts and edits. A workspace with no credits left gets 402 with code limit_reached; an inactive subscription gets 402 with code subscription_inactive.
Calling the start endpoint twice starts, and bills, two takeoffs. To add pages to a takeoff, use POST /v1/blueprint-takeoffs/{takeoff_id}/sheets.
Sheets and coordinates#
Every page you import becomes a sheet, numbered from 1 in import order. Positions on a sheet are sheet pixels: the PDF page at 144 dpi (PDF points x 2), origin at the top-left corner, written [x, y]. A 36 x 24 inch sheet is 5,184 x 3,456 sheet pixels.
Endpoints that take positions also accept "coordinates": "fraction": fractions of the sheet's width and height from 0 to 1. Fractions are the easiest way to point at something you see in the sheet picture.
Scale is per sheet. TruTec reads the printed scale (and the scale bar, which catches half-size prints) when a sheet is stored. A scale read off the sheet is not verified; one you set is. Until a sheet has a scale, counts are right but areas and lengths read 0, and they fill in as soon as you set it. If the workspace requires a verified scale before drawing (Settings, Measurement preferences), drawing and model runs on that sheet answer 409 with code scale_not_verified until you set or confirm it.
Items and shapes#
A takeoff's items are what quantities are kept by:
| Kind | Id | Measured in | Examples |
|---|---|---|---|
surface |
layer:<id> |
SF | layer:asphalt (the paved "Area"), layer:concrete, layer:sidewalk, surface types the models split out |
area |
cat_<id> |
SF | cat_crosswalks, cat_hatching, cat_stopbars |
line |
cat_<id> |
LF | cat_curb, cat_curbgutter, cat_whitelines, cat_yellowlines, cat_fence |
count |
cat_<id> |
EA | cat_stalls, cat_handicap, cat_arrows, cat_signage, cat_wheelstop, cat_bollard |
Each drawn shape belongs to one item and one sheet: an area (rings: the outline, then any holes), a line (points) or a count (point). A shape's source is ai when a model put it there and manual when it was drawn. Running a model again replaces that sheet's earlier ai shapes for the same items; drawn shapes stay.
Items you add with POST .../items belong to that takeoff. A line item can carry width_ft: its footage then also counts as square feet (a trench, a band).
Runs#
Storing sheets and every model call run in the background. Those endpoints answer 202 with a run; poll GET /v1/blueprint-runs/{run_id} until status is completed or failed. A completed run's result says what was found and added, and the new totals.
Run kind |
Started by |
|---|---|
import_sheets |
Starting a takeoff, adding sheets |
read_scale |
POST .../sheets/{page}/read-scale |
read_labels |
POST .../read-sheet-labels |
ai_takeoff |
POST .../sheets/{page}/ai-takeoff |
detect_curbs |
POST .../sheets/{page}/detect-curbs |
fill_area |
POST .../sheets/{page}/fill |
match_pattern |
POST .../sheets/{page}/match-pattern |
count_symbols |
POST .../sheets/{page}/count-symbols |
find_text |
POST .../sheets/{page}/find-text |
Start a takeoff#
POST /v1/jobs/{job_id}/blueprint-takeoffs
Permission: Write measurements. Uses one takeoff credit.
| Name | Type | Required | Description |
|---|---|---|---|
file_url |
string | One of the two | A public https link to the PDF itself, up to 200 MB |
file_id |
integer | One of the two | A PDF already on a job (Photos & files) |
name |
string | No | Up to 200 characters. Default "<job name> Blueprint Takeoff" |
pages |
integer[] | No | Pages of the PDF to import, for example only the site, paving and striping sheets. Default every page, up to 100 |
read_sheets |
boolean | No | Read each sheet's scale and title block. Default true |
curl -X POST https://trutec.ai/api/v1/jobs/4812/blueprint-takeoffs \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"file_url": "https://example.com/plans/lakeside-plaza-civil.pdf", "pages": [3, 4, 7]}'
{
"data": {"id": 90412, "name": "Lakeside Plaza Blueprint Takeoff", "job_id": 4812, "status": "importing",
"sheet_count": 0, "surface_area_sf": 0.0,
"editor_url": "https://trutec.ai/measurements/blueprint?measurement_id=90412&ai=1&job_id=4812"},
"run": {"id": "8d6c1f0e-2a4b-4b7e-9d43-0f6b2e8a1c55", "kind": "import_sheets", "takeoff_id": 90412,
"status": "queued", "created_at": "2026-10-13T16:00:02Z"}
}
Add more pages later, or retry a failed import, with POST /v1/blueprint-takeoffs/{takeoff_id}/sheets (same file_url / file_id / pages / read_sheets). New sheets go after the existing ones.
Read a takeoff#
GET /v1/blueprint-takeoffs lists takeoffs (filter by job_id). GET /v1/blueprint-takeoffs/{takeoff_id} returns the sheets, the items with their totals and the latest runs:
{
"data": {
"id": 90412, "name": "Lakeside Plaza Blueprint Takeoff", "status": "ready", "sheet_count": 3,
"sheets": [
{"page": 2, "sheet_number": "C2.0", "title": "Paving and striping plan", "discipline": "civil",
"paving_relevant": true,
"scale": {"feet_per_inch": 30.0, "feet_per_pixel": 0.208333, "verified": false, "text_on_sheet": "1\" = 30'"},
"shapes": {"surface_areas": 3, "areas": 6, "lines": 41, "counts": 212}}
],
"items": [
{"id": "layer:asphalt", "name": "Area", "kind": "surface", "color": "#34d399", "shape_count": 3, "area_sf": 148210.5},
{"id": "cat_stalls", "name": "Stalls", "kind": "count", "color": "#3b82f6", "shape_count": 188, "count": 188},
{"id": "cat_curb", "name": "Curb", "kind": "line", "color": "#f472b6", "shape_count": 22, "length_lf": 2875.4}
]
}
}
GET /v1/blueprint-takeoffs/{takeoff_id}/sheets/{page} adds the sheet's legend, its size in sheet pixels and the totals on that sheet. GET /v1/blueprint-takeoffs/{takeoff_id}/shapes lists the shapes (filter by page, item_id, source; paged).
The quantities an estimate pulls are at GET /v1/measurements/{takeoff_id}/items (Saving & managing takeoffs): a blueprint takeoff is a takeoff like any other, so PATCH / DELETE /v1/measurements/{takeoff_id} rename and delete it, and estimates can be built on it.
Look at a sheet#
GET /v1/blueprint-takeoffs/{takeoff_id}/sheets/{page}/image returns the sheet as a JPEG (at most max_edge pixels, default 3,000). style=annotated draws the takeoff on it in each item's color; crop=left,top,right,bottom (fractions) zooms in. The X-TruTec-Crop-Px and X-TruTec-Sheet-Px-Per-Image-Px headers map picture pixels back to sheet pixels.
POST .../sheets/{page}/image-link makes a link that opens the same picture in a browser for 24 hours without an API key. Anyone with the link can see the sheet until it expires.
Set the scale#
PUT /v1/blueprint-takeoffs/{takeoff_id}/sheets/{page}/scale, with one of:
| Body | Means |
|---|---|
{"feet_per_inch": 30} |
1" = 30' |
{"paper_inches": 1, "real_feet": 40} |
The same as a ratio |
{"points": [[812, 1540], [1388, 1540]], "distance_ft": 120} |
Two sheet-pixel points on a dimension you know |
{"verify": true} |
The scale read off the sheet is right |
Every area and length on the sheet is re-measured. POST .../sheets/{page}/read-scale reads the printed scale again ("replace": true overwrites a scale already set), and POST .../read-sheet-labels reads sheet numbers, titles and legends (pages, replace).
Run the takeoff models#
POST /v1/blueprint-takeoffs/{takeoff_id}/sheets/{page}/ai-takeoff
Permission: Write measurements.
| Name | Type | Required | Description |
|---|---|---|---|
region_box |
number[4] | One of the two | The work area as [left, top, right, bottom] fractions of the sheet. [0, 0, 1, 1] is the whole sheet |
region |
array | One of the two | The work area as one or more outlines in sheet pixels |
areas |
boolean | No | Paved area. Default true |
surface_types |
boolean | No | Split the paved area into each hatch type the sheet uses (heavy duty, concrete, pavers…), each its own surface. Default false |
lines |
boolean | No | Curb and striping lines. Default true |
objects |
boolean | No | Stalls, ADA and truck stalls, arrows, symbols, wheel stops, bollards, curb ramps and speed bumps counted; crosswalks, hatching and stop bars outlined. Default true |
The work area is required, as in the app: outline the site plan so title blocks, legends and detail views aren't counted. Results are clipped to it.
curl -X POST https://trutec.ai/api/v1/blueprint-takeoffs/90412/sheets/2/ai-takeoff \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"region_box": [0.05, 0.08, 0.72, 0.9]}'
When the run completes, result.stages says what each model added and result.items gives the sheet's new totals.
More ways to find things#
| Endpoint | What it does |
|---|---|
POST .../sheets/{page}/detect-curbs |
Reads curb and gutter from a vector (CAD) sheet's curb layers, by curb type, and adds each proposed type as a line item cat_curb_<type>. types limits it to some types. Existing, to-remove and future curb is reported, not added. Not every workspace has this reader yet; the run says so. Scanned sheets have no CAD lines: use ai-takeoff with lines |
POST .../sheets/{page}/fill |
point inside an outlined area: the outlined region becomes a surface area (item_id, default layer:asphalt) |
POST .../sheets/{page}/match-pattern |
outline around one patch of a hatch: every region hatched the same way on pages (default every sheet) lands on a surface |
POST .../sheets/{page}/count-symbols |
box around one symbol: every match on pages is counted on a count item (item_id). sensitivity 0–100 (default 60) keeps closer matches as it rises |
POST .../sheets/{page}/find-text |
Words containing query, each with its box in sheet pixels. Changes nothing |
Edit shapes and items#
Draw: POST /v1/blueprint-takeoffs/{takeoff_id}/shapes with up to 500 shapes.
{
"shapes": [
{"item_id": "layer:concrete", "page": 2, "rings": [[[1200, 900], [1600, 900], [1600, 1150], [1200, 1150]]]},
{"item_id": "cat_curb", "page": 2, "points": [[1180, 880], [1620, 880]]},
{"item_id": "cat_bollard", "page": 2, "point": [1650, 1170]}
]
}
An area goes on a surface or an area item (an area on a line item is a patch: square feet, no footage), a line on a line item, a count on a count item. A shape joins the phase of the region it lands in unless you send phase.
Change: PATCH /v1/blueprint-takeoffs/{takeoff_id}/shapes/{shape_id} with item_id (move it to another item of the same kind: a paved area to concrete, a stall to ADA), rings / points / point (reshape) or phase.
Erase: POST /v1/blueprint-takeoffs/{takeoff_id}/shapes/remove with shape_ids, or with page and/or item_id (plus source to erase only model results or only drawn work).
Items: POST /v1/blueprint-takeoffs/{takeoff_id}/items with name and kind (count, area, line, surface), optional color and, for lines, width_ft. PATCH .../items/{item_id} renames or recolors one, or sets its width_ft (null clears it).
Every edit saves the takeoff the way the plan editor does and returns the changed items' new totals. If someone saves the same takeoff in the editor at the same moment, the API reads it again and re-applies your change; a takeoff that keeps changing answers 409.
What stays in the app#
Some plan editor lanes have no endpoint yet: grading and earthwork (cut/fill, LandXML and DXF export), material assemblies and spec detection, measured inputs typed per mark, paving callouts and keynote counts, zones, the takeoff chat, and landscape plans. Takeoffs that use them open in the editor as usual; API edits leave that work as it is.