# Blueprint takeoffs

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](partner-api.md).

## 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](photos-and-files.md)) |
| `name` | string | No | Up to 200 characters. Default "&lt;job name&gt; 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` |

```bash
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]}'
```

```json
{
  "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:

```json
{
  "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](takeoffs.md)): 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.

```bash
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.

```json
{
  "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.
