# Saved measurements & imagery

A **saved measurement** is a takeoff saved in the TruTec app, including every edit your estimators made. These endpoints read them.

Detection calls (`/detect/full`, `/detect/address`, `/detect/tiles`) do **not** create saved measurements. Store detection results in your own database, or fetch an async result again from `GET /v1/detect/status/{job_id}`. See [Measure a site](measure-a-site.md).

All endpoints on this page need the **Read measurements** permission, except the tile and map picture URLs, which carry their own token.

## Endpoints

### List saved measurements

`GET /v1/measurements`

**Permission:** Read measurements

Returns your workspace's saved measurements, newest first.

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | No | Only measurements on this job. |
| `limit` | integer | No | Page size. Default `50`, maximum `100`. |
| `offset` | integer | No | Rows to skip. Default `0`. |

```bash
curl "https://trutec.ai/api/v1/measurements?job_id=4812&limit=20" \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": [
    {
      "id": 90311,
      "name": "Northgate Plaza - Takeoff",
      "type": "parking_lot",
      "job_id": 4812,
      "area": 186420.4,
      "created_at": "2026-10-08T14:22:51.318204"
    }
  ],
  "pagination": {"total": 1, "limit": 20, "offset": 0}
}
```

`area` is the total drawn area in square feet. `created_at` is UTC.

### Get a saved measurement

`GET /v1/measurements/{measurement_id}`

**Permission:** Read measurements

Returns the takeoff as it is saved now, with quantities per category, per area and per phase.

| Name | Type | Required | Description |
|---|---|---|---|
| `measurement_id` | integer (path) | Yes | From the list. |
| `unit` | string | No | Area unit for `counter_summary` values: `sf` (default), `sy`, `sm` or `acres`. |

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

```json
{
  "data": {
    "id": 90311,
    "name": "Northgate Plaza - Takeoff",
    "type": "parking_lot",
    "area": 186420.4,
    "created_at": "2026-10-08T14:22:51.318204",
    "counter_summary": {
      "stalls": {
        "label": "Stalls",
        "data_type": "marker",
        "default_display": "count",
        "count": 401,
        "formatted": "401",
        "values": {
          "count": {"value": 401, "formatted": "401"},
          "linear_feet": {"value": 7140.0, "unit": "LF", "formatted": "7,140 LF"}
        },
        "linear_feet": 7140
      }
    },
    "parking_areas": [
      {
        "id": "area-1",
        "type": "parking_detection",
        "phase": "Phase 1",
        "spec": null,
        "area_sqft": 154210.4,
        "rings": [[[30.40104, -97.72225], [30.40101, -97.72066], [30.39993, -97.72068], [30.39996, -97.72229]]],
        "is_hole": false,
        "is_carport": false,
        "is_concrete": false,
        "is_sidewalk": false,
        "objects_counted": true
      }
    ],
    "phases": [
      {"name": "Phase 1", "area_sqft": 154210.4, "areas": [], "specs": {}, "striping": {}}
    ],
    "deducted_counts": {"stalls": 401},
    "measurement_types_by_object": {},
    "striping_by_phase": {},
    "custom_items_summary": {},
    "geometry": {},
    "metrics": {}
  }
}
```

| Field | Description |
|---|---|
| `counter_summary` | One entry per category, same shape as in a detection result. See [Measurement result reference](measurement-reference.md#counter_summary). Labels use your workspace's names. |
| `parking_areas[]` | Every drawn area: `type`, `phase`, `spec` (the surface spec chosen in the app, if any), `area_sqft`, `rings` (`[lat, lng]`, first ring is the outline), and flags for holes, carports, concrete and sidewalk. |
| `phases[]` | Drawn areas grouped by phase, with `area_sqft` (holes excluded), area per spec in `specs`, and striping quantities in `striping`. |
| `deducted_counts` | Final count per category. |
| `striping_by_phase` | Striping quantities split by phase. |
| `custom_items_summary` | Custom items your team added in the app. |
| `geometry`, `metrics` | The raw data the app saves. Their shape can change; use the fields above where you can. |

Returns `404 {"error": "Measurement not found"}` when the id doesn't exist or belongs to another workspace, and `403 {"error": "API key lacks measurement read permission"}` without Read measurements.

### Download the map export

`GET /v1/measurements/{measurement_id}/export-image`

**Permission:** Read measurements

Downloads the rendered aerial map with the takeoff drawn on it and your logo, as in the app's map export.

| Name | Type | Required | Description |
|---|---|---|---|
| `measurement_id` | integer (path) | Yes | |
| `format` | string | No | `png` (default) or `pdf`. |

```bash
curl -o northgate-map.png \
  "https://trutec.ai/api/v1/measurements/90311/export-image?format=png" \
  -H "X-API-Key: sk_your_api_key_here"
```

The response is the file, sent as a download. Headers `X-TruTec-Measurement-Id`, `X-TruTec-Job-Id` and `X-TruTec-Project-Id` (when set) identify it.

| Status | `error` |
|---|---|
| 400 | `format must be either pdf or png` |
| 404 | `Measurement not found` |
| 422 | `Measurement has no drawn areas to export` (also returned for plan-sheet takeoffs) |
| 422 | `Unable to render map export for this measurement` |

### Download the raw imagery

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

**Permission:** Read measurements

Downloads the clean aerial image behind a takeoff: the same imagery source the takeoff was measured on, covering the takeoff, with no drawings, legend or logo. Use it to keep the imagery next to the quantities in your own system. The longest side is at most 3,600 pixels.

| Name | Type | Required | Description |
|---|---|---|---|
| `measurement_id` | integer (path) | Yes | |
| `format` | string | No | `png` (default) or `jpeg` (`jpg` also accepted). |

```bash
curl -o northgate-imagery.jpg \
  "https://trutec.ai/api/v1/measurements/90311/imagery?format=jpeg" \
  -H "X-API-Key: sk_your_api_key_here"
```

The `X-TruTec-Imagery-Source` header says which imagery was used, for example `google` or `nearmap:88f1e2a0-1234-5678-abcd-1234567890ab` (Nearmap plus the survey id). `X-TruTec-Measurement-Id` and `X-TruTec-Job-Id` are also set.

| Status | `error` |
|---|---|
| 400 | `format must be png or jpeg` |
| 404 | `Measurement not found` |
| 422 | `Measurement has no mapped area to fetch imagery for` |
| 422 | `Unable to fetch imagery for this measurement` |

### Get a link to the map

`POST /v1/measurements/{measurement_id}/map-link`

**Permission:** Read measurements

Returns a link that opens the takeoff's map picture in a browser for 24 hours, with no API key. Use it to show a map to someone, or in a chat app, without handing out your key.

| Name | Type | Required | Description |
|---|---|---|---|
| `measurement_id` | integer (path) | Yes | |
| `style` | string (body) | No | `annotated` (default): the takeoff drawn on the aerial with its legend and your logo, like the map export. `aerial`: the clean imagery. |

```bash
curl -X POST https://trutec.ai/api/v1/measurements/90311/map-link \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"style": "annotated"}'
```

```json
{
  "data": {
    "measurement_id": 90311,
    "url": "https://trutec.ai/api/v1/map-images/eyJtIjo5MDMxMX0.abc123.def456.jpg",
    "style": "annotated",
    "expires_at": "2026-10-14T15:04:11Z"
  }
}
```

The `annotated` picture leaves out internal assessment pins and takeoff notes, because a link can be forwarded. The `export-image` download above keeps them.

| Status | `error` |
|---|---|
| 400 | `style must be one of: annotated, aerial` |
| 404 | `Measurement not found` |
| 422 | `Measurement has no drawn areas to export` (also returned for plan-sheet takeoffs) |

### Fetch a map picture

`GET /v1/map-images/{token}.jpg`

**Permission:** None. The token in the URL is the credential.

Use the `url` from the call above rather than building this URL yourself. Returns a JPEG, at most 2,400 pixels on its long side. Anyone with the URL can see the map until it expires, so treat it like a secret.

An expired or altered link returns an empty `404`. The link also stops working early when the key that made it is revoked or the takeoff leaves the workspace; get a new link then. A `503` with a `Retry-After` header means the map is still being drawn: try again after that many seconds.

### Get a Nearmap tile layer

`GET /v1/measurements/{measurement_id}/imagery-tiles`

**Permission:** Read measurements

For drawing a takeoff on your own map over the same Nearmap imagery it was measured on. Returns an XYZ tile URL template that needs no headers, limited to the area around the site.

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

```json
{
  "data": {
    "measurement_id": 90311,
    "imagery_source": "nearmap",
    "tiles": {
      "provider": "nearmap",
      "survey_id": "88f1e2a0-1234-5678-abcd-1234567890ab",
      "url_template": "https://trutec.ai/api/v1/imagery-tiles/eyJtIjo5MDMxMX0.abc123/{z}/{x}/{y}.jpg",
      "tile_size": 256,
      "min_zoom": 15,
      "max_zoom": 21,
      "bounds": {"south": 30.39685, "west": -97.72536, "north": 30.40412, "east": -97.71758},
      "attribution": "Imagery © Nearmap",
      "expires_at": "2027-01-10T15:04:11Z"
    }
  }
}
```

`tiles` is `null` when the takeoff is on Google imagery, when your plan no longer includes Nearmap, or when the takeoff spans more than about 3.5 miles. Keep your own base map in that case. The URL expires after 90 days; fetch a new one before then.

### Fetch a tile

`GET /v1/imagery-tiles/{token}/{z}/{x}/{y}.jpg`

**Permission:** None. The token in the URL is the credential.

Use the `url_template` from the call above rather than building this URL yourself. Returns a JPEG tile. Tiles outside the site area or below zoom 15 return an empty `404`. The layer also stops working if the key that created it is revoked, the workspace loses Nearmap, or the takeoff is re-measured on another survey; fetch a new layer then. Treat the URL like a secret.

## Imagery source and capture date

| Where | Source | Capture date |
|---|---|---|
| Detection result (`imagery_source`) | `google` by default; `nearmap` when you pass `nearmap_survey_id` | Google: none. Nearmap: only the `nearmap_date` you sent, copied back. |
| `GET /v1/measurements/{id}/imagery` | `X-TruTec-Imagery-Source` header: `google` or `nearmap:<survey id>` | Not returned |
| `GET /v1/measurements/{id}/imagery-tiles` | `imagery_source` and `tiles.survey_id` | Not returned |
| `GET /v1/measurements/{id}` | Not returned | Not returned |
| Condition ratings (`image`) | `google` or `nearmap` | Nearmap: `image.capture_date` of the survey used. Google: `null`. See [Condition ratings](condition-ratings.md). |

Google does not publish capture dates for its satellite imagery. When the date of the imagery matters, measure on a Nearmap survey whose date you know, or upload your own dated imagery with `/detect/tiles`.
