# Condition ratings (PASER & PCI)

Send an image, or a location for TruTec to pull aerial imagery for, and get back a PASER rating (1 to 10) or a Pavement Condition Index (0 to 100). Each result lists the distresses found and a recommended treatment.

Ratings follow PASER and ASTM D6433. They are AI estimates from imagery: a screening-level rating, not a field survey.

A rating usually takes 10 to 20 seconds. Set your HTTP timeout to at least 90 seconds.

## Endpoints

### Rate PASER

`POST /v1/condition/paser`

**Permission:** Read measurements

### Rate PCI

`POST /v1/condition/pci`

**Permission:** Read measurements

Both endpoints take the same fields, as JSON or as `multipart/form-data` for a file upload. In a form, send `polygon` as a JSON string.

Send **one** image source. If you send an image, location fields are ignored. Otherwise TruTec uses the first of `polygon`, `lat` + `lng`, `address`.

| Name | Type | Required | Description |
|---|---|---|---|
| `image` | file or string | One image source | Your own photo: JPEG, PNG or WebP, up to 15 MB and 60 megapixels, at least 64 px. As a string, plain base64 or a `data:image/...;base64,` URI. Aerial, drone and ground photos all work. |
| `image_url` | string | One image source | A public `https://` URL to an image (up to 2,000 characters). |
| `polygon` | array or object | One image source | The area to rate, as `[[lat, lng], ...]` (3 or more points) or a GeoJSON `Polygon` or `Feature` (GeoJSON order `[lng, lat]`). TruTec frames the aerial to it and rates only pavement inside it. Recommended when you have a boundary. |
| `lat`, `lng` | number | One image source | Center point for the aerial. |
| `address` | string | One image source | Street address, geocoded to a center point. |
| `imagery_source` | string | No | `google` (default) or `nearmap`, when TruTec fetches the aerial. Nearmap uses the newest Nearmap survey at the point and needs a plan that includes Nearmap. |
| `zoom` | integer | No | 16 to 21. By default, fits the polygon (up to 20), or 20 for a point. |
| `pavement_type` | string | No | `auto` (default), `asphalt` or `concrete`. |
| `notes` | string | No | Context for the rating, up to 1,000 characters, e.g. `"Seal coated 2021, heavy truck traffic at the dock"`. |
| `return_image` | boolean | No | Return the exact JPEG that was rated in `image.jpeg_base64`, with the polygon outlined. Not available with `image_url`. |

One call rates one frame. A polygon up to about 120 m (400 ft) across is rated at full detail. Larger polygons are fitted at a lower zoom and lose crack-level detail, so split large properties into sections.

```bash
curl https://trutec.ai/api/v1/condition/pci \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "polygon": [[30.40112, -97.72231], [30.40109, -97.72058], [30.39985, -97.72061], [30.39988, -97.72236]],
    "imagery_source": "nearmap"
  }'
```

Rate your own photo:

```bash
curl https://trutec.ai/api/v1/condition/paser \
  -H "X-API-Key: sk_your_api_key_here" \
  -F "image=@north-lot.jpg" \
  -F "pavement_type=asphalt"
```

Python:

```python
import os
import requests

resp = requests.post(
    "https://trutec.ai/api/v1/condition/paser",
    headers={"X-API-Key": os.environ["TRUTEC_API_KEY"]},
    json={"lat": 30.4005, "lng": -97.7215, "imagery_source": "nearmap"},
    timeout=120,
)
resp.raise_for_status()
result = resp.json()
print(result["paser"]["rating"], result["paser"]["label"], result["image"]["capture_date"])
```

## Response

Use `paser.rating` or `pci.score`. Everything else explains it.

```json
{
  "rating_type": "paser",
  "rubric_version": "1.0",
  "paser": {
    "rating": 6,
    "label": "Good",
    "description": "Slight raveling and traffic wear; cracks 1/4\"–1/2\" open, some closer than 10 ft; ...",
    "recommended_treatment": "Crack fill and seal coat; sound structure.",
    "limited_by": [
      {"distress": "weathering", "severity": "medium", "density_pct": 70.0, "max_rating": 6, "rule": "Weathered surface, traffic wear"}
    ]
  },
  "pavement_type": "asphalt",
  "confidence": 0.6,
  "summary": "The lot is structurally sound with no visible potholes. The surface has oxidized to a light gray. A seal coat would help protect it.",
  "distresses": [
    {"type": "weathering", "label": "Weathering / oxidation", "severity": "medium", "density_pct": 70.0, "density_unit": "area", "where": "Most of the lot"},
    {"type": "raveling", "label": "Raveling", "severity": "low", "density_pct": 15.0, "density_unit": "area", "where": "Drive aisles"}
  ],
  "limitations": ["Tight or hairline cracks may not be visible at this resolution."],
  "image": {
    "source": "nearmap",
    "capture_date": "2026-03-14",
    "survey_id": "88f1e2a0-1234-5678-abcd-1234567890ab",
    "lat": 30.4004850,
    "lng": -97.7214700,
    "zoom": 19,
    "width": 1280,
    "height": 1280,
    "meters_per_pixel": 0.129,
    "outlined": true,
    "view": "aerial",
    "quality": "good",
    "visible_pavement_pct": 85
  },
  "request_id": "9b2f4c1e8a7d4e0f9c3b6a5d2e1f0a9b",
  "rated_at": "2026-10-12T15:04:11.512Z"
}
```

A PCI response has the same envelope with a `pci` block instead of `paser`:

```json
"pci": {
  "score": 85,
  "label": "Satisfactory",
  "strategy": "Preventive maintenance",
  "color": "#4ade80",
  "recommended_treatment": "Crack fill, seal coat and spot repairs.",
  "method": "astm_d6433",
  "deducts": [
    {"distress": "weathering", "severity": "medium", "density_pct": 75.0, "deduct_value": 11.3}
  ]
}
```

| Field | Description |
|---|---|
| `paser.rating` | PASER rating, 1 to 10, with `label`, `description` and `recommended_treatment`. |
| `paser.limited_by` | The distresses holding the rating at this level. Empty when overall surface condition sets the rating. |
| `pci.score` | PCI, 0 to 100. |
| `pci.label`, `strategy`, `color` | PCI band: Good 86–100, Satisfactory 71–85, Fair 56–70, Poor 41–55, Very poor 26–40, Serious 11–25, Failed 0–10. |
| `pci.method` | `astm_d6433` for asphalt; `visual_estimate` for concrete. |
| `pci.deducts` | The deduct value for each distress (asphalt). |
| `pavement_type` | `asphalt` or `concrete`. |
| `confidence` | 0 to 1, reflecting image quality and resolution. |
| `summary` | Two or three plain sentences for a property manager. |
| `distresses[]` | `type`, `label`, `severity` (`low`, `medium`, `high`), `density_pct`, `density_unit` and a plain-language `where`. |
| `distresses[].density_unit` | `area` = % of rated pavement; `linear` = linear feet per 100 sq ft; `pothole_area` = % of area that is pothole; `slabs` = % of concrete slabs. |
| `limitations` | What the image could not show. |
| `image.source` | `google`, `nearmap`, `upload` or `url`. |
| `image.capture_date` | The Nearmap survey's capture date. `null` for Google, which does not publish dates. Not present for your own images. |
| `image.survey_id` | The Nearmap survey used. `null` for Google. |
| `image.view`, `quality`, `visible_pavement_pct` | `aerial`, `ground` or `other`; `good`, `fair` or `poor`; how much pavement could be seen. |
| `request_id` | Quote this when contacting support. |
| `rated_at` | When the rating ran (UTC). |

## Accuracy

- Satellite imagery cannot resolve hairline cracks, so aerial ratings can read high on lots with fine cracking. Nearmap, drone imagery or ground photos show more.
- Crack width and rut depth can't be measured from above. Their severity is judged from what is visible and noted in `limitations`.
- Cars, shadows and trees hide pavement. `visible_pavement_pct` and `confidence` show how much could be assessed.
- Aerial imagery may be months or years old. Use Nearmap for a dated capture, or upload a current photo.

## Usage

Each **successful** rating counts as one call on your API key. Errors are not counted.

## Errors

Errors return `{"error": "<message>", "code": "<code>"}`. `502` and `503` are safe to retry.

| Status | `code` | When |
|---|---|---|
| 400 | `missing_image` | No image and no location sent. |
| 400 | `invalid_request` | Bad `polygon`, `lat`/`lng`, `zoom`, `imagery_source`, `pavement_type` or `image_url`. |
| 400 | `invalid_image` | The image is empty, can't be decoded, or is under 64 px. |
| 403 | `forbidden` | The key lacks Read measurements. |
| 403 | `upgrade_required` | Nearmap requested on a plan without Nearmap. |
| 413 | `image_too_large` | Over 15 MB or 60 megapixels. |
| 422 | `no_pavement_visible` | Nothing ratable in the image. |
| 422 | `no_imagery_coverage` | Nearmap has no survey at that location. Retry with `google`. |
| 422 | `address_not_found` | The address could not be found. |
| 422 | `image_rejected` | The image could not be rated. Try a clearer or closer image. |
| 500 | `imagery_unavailable` | Imagery is not configured on the server. |
| 502 | `imagery_unavailable`, `geocode_failed`, `rating_failed` | A step failed. Retry. |
| 503 | `service_unavailable` | Temporarily unavailable. Retry after a minute. |

Authentication errors (`401`), plan errors (`402`/`403`) and usage-limit errors (`429`) use the standard API error format without `code`.
