# Saving & managing takeoffs

A **takeoff** (a saved measurement) is what a job's estimate pulls quantities from. Takeoffs you draw in TruTec are already saved. A detection you run through the API ([Measure a site](measure-a-site.md)) is not: it returns quantities and geometry but attaches nothing to a job. Save it onto a job to make it a takeoff: it then shows on the job, opens in the aerial takeoff editor where your team can adjust it, and feeds estimates.

Typical flow:

1. `POST /v1/detect/address` (or `/v1/detect/full`) with `"async": true`. Keep the returned `id`.
2. Poll `GET /v1/detect/status/{id}` until `status` is `completed`.
3. `POST /v1/jobs/{job_id}/measurements` with `{"detection_id": "<id>"}`.
4. `GET /v1/measurements/{measurement_id}/items` for the quantities.

Reading saved takeoffs is covered in [Saved measurements & imagery](measurements.md). To add, remove or reclassify an aerial takeoff's areas, counts, lines and custom item shapes, see [Editing takeoff shapes](takeoff-editing.md). Plan-sheet takeoffs can be built and edited end to end through the API, see [Blueprint takeoffs](blueprints.md).

!!! 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).

### Save a detection onto a job

`POST /v1/jobs/{job_id}/measurements`

**Permission:** Write measurements

| Name | Type | Required | Description |
|---|---|---|---|
| `detection_id` | string | One of the two | The `id` of an async detection run by this workspace. It must be `completed`. |
| `detection` | object | One of the two | The JSON body a synchronous detect call returned. It must be the `json` format, not GeoJSON or KMZ. |
| `name` | string | No | Up to 200 characters. Default "&lt;job name&gt; Aerial Takeoff". |

```bash
curl -X POST https://trutec.ai/api/v1/jobs/4812/measurements \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"detection_id": "6b0f2a51-8c1e-4d7a-9f3e-2a7c4b9d1e60", "name": "Lakeside Plaza main lot"}'
```

Returns `201`:

```json
{
  "data": {
    "id": 90311,
    "name": "Lakeside Plaza main lot",
    "type": "parking",
    "job_id": 4812,
    "area": 18250.5,
    "imagery_source": "google",
    "created_at": "2026-10-12T15:04:11Z",
    "updated_at": "2026-10-12T15:04:11Z"
  }
}
```

The takeoff is recorded on the job's timeline as the person who created the API key. `imagery_source` is `google`, or `nearmap:<survey id>` when the detection ran on a Nearmap survey, so map exports and imagery downloads use the same imagery.

If the detection ran with [add-on models](measure-a-site.md#add-on-models), its curbs, wheel stops, speed bumps, bollards and parallel stalls are saved as the takeoff's curb, wheel stop, speed bump, bollard and parallel stall items, so they show in the takeoff editor. Curb linear feet and curb count are in the takeoff's quantities right away; the other items' counts, and parallel stall striping footage, are worked out by the editor and fill in the first time someone saves the takeoff there. Add-ons whose `status` is `failed` add nothing. Landscape results are not saved into the takeoff.

Saving does not run or bill another detection. Each save makes a new takeoff, so save a detection once. If your team later runs detection again on this takeoff inside the TruTec editor, that run is billed like any new detection.

| Status | `error` |
|---|---|
| 400 | `Send detection_id or detection` (neither or both sent), `name must be 200 characters or less`, `Unknown fields` |
| 403 | `API key lacks measurement write permission` |
| 404 | `Job not found`, `Detection not found` (unknown id, or another workspace's run) |
| 409 | `Detection still running`. Poll `GET /v1/detect/status/{id}` and save it when it is `completed`. |
| 422 | `Detection failed`, `GeoJSON results cannot be saved`, `Not a detection result` |

### Get takeoff quantities

`GET /v1/measurements/{measurement_id}/items`

**Permission:** Read measurements

Every quantity an estimate can pull from the takeoff: the same list the estimate's quantity picker shows. It covers total area, area by spec, concrete and sidewalk area, stall and marking counts and their striping footage, spec splits (for example van-accessible handicap stalls), custom items, and blueprint layers. Works for any saved takeoff, aerial or blueprint.

`services` lists services the takeoff's **Services** tab mapped measured items to, with the summed quantity in the unit the takeoff measured. It is empty when nothing was mapped.

```bash
curl https://trutec.ai/api/v1/measurements/90311/items \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": {
    "measurement_id": 90311,
    "name": "Lakeside Plaza main lot",
    "job_id": 4812,
    "items": [
      {"key": "total_area_sf", "label": "Total area", "quantity": 18250.5, "unit": "SF"},
      {"key": "stalls", "label": "Parking stalls", "quantity": 142, "unit": "EA"},
      {"key": "stalls_lf", "label": "Parking stalls", "quantity": 2556.0, "unit": "LF"},
      {"key": "item_spec:handicap:Van accessible", "label": "Handicap — Van accessible", "quantity": 2, "unit": "EA", "kind": "spec"}
    ],
    "services": [
      {"service_id": 318, "service_name": "4\" Restripe", "item_keys": ["stalls", "crosswalks"], "quantity": 2980.0, "unit": "LF"}
    ]
  }
}
```

| Field | Description |
|---|---|
| `items[].key` | Stable key for the quantity on this takeoff. |
| `items[].label` | What the estimate picker calls it. |
| `items[].quantity`, `items[].unit` | Value and unit: `SF`, `LF`, `EA`, `CY`, `TON`. |
| `items[].kind` | `spec` for a spec or size split, `custom` for a custom item. Absent for standard quantities. |
| `services[].service_id`, `service_name` | The catalog service. |
| `services[].item_keys` | Measured items mapped to it. |
| `services[].quantity`, `unit` | The summed quantity, or `null` when nothing mapped was measured. |

Only quantities above zero are listed. `422 Could not read quantities` means the takeoff's saved data could not be read; open it in TruTec and save it again.

### Rename a takeoff

`PATCH /v1/measurements/{measurement_id}`

**Permission:** Write measurements

| Name | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Up to 200 characters. |

```bash
curl -X PATCH https://trutec.ai/api/v1/measurements/90311 \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Main lot"}'
```

Returns `200` with the takeoff in the same shape as the save response. Errors: `400 name is required`, `400 name must be 200 characters or less`, `400 Unknown fields` (only `name` can change here).

### Delete a takeoff

`DELETE /v1/measurements/{measurement_id}`

**Permission:** Write measurements and Delete records

Deletes the takeoff the way the editor's Delete does: it is logged on the job, and its stored plan sheets and drone photos are cleaned up. Estimates that already pulled quantities from it keep their numbers. Returns `204`. This cannot be undone.

```bash
curl -X DELETE https://trutec.ai/api/v1/measurements/90311 \
  -H "X-API-Key: sk_your_api_key_here"
```
