Skip to content
TruTecDevelopers
Get an API key

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 Markdown

A 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:

  1. POST /v1/jobs/{job_id}/blueprint-takeoffs with the PDF. Poll the import run it returns.
  2. GET /v1/blueprint-takeoffs/{takeoff_id} to see the sheets, their titles and scales.
  3. GET /v1/blueprint-takeoffs/{takeoff_id}/sheets/{page}/image to look at a sheet. Set or confirm its scale with PUT .../sheets/{page}/scale.
  4. POST .../sheets/{page}/ai-takeoff with the part of the sheet that holds the site plan. Poll the run.
  5. Fix the results: PATCH .../shapes/{shape_id} to move a shape to the right item, POST .../shapes to draw what's missing, POST .../shapes/remove to erase what's wrong.
  6. Read the totals from GET /v1/blueprint-takeoffs/{takeoff_id}, or the estimate's view from GET /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.