# Measurement result reference

This page describes the JSON returned by `POST /v1/detect/full`, `POST /v1/detect/address` and `POST /v1/detect/tiles`, and by `GET /v1/detect/status/{job_id}` under `result`. Saved measurements reuse the `counter_summary` shape; see [Saved measurements & imagery](measurements.md).

Coordinates are `[latitude, longitude]` everywhere in JSON output. Fields are only ever added over time, so ignore fields you don't use.

## Top-level fields

| Field | Type | Description |
|---|---|---|
| `success` | boolean | `true` on a completed detection. |
| `summary` | object | Totals. See [summary](#summary). |
| `parking_areas` | array | Paved areas with geometry. See [parking_areas](#parking_areas). |
| `counters` | object | Detected items by category, with geometry. See [counters](#counters). |
| `counter_summary` | object | Every quantity by category. The best field to store. See [counter_summary](#counter_summary). |
| `imagery_source` | object | Which imagery was measured. See [imagery_source](#imagery_source). |
| `carports`, `concrete`, `sidewalk` | object | Present when you turned them on. See [Optional layers](#optional-layers). |
| `curbs`, `wheel_stops`, `site_objects`, `landscape` | object | Present when you turned on the matching add-on model. See [Add-on models](#add-on-models). |
| `areas` | array | Present when you sent named `areas`. See [areas](#areas). |
| `excluded_islands` | object | Present when you sent islands to leave out. `count` (islands applied) and `area_sqft` (ground they take out of your boundary, in square feet). See [Excluded islands](measure-a-site.md#excluded-islands). |
| `marking_details`, `striping_by_color` | object | Present when you sent `marking_details: true`. See [Marking details](#marking-details). |
| `context` | object | `captured_at` (when this detection ran, UTC; **not** the imagery date) and `map_view.bounds` (the box measured). |
| `billing` | object | Partner keys only. |

The response also carries fields kept for compatibility with TruTec's embedded editor: `contours`, `holes`, `parking`, `objects`, `predictions`, `preds`, `count`, `deducted_counts`, `measurement_types_by_object`, `measurement_entry`, `iframe_payload`, `striping_by_phase`, `custom_items`, `custom_items_summary`, `phase_colors`, `specs_breakdown`, `spec_definitions`, `value_overrides`, `lf_assumption_overrides` and `object_stats`. They repeat data from the fields above in other shapes. New integrations can ignore them.

## summary

| Field | Type | Description |
|---|---|---|
| `total_area.value` | number | Total paved area in your `unit`, rounded to 2 decimals. Includes carport area when carports are on. |
| `total_area.unit` | string | `SF`, `SY`, `SM` or `AC`. |
| `total_area.formatted` | string | e.g. `"186,420 SF"`. |
| `total_area_sqft` | integer | The total in square feet. |
| `total_stalls` | integer | `stalls` + `handicap` + `trucks`. |
| `polygon_count` | integer | Number of separate paved areas. |
| `unit` | string | The `unit` you requested, e.g. `sf`. |
| `phase` | string | `Phase 1`, or the first area's name when you sent `areas`. |

## parking_areas

| Field | Type | Description |
|---|---|---|
| `rings` | array | `[outline, hole, hole, ...]`, each a list of `[lat, lng]`. Use this to draw the area with its holes (islands, buildings). |
| `coordinates` | array | The outline only (same as `rings[0]`). |
| `holes` | array | The holes only. |
| `area_sqft` | number | Area in square feet, net of holes. |
| `type` | string | Always `parking_area`. |
| `area`, `phase` | string | The area name, when you sent `areas`. |

## counters

One entry per detected category. Categories with nothing detected are left out. Every entry has `label`, `unit`, `formatted`, `values`, `color`, `group`, `data_type`, `display_modes` and, where relevant, `assumptions`. The top-level `count` or `value` shows the category's display value (see `counter_display`); `values` always has every quantity.

**Markers** (`stalls`, `handicap`, `trucks`, `arrows`, `abc`) list items in `points[]`:

```json
{"id": "stalls-12", "lat": 30.40087, "lng": -97.72201, "confidence": 0.94, "area_sqft": 161.8,
 "polygon": [[30.40085, -97.72203], [30.40085, -97.72199], [30.40089, -97.72199], [30.40089, -97.72203]]}
```

`confidence` (0 to 1), `area_sqft` and `polygon` appear when known. `polygon` is a simplified 4-corner box around the item; `area_sqft` is measured on the detected shape, so it won't exactly match the box.

**Area markings** (`crosswalks`, `crosshatches`, `stop_line`) list items in `polygons[]`: `id`, `coordinates` (one ring), `area_sqft` and `confidence`. Crosswalks add `crosswalk_pattern` (`standard`, `continental`, `ladder` or `unknown`), `has_borders` and `has_bars`. Hatching adds `hatching_type` (`striped`, `solid` or `ada_adjacent`), `has_interior_stripes` and `is_ada_adjacent`. Stop bars add `bar_count`.

**Striping** (`lines_white`, `lines_yellow`, `lines_double_yellow`) lists each stripe in `lines[]` as a centerline: `id`, `points` (`[lat, lng]` list), `length_ft`, `phase` and `confidence`, plus `overlap_with_stalls_ft`, `counted_length_ft` and `overlap_stall_ids` (see [Adding up linear feet](#adding-up-linear-feet)). `source_polygons[]` holds the painted stripe outline each line came from, with the same `id`.

**Per-item ids.** Every item has an `id` of the form `<category>-<index>`, e.g. `stalls-12`. Ids are unique within one response and match across `counters`, the per-polygon breakdowns, and GeoJSON features. A new detection of the same site can number items differently.

## values

Each category's `values` object holds every quantity that applies to it:

| Key | Unit | Categories |
|---|---|---|
| `count` | items | All |
| `area` | your `unit` | `crosswalks`, `crosshatches`, `stop_line`; `arrows` and `abc` (count × 12 sq ft or 16 sq ft per item; `measured_sqft` adds the detected area when known) |
| `linear_feet` | `LF` | Striping (sum of each stripe's `counted_length_ft`); stalls (stall striping); area markings (estimated paint) |
| `line_count` | lines | `stalls`, `handicap`, `trucks` (divider lines) |
| `units` | your label | Only when your workspace defines a custom unit rule for the category |

Each value is `{"value": ..., "unit": ..., "formatted": ...}`. Some include a `breakdown`:

- **Stall linear feet** = divider lines between stalls (18 ft each, 21 ft for truck stalls) plus the center line between facing rows. `breakdown` has `row_divider_lf`, `divider_line_count`, `center_spine_lf`, `center_spine_count`, `row_count` and `divider_length_buckets`. A divider between two different stall types (e.g. a regular stall next to a handicap stall) is counted once, on the handicap or truck category, in `shared_divider_count` / `shared_divider_lf`. Stall width defaults to 9 ft (handicap and truck 12 ft); your workspace settings can change it.
- **Crosswalk linear feet** = border lines plus bars every 4 ft, by pattern. **Hatching linear feet** = outline plus diagonal stripes every 2 ft. **Stop bar linear feet** = bar length × bar count. `breakdown.polygons[]` has one row per item, with the item's `id` and `lf`.
- **Striping linear feet** = the stripes' centerlines, less any stretch that lies on stall striping (counted under the stalls). See [Adding up linear feet](#adding-up-linear-feet).

### Adding up linear feet

Every category's `values.linear_feet.value` adds to the others: `stalls`, `handicap`, `trucks`, `crosshatches`, `crosswalks`, `stop_line`, `lines_white`, `lines_yellow` and `lines_double_yellow` never count the same paint twice. Total striping paint is their sum.

Stall linear feet come from the stalls themselves (the dividers between stalls plus the center line between facing rows), so that paint is often detected as a stripe too. A stripe, or the stretch of one, that runs along a divider or center line counted in stall linear feet is counted under the stall category and left out of striping:

- A stretch is on the stall striping when it is within 1.5 ft of the divider or center line and within 20° of parallel to it.
- Only the dividers and center lines that stall linear feet count are matched. A row's end lines and the line along the stall fronts are not in stall linear feet, so they stay in striping.
- A long stripe that crosses many dividers, such as one running through a row, loses only the stretches that run along one.

Each stripe in `counters.<category>.lines[]` shows its split:

| Field | Description |
|---|---|
| `length_ft` | The detected length of the stripe. |
| `overlap_with_stalls_ft` | The length that runs along counted stall striping and is counted under the stalls. `0.0` when none. |
| `counted_length_ft` | `length_ft` − `overlap_with_stalls_ft`: what the stripe adds to the striping total. |
| `overlap_stall_ids` | The `id`s of the stalls, handicap stalls and truck stalls whose divider or center line the stripe runs along. Empty when none. |

The category's `values.linear_feet` has `value` (the sum of `counted_length_ft`), `detected_lf` (the sum of `length_ft`) and `overlap_with_stalls_lf` (the length counted under the stalls). `counter_summary`, `areas[]` and `striping_by_phase` use `value`. In GeoJSON, each stripe feature carries the same three fields, and its `lf` is its `counted_length_ft`.

```json
"lines_white": {
  "lines": [
    {"id": "lines_white-3", "points": [[30.400710, -97.72187], [30.400766, -97.72187]],
     "length_ft": 20.4, "overlap_with_stalls_ft": 18.0, "counted_length_ft": 2.4,
     "overlap_stall_ids": ["stalls-14", "stalls-15"]},
    {"id": "lines_white-4", "points": [[30.40061, -97.72190], [30.40061, -97.72121]],
     "length_ft": 218.7, "overlap_with_stalls_ft": 0.0, "counted_length_ft": 218.7,
     "overlap_stall_ids": []}
  ],
  "values": {
    "linear_feet": {"value": 221.1, "unit": "LF", "formatted": "221 LF",
                    "detected_lf": 239.1, "overlap_with_stalls_lf": 18.0}
  }
}
```

Here the first stripe lies on the divider between `stalls-14` and `stalls-15`: 18 ft of it is that divider, already in `stalls`, and the 2.4 ft past the stall ends stays in `lines_white`.

## counter_summary

One entry per category, ready to store. It has the same quantities as `counters` without the geometry:

```json
"crosswalks": {
  "label": "Crosswalks",
  "color": "#14b8a6",
  "group": "area",
  "data_type": "polygon",
  "display_modes": ["area", "linear_feet", "count"],
  "default_display": "area",
  "count": 6,
  "formatted": "1,285 SF",
  "values": {
    "count": {"value": 6, "formatted": "6"},
    "area": {"value": 1284.6, "unit": "SF", "formatted": "1,285 SF"},
    "linear_feet": {"value": 1046.2, "unit": "LF", "formatted": "1,046 LF"}
  },
  "assumptions": {"default_width_ft": 6},
  "area_sqft": 1285,
  "linear_feet": 1046,
  "lf_assumptions": {"bar_spacing_ft": 4.0},
  "lf_breakdown": {"polygon_count": 6, "border_lf_total": 512.4, "bar_lf_total": 533.8}
}
```

`area_sqft`, `linear_feet` and `line_count` here are rounded to whole numbers. Use `values.*.value` (one decimal) for estimating.

## areas

With named `areas`, each entry is:

| Field | Type | Description |
|---|---|---|
| `name` | string | The name you sent. |
| `area_sqft` | integer | Pavement inside the area, in square feet. Left out when `include_parking` is `false`. |
| `excluded_islands` | object | Present when the request has islands: `count` (islands that overlap this area) and `area_sqft` (the part of this area's polygon they take out). |
| `counter_summary` | object | Same shape as the top-level one, for this area's items only. |

Items in `counters` and `parking_areas` carry `area` and `phase` set to the area name. The areas add up to the top-level totals.

## Optional layers

`carports`, `concrete` and `sidewalk` each have `area_sqft`, `count` and `polygons[]` (`coordinates`, `area_sqft`). A layer's `area_sqft` is the sum of its `polygons[].area_sqft`, and `count` is the number of polygons. `coordinates` is a list of rings (`[lat, lng]`): the first is the outline, any others are holes, and a polygon's `area_sqft` leaves its holes out. Shapes are clipped to your `polygon`; shapes and cut pieces under about 8 sq ft are not returned or counted. Carports also have `estimated_stalls` in total and per polygon; stalls under carports are added to `counters.stalls`, and carport area is added to `summary.total_area`. Concrete and sidewalk are not added to the paved total.

The paved-area model finds drivable paved surface whatever it is made of, so `parking_areas` can include concrete paving. To get asphalt only, send `asphalt_only: true`: concrete (and sidewalk, when `include_sidewalk` is on) is cut out of `parking_areas` and `summary.total_area`, and `asphalt_only.removed_sqft` is the paved area taken out. Carports are already their own layer.

## Add-on models

Sent with `include_curbs`, `include_wheel_stops`, `include_site_objects` or `include_landscape`. Each adds its own section. Add-ons never change `counters`, `counter_summary` or `summary`.

Every section has `status` (`complete` or `failed`; `site_objects` can also be `partial`) and, unless complete, `error`. A failed section has no quantities.

| Section | Fields |
|---|---|
| `curbs` | `count` (lines), `linear_feet` (0.1 ft), `follows_paved_edges` (`true` when the lines trace the detected pavement's edges), `lines[]`: `id` (`curbs-<n>`), `points` (`[lat, lng]`), `length_ft` |
| `wheel_stops` | `count`, `points[]`: `id` (`wheel_stops-<n>`), `lat`, `lng` |
| `site_objects` | `count`, `count_by_class` (`parallel_stall`, `speed_bump`, `bollard`: `label`, `count`, zero included), `points[]`: `id` (`site_objects-<n>`), `class`, `label`, `lat`, `lng`, `confidence` |
| `landscape` | `unit` (`SF`, `SY`, `SM` or `AC`), `total_area`, `total_area_sqft`, `area_by_class` (per area type found: `label`, `count`, `area` in `unit`, `area_sqft`), `count_by_class` (per object type found: `label`, `count`), `tree_count` (deciduous + evergreen + palm), `point_count`, `polygons[]`: `id` (`landscape_areas-<n>`), `class`, `label`, `rings`, `area_sqft`; `points[]`: `id` (`landscape_points-<n>`), `class`, `label`, `lat`, `lng`, `confidence` |

Landscape area types: `driveway`, `road`, `parking_area`, `gravel`, `hedge`, `lawn`, `sidewalk`, `mulch`, `sand`, `clay`, `deck`, `pond`, `trash`. Landscape object types: `deciduous`, `evergreen`, `palm`, `shrub`, `stump`, `fountain`. Only types that were found are listed.

For `areas` requests, add-on items also carry `area`, the named area they fall in.

## imagery_source

| Value | Meaning |
|---|---|
| `{"type": "google"}` | Google satellite imagery (the default). No capture date is available. |
| `{"type": "nearmap", "survey_id": "...", "date": "..."}` | The Nearmap survey you passed. `date` is the `nearmap_date` you sent, or `null`. |
| `{"type": "uploaded_tiles", ...}` | Your `/detect/tiles` upload: `coverage_bounds`, `tile_count`, `meters_per_pixel_at_center`, `edge_handling` and the size of each tile. |

## Marking details

Sent with `marking_details: true`. It only adds fields. Existing counts and quantities don't change.

Per item in `counters`:

| Field | On | Values |
|---|---|---|
| `paint_color` | every item | `white`, `yellow`, `blue`, `red`, `green`, `black`, `other`, `unknown` |
| `paint_color_source` | every item with `paint_color` | Where the color comes from: `checked` (this item's own image was read), `row` (stalls and trucks: the checked stalls in its row), `nearby` (stalls and trucks: the nearest checked stall within 60 ft), `line_class` (lines: the white or yellow line type from detection, because the line wasn't read or its read didn't confirm a color), `unknown` |
| `arrow_type` | `arrows` | `straight`, `left`, `right`, `straight_left`, `straight_right`, `left_right`, `u_turn`, `merge`, `bike`, `other`, `unknown` |
| `stencil_kind` | `abc` | `text`, `number`, `symbol`, `logo`, `wheelchair`, `other`, `unknown` |
| `text` | `abc` | The painted words, uppercase, or `null` when unreadable |
| `is_custom` | `abc` | `true` for a logo or custom stencil, `false` for a stock stencil, `null` when unknown |
| `handicap_stall_id` | `abc` wheelchair symbols | The `id` of the handicap stall the symbol is painted in, or `null` when it is outside every handicap stall |
| `wheelchair_symbol` | `handicap` | `true`, `false`, or `null` when hidden. `true` also when a wheelchair symbol in `abc` is painted in the stall. |
| `wheelchair_symbol_ids` | `handicap` | The `abc` wheelchair symbols painted in this stall, e.g. `["abc-12"]`. Only present when there is one. |
| `divider_style` | `stalls`, `trucks`, `handicap` | `single` (one painted line between neighboring stalls), `double` (two parallel painted lines about 1 to 2 ft apart, often joined at the ends), `unknown` |
| `divider_style_source` | `stalls`, `trucks`, `handicap` | `checked`, `row`, `nearby` (the nearest checked stall within 60 ft) or `unknown` |

Paint color and divider style are read on a sample of stalls in every row; the other stalls in the row take the row's values (`row`), or the nearest checked stall's when the row has none (`nearby`). `values.count.by_color_source` on `stalls` and `trucks` counts the stalls by source, e.g. `{"checked": 60, "row": 490, "nearby": 10, "unknown": 6}`.

Also added: `counters.abc.stencils[]` (shapes grouped into painted messages), `values.count.by_color` and `values.linear_feet.by_color` in `counter_summary`, a top-level `striping_by_color`, and `counter_summary.ada_stencils.values.detected_count`. Each named area's `counter_summary` gets the same summary fields for its own items.

The top-level `marking_details` block reports `status` (`complete`, `partial`, `failed`, `skipped` or `off`), `items_reviewed`, `items_unknown` and `notes`. Marking details never fail a detection. On a synchronous request they are `skipped` if the detection used most of the available time; send `async: true` for large sites.

### Stencils

Each entry in `counters.abc.stencils[]` is one painted message. Its kind is in both `kind` and `stencil_kind` (the same value, so one name works for stencils and `abc` items). Wheelchair symbols are not stencils; they are counted under `ada_stencils`. `values.stencil_count` on `abc` counts the stencils, with `by_color`:

```json
"stencil_count": {"value": 3, "formatted": "3", "by_color": {"white": 2, "yellow": 1}}
```

### Wheelchair symbols

`ada_stencils.values.detected_count` counts each painted wheelchair symbol once, whether it was seen in the handicap stall's own image, as an `abc` symbol, or both. Every handicap stall is in exactly one of `in_handicap_stalls`, `handicap_stalls_without_symbol` and `handicap_stalls_unknown`, and `value` is `in_handicap_stalls` + `outside_handicap_stalls`:

```json
"detected_count": {
  "value": 5, "formatted": "5", "basis": "detected",
  "in_handicap_stalls": 4,
  "outside_handicap_stalls": 1,
  "handicap_stalls_without_symbol": 1,
  "handicap_stalls_unknown": 1
}
```

An `abc` wheelchair symbol keeps `duplicate_of: "ada_stencils"` and names its stall in `handicap_stall_id`; that stall lists it in `wheelchair_symbol_ids`.

### Stall striping

Stall `linear_feet.value` counts one line for each divider between neighboring stalls, plus the center line between facing rows. It doesn't count the line at each end of a row, and it doesn't change with marking details. `painted_lf` is what is painted as far as the imagery shows: each double-line divider counts as two lines, each painted row end counts (two lines on a double-line row), and the center line counts once. Unknown divider styles count as one line; unknown row ends aren't included, so add them yourself if you want them. The short caps joining the two lines of a double-line divider (about 1 ft each) aren't included. When a double divider style in your measurement settings already doubles `value`, `painted_lf` starts from one line per divider and one center line, so nothing is counted twice.

`values.linear_feet` on `stalls`, `trucks` and `handicap` adds:

| Field | Meaning |
|---|---|
| `divider_styles` | Dividers between neighboring stalls by the style of their row: `{"single": n, "double": n, "unknown": n}`. A row of N stalls has N - 1. |
| `row_end_lines` | The two ends of every row: `painted`, `not_painted`, `unknown` and `painted_lf` (the painted ends' feet, two lines on a double-line row). An end next to another stall is a divider and isn't listed. Ends over the per-request limit are `unknown`. |
| `painted_lf` | Painted stall striping in feet, as above. |

```json
"linear_feet": {
  "value": 1314.0, "unit": "LF", "formatted": "1,314 LF",
  "breakdown": {"divider_line_count": 70, "divider_line_size_ft": 18, "center_spine_lf": 54.0},
  "divider_styles": {"single": 0, "double": 70, "unknown": 0},
  "row_end_lines": {"painted": 10, "not_painted": 1, "unknown": 1, "painted_lf": 360.0},
  "painted_lf": 2934.0
}
```

Here 1,314 LF = 70 dividers × 18 ft + 54 ft of center line, and 2,934 LF = 1,314 + 70 × 18 ft for the second line of each divider + 360 ft for 10 painted double-line row ends.

## Other output formats

- **`format: "geojson"`** returns a `FeatureCollection` with `[longitude, latitude]` coordinates. Paved areas, layers and every item are features. `properties.featureType` is `parking_area`, `carport`, `concrete`, `sidewalk` or `counter`; counter features also have `category`, `label`, `id` and their measurements. Add-on items are features with `featureType` `curb`, `wheel_stop`, `site_object`, `landscape_area` or `landscape_point`. The collection's `properties` holds `summary`, each add-on section without its geometry, and `excluded_islands` when you sent islands.
- **`format: "kmz"`** returns a KMZ file (`application/vnd.google-earth.kmz`) with folders for paved areas, layers and counted items. Synchronous only.

## Categories

### List categories

`GET /v1/categories`

**Permission:** Any valid key

Returns metadata for every category: label, color, group, data type, the measurement types it supports and its defaults. Build your UI from this instead of hard-coding categories.

```bash
curl https://trutec.ai/api/v1/categories -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "categories": {
    "stalls": {
      "label": "Stalls",
      "color": "#3b82f6",
      "group": "stalls",
      "data_type": "marker",
      "display_modes": ["count", "line_count", "linear_feet"],
      "measurement_types": ["count", "line_count", "linear_feet"],
      "default_display": "count",
      "default_width_ft": 9,
      "default_sqft": 162
    }
  },
  "groups": {
    "stalls": {"label": "Stalls & Markings", "description": "Point-based detections"},
    "area": {"label": "Area Markings", "description": "Polygon-based area detections"},
    "lines": {"label": "Striping", "description": "Line-based striping detections"}
  }
}
```

Detection returns these categories:

| Key | Label in results | Type | Measurement types |
|---|---|---|---|
| `stalls` | Stalls | marker | count, line count, linear feet |
| `handicap` | Handicap | marker | count, line count, linear feet |
| `trucks` | Trucks | marker | count, line count, linear feet |
| `arrows` | Arrows | marker | count, area |
| `abc` | Symbols | marker | count, area |
| `crosswalks` | Crosswalks | polygon | area, linear feet, count |
| `crosshatches` | Crosshatches | polygon | area, linear feet, count |
| `stop_line` | Stop Line | polygon | area, linear feet, count |
| `lines_white` | White Lines | line | linear feet, count |
| `lines_yellow` | Yellow Lines | line | linear feet, count |
| `lines_double_yellow` | Double Yellow | line | linear feet, count |

`/categories` also lists categories used in saved takeoffs (`ada_stencils`, `ada_crosshatch`, `stall_centerline`, `lane_lines_skip`, `misc_lines`). Its labels are the defaults; results use your workspace's labels or any `counter_labels` you send.

## Units

| `unit` | Area label | Linear |
|---|---|---|
| `sf` (default) | `SF` square feet | `LF` |
| `sy` | `SY` square yards | `LF` |
| `sm` | `SM` square meters | `LF` |
| `acres` | `AC` acres | `LF` |

Fields ending in `_sqft` are always square feet. Linear quantities are always feet.

## Filtering

Results match what the TruTec editor shows:

- Paved areas under 200 sq ft and holes under 50 sq ft are dropped.
- Hatching under 5 sq ft is dropped.
- When paved area is measured, markings outside it are dropped.
- Crosswalks take precedence over hatching, and hatching over stripes and stalls, so nothing is counted twice.
