# Site assessments

A **site assessment** is a field walkthrough on a job. As the crew walks the lot they drop **pins**: each pin is a location with what was found (a service such as *Pothole repair* or *Crack fill*), a quantity, a severity, notes and photos. Assessments roll their pins up into totals per service, which flow into the job's estimate.

A job has at most one **open** assessment. New pins on the job join it until it is completed. Completing an assessment saves a handoff snapshot for the operations team; it can be reopened.

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

## The assessment object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Assessment id. |
| `job_id` | integer | The job. |
| `location_id` | integer or null | The job's site. |
| `scope` | string | `job` (one scope of work) or `location` (a whole-site walkthrough). |
| `title`, `notes` | string | |
| `status` | string | `open` or `completed`. |
| `pin_count` | integer | Pins in the assessment. |
| `created_by` | object | `{id, name}`. |
| `completed_at`, `created_at`, `updated_at` | string | UTC, ISO 8601 with `Z`. |

## The pin object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Pin id. |
| `job_id`, `assessment_id` | integer | `assessment_id` is null for pins dropped outside an assessment. |
| `label` | string | Display name: the pin's `name`, else "Pothole repair 2", else "Pin 3". |
| `name` | string or null | A name someone gave the pin. |
| `latitude`, `longitude` | number | Where it is. |
| `service_type` | object or null | `{id, name}`: what the pin is for. See [List assessment services](#list-assessment-services). |
| `tag` | object or null | `{id, name}` for pins recorded with a photo tag instead of a service. |
| `color` | string | Map color. |
| `severity` | string | `low`, `medium` or `high`. |
| `size` | string | `small`, `medium` or `large`. |
| `notes` | string or null | |
| `visibility` | string | `internal` (team only) or `external` (may show on customer maps and reports). |
| `keep_for_proposal` | boolean | Kept for the proposal. |
| `measurement` | object | `type` (`none`, `count`, `sqft`, `dimensions`, `volume`, `lf`), `value`, `unit` (`EA`, `SF`, `LF`, `CY`) and `data` (dimensions, drawn shapes and form answers as captured). |
| `ai_severity`, `ai_rationale` | string or null | Automatic grade, when one was run. |
| `photos` | array | `{id, url, thumbnail_url, title, caption, stage, visibility, is_video, taken_at}`. |
| `created_by_user_id` | integer | |
| `created_at`, `updated_at` | string | UTC, ISO 8601 with `Z`. |

## Assessments

### List site assessments

`GET /v1/site-assessments`

**Permission:** Read jobs

Most recently changed first.

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | No | Only this job. |
| `location_id` | integer | No | Only this site. |
| `status` | string | No | `open` or `completed`. |
| `scope` | string | No | `job` or `location`. |
| `updated_after` | string | No | ISO 8601. Assessments changed after it, oldest first. |
| `limit`, `offset` | integer | No | Default 50 (max 200) and 0. |

```bash
curl "https://trutec.ai/api/v1/site-assessments?job_id=4812" \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": [
    {
      "id": 702,
      "job_id": 4812,
      "location_id": 2290,
      "scope": "job",
      "title": "Spring walkthrough",
      "notes": null,
      "status": "open",
      "pin_count": 2,
      "created_by": {"id": 311, "name": "Dana Ortiz"},
      "completed_at": null,
      "created_at": "2026-10-12T15:04:11Z",
      "updated_at": "2026-10-12T15:30:52Z"
    }
  ],
  "pagination": {"total": 1, "limit": 50, "offset": 0}
}
```

A pin added to an assessment does not change the assessment's `updated_at`. To sync pins, use [List pins](#list-pins) with `updated_after`.

### Get a site assessment

`GET /v1/site-assessments/{assessment_id}`

**Permission:** Read jobs

The assessment with totals per service (grouped the same way as the job page's assessment card) and every pin. Add `include_pins=false` for the totals only.

```json
{
  "data": {
    "id": 702,
    "job_id": 4812,
    "title": "Spring walkthrough",
    "status": "open",
    "pin_count": 2,
    "services": [
      {"label": "Pothole repair", "color": "#f97316", "pin_count": 1, "photo_count": 1,
       "quantities": [{"unit": "EA", "value": 3}], "severities": {"high": 1}, "pin_ids": [15520]},
      {"label": "Crack fill", "color": "#0ea5e9", "pin_count": 1, "photo_count": 0,
       "quantities": [{"unit": "LF", "value": 120.0}], "severities": {"medium": 1}, "pin_ids": [15521]}
    ],
    "severity_totals": {"low": 0, "medium": 1, "high": 1},
    "pins": [
      {
        "id": 15520,
        "job_id": 4812,
        "assessment_id": 702,
        "label": "Entrance potholes",
        "name": "Entrance potholes",
        "latitude": 30.40121,
        "longitude": -97.72088,
        "service_type": {"id": 41, "name": "Pothole repair"},
        "tag": null,
        "color": "#f97316",
        "severity": "high",
        "size": "medium",
        "notes": "Three potholes at the entrance, one deep",
        "visibility": "external",
        "keep_for_proposal": true,
        "measurement": {"type": "count", "value": 3, "unit": "EA", "data": {}},
        "photos": [
          {"id": 9021, "url": "https://media.example.com/photos/job_4812/c11e0a.jpg",
           "thumbnail_url": "https://media.example.com/photos/job_4812/c11e0a_thumb.jpg",
           "title": null, "caption": "Deepest one", "stage": "before", "visibility": "external",
           "is_video": false, "taken_at": "2026-10-12T14:58:02Z"}
        ],
        "created_by_user_id": 311,
        "created_at": "2026-10-12T15:04:11Z",
        "updated_at": "2026-10-12T15:04:11Z"
      }
    ]
  }
}
```

### Start a site assessment

`POST /v1/site-assessments`

**Permission:** Write jobs

Starts an assessment on a job, the same as **Start assessment** on the job page. Pins already on the job that belong to no assessment join it. If the job already has an open assessment, that one is returned with `200` and `"created": false`.

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | Yes | The job. |
| `title` | string | No | Default "Site Assessment · Oct 12, 2026". |
| `notes` | string | No | |

```bash
curl -X POST https://trutec.ai/api/v1/site-assessments \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"job_id": 4812, "title": "Spring walkthrough"}'
```

Returns `201` with `{"data": assessment, "created": true}`. Whole-site (`location`) walkthroughs are started in the app.

### Update a site assessment

`PATCH /v1/site-assessments/{assessment_id}`

**Permission:** Write jobs

Change `title` (cannot be empty) or `notes`. Returns `200` with `{"data": assessment}`.

### Complete a site assessment

`POST /v1/site-assessments/{assessment_id}/complete`

**Permission:** Write jobs

Closes the assessment: new pins on the job stop joining it, loose pins on the job are attached, and a handoff snapshot is saved for the operations team. Completing a completed assessment changes nothing. Returns `200` with the assessment.

### Reopen a site assessment

`POST /v1/site-assessments/{assessment_id}/reopen`

**Permission:** Write jobs

New pins on the job join it again. Returns `200` with the assessment.

### List assessment services

`GET /v1/assessment-services`

**Permission:** Read jobs

The services a pin can be for, as set up in TruTec under Site Assessment settings, with the measurement each one takes.

```json
{
  "data": [
    {"id": 41, "name": "Pothole repair", "group": "Asphalt repair", "color": "#f97316", "measurement_type": "count"},
    {"id": 42, "name": "Crack fill", "group": "Asphalt repair", "color": "#0ea5e9", "measurement_type": "lf"}
  ]
}
```

## Pins

### List pins

`GET /v1/assessment-pins`

**Permission:** Read jobs

Pins across the workspace, newest first.

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | No | Only this job. |
| `assessment_id` | integer | No | Only this assessment. |
| `service_type_id` | integer | No | Only this service. |
| `updated_after` | string | No | ISO 8601. Pins changed after it, oldest first. |
| `limit`, `offset` | integer | No | Default 50 (max 200) and 0. |

### Get a pin

`GET /v1/assessment-pins/{pin_id}`

**Permission:** Read jobs

Returns `{"data": pin}`.

### Add a pin

`POST /v1/assessment-pins`

**Permission:** Write jobs

Drops a pin the way the field capture page does. The pin joins `assessment_id` if you send it (it must be open), otherwise the job's open assessment. A pin for a service on a job that has never had an assessment starts one. Pin quantities reach the job's draft estimate like pins captured in the field.

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | Yes, unless `assessment_id` | The job. |
| `assessment_id` | integer | No | An open assessment on that job. |
| `latitude`, `longitude` | number | Yes | Location. |
| `service_type_id` | integer | No | What it is for. |
| `measurement_type` | string | No | `none`, `count`, `sqft`, `dimensions`, `volume` or `lf`. |
| `measurement_value` | number | No | The quantity. |
| `measurement_unit` | string | No | `EA`, `SF`, `LF` or `CY`. |
| `length_ft`, `width_ft`, `depth_in` | number | No | Dimensions instead of a value, for `dimensions` and `volume` services. |
| `measurement_data` | object | No | Extra measurement detail, as the capture page stores it. |
| `severity` | string | No | `low`, `medium` (default) or `high`. |
| `size` | string | No | `small`, `medium` (default) or `large`. |
| `notes` | string | No | |
| `name` | string | No | Pin name, up to 80 characters. |
| `visibility` | string | No | `external` (default) or `internal`. |
| `keep_for_proposal` | boolean | No | |
| `tag_type_id` | integer | No | A photo tag instead of a service. |
| `defect_type_id` | integer | No | A standard defect type. |

```bash
curl -X POST https://trutec.ai/api/v1/assessment-pins \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "assessment_id": 702,
    "latitude": 30.40121,
    "longitude": -97.72088,
    "service_type_id": 41,
    "measurement_type": "count",
    "measurement_value": 3,
    "measurement_unit": "EA",
    "severity": "high",
    "notes": "Three potholes at the entrance",
    "name": "Entrance potholes"
  }'
```

Returns `201` with `{"data": pin}`.

| Status | `error` |
|---|---|
| 400 | `latitude and longitude are required`, `job_id or assessment_id is required`, a field message such as `severity must be one of: low, medium, high`, `Unknown fields` |
| 404 | `Job not found`, `Site assessment not found`, `Assessment service not found`, `Photo tag not found` |
| 409 | `Assessment is completed`. Reopen it first. |

### Update a pin

`PATCH /v1/assessment-pins/{pin_id}`

**Permission:** Write jobs

Send only what changes: any field from [Add a pin](#add-a-pin) except `job_id` and `assessment_id`. Moving a pin also moves any shape drawn around it. Returns `200` with `{"data": pin}`.

```bash
curl -X PATCH https://trutec.ai/api/v1/assessment-pins/15520 \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"notes": "Three potholes, one deep", "severity": "medium"}'
```

### Delete a pin

`DELETE /v1/assessment-pins/{pin_id}`

**Permission:** Write jobs and Delete records

Deletes the pin and its photos from the assessment and updates the job's draft estimate. Returns `204`. This cannot be undone.

### Add a photo to a pin

`POST /v1/assessment-pins/{pin_id}/photos`

**Permission:** Write jobs

Multipart field `photo` (repeat it for several), or JSON with `url` (a public `https://` image link, up to 15 MB). Optional: `caption`, `title`, `stage` (`before`, `during`, `after`, `general`), `visibility` (`external`, `internal`), `taken_at`, `client_uid` (repeat uploads with the same id are not duplicated).

```bash
curl -X POST https://trutec.ai/api/v1/assessment-pins/15520/photos \
  -H "X-API-Key: sk_your_api_key_here" \
  -F "photo=@pothole.jpg" -F "caption=Deepest one" -F "stage=before"
```

Returns `201` with the pin and its photos, plus `photo_ids` of the photos just added. Errors: `400 No photo provided` or `Invalid photo`, `422 url is not an image`, `503` when photo storage is briefly unavailable (retry).

## Copy an assessment

### Duplicate a site assessment

`POST /v1/site-assessments/{assessment_id}/duplicate`

**Permission:** Write jobs

The same as **Duplicate** on the assessment page. Makes a new assessment on the same job with a copy of every pin and its photos, so one walkthrough can be split into phases: duplicate it, then delete the pins that don't belong in each copy (`DELETE /v1/assessment-pins/{pin_id}`).

The copy is saved as **completed**, so new pins on the job keep joining the open assessment; reopen the copy if new pins should join it instead. Photos are shared, not uploaded again, and deleting a pin or photo in one copy leaves the other untouched.

| Name | Type | Required | Description |
|---|---|---|---|
| `title` | string | No | Title of the copy. Default is the original's title followed by "(Copy)". |

```bash
curl -X POST https://trutec.ai/api/v1/site-assessments/702/duplicate \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"title": "Spring walkthrough, phase 2"}'
```

Returns `201` with `{"data": assessment, "pins_copied": 14, "photos_copied": 22}`. Errors: `400` for an unknown field or an empty `title`, `404` when the assessment is not in your workspace.

Assessments can't be deleted on their own (in TruTec they go when their job is deleted). To take pins out of one, delete the pins.
