> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arterial.us/llms.txt
> Use this file to discover all available pages before exploring further.

# Feature Service

> Connect ArcGIS, QGIS, or any client that speaks the Esri FeatureServer protocol to Arterial's hazards, coverage, detections, and service-area layers.

Arterial publishes a read-only **ArcGIS FeatureServer** that mirrors the
operational data from the dashboard so your existing GIS stack can
consume it directly. The same layers are also available as a
**multi-shapefile zip** through the **Settings → Workspace → GIS exports**
panel.

This is the integration surface for analysts who want hazards, coverage
cells, detections, and the org's service-area polygon to live alongside
their other reference data.

## Service URL

```
https://app.arterial.us/api/arcgis/arterial/FeatureServer
```

The service is read-only and authenticated by a long-lived token issued
from the dashboard. Hand the token over via a `?token=` query
parameter; ArcGIS clients can paste the URL with the token appended
(`...?token=artgis_…`) into **Add Data → From URL**.

The legacy hazards-only path
`https://app.arterial.us/api/arcgis/hazards/FeatureServer/0` continues
to work unchanged for clients that already integrated against it.

## Layers

The service exposes **four parent layers** plus **seven pre-filtered
sub-layers** for clients that prefer to subscribe to a slice rather
than apply a `where=` clause.

| #  | Name                          | Geometry | Notes                                                                          |
| -- | ----------------------------- | -------- | ------------------------------------------------------------------------------ |
| 0  | Hazards                       | Point    | Every hazard owned by your org.                                                |
| 1  | Coverage Cells                | Polygon  | H3 resolution-9 hexes inside your service area, with read counts.              |
| 3  | Detections                    | Point    | Raw detections (pre-rollup) inside your service area.                          |
| 4  | Service Area                  | Polygon  | Your org's boundary as a single polygon.                                       |
| 5  | Hazards — Active              | Point    | Hazards whose work order is `unfiled` or `scheduled`.                          |
| 6  | Hazards — Resolved            | Point    | Hazards whose work order is `complete`.                                        |
| 7  | Coverage Cells — Fresh (≤7d)  | Polygon  | Cells last visited within seven days.                                          |
| 8  | Coverage Cells — Stale (>30d) | Polygon  | Cells whose last visit is older than thirty days.                              |
| 9  | Coverage Cells — Gap (never)  | Polygon  | Cells inside the service area never visited in the lookback window.            |
| 10 | Detections — Rolled Up        | Point    | Detections that already belong to a hazard.                                    |
| 11 | Detections — Orphan           | Point    | Detections inside the service area that aren't yet associated with any hazard. |

## Service manifest

```http theme={null}
GET /api/arcgis/arterial/FeatureServer?f=json&token=...
```

Returns the service descriptor — every client uses this to learn the
layer set. Each entry includes its numeric `id`, `parentLayerId`
(`-1` for top-level layers), and the canonical layer URL.

## Layer descriptor

```http theme={null}
GET /api/arcgis/arterial/FeatureServer/{layerId}?f=json&token=...
```

Returns the field schema, geometry type, and pagination caps for one
layer. Field metadata uses the standard `esriFieldType*` identifiers
(`esriFieldTypeOID`, `esriFieldTypeString`, `esriFieldTypeInteger`,
`esriFieldTypeDouble`, `esriFieldTypeSmallInteger`).

## Querying features

```http theme={null}
GET /api/arcgis/arterial/FeatureServer/{layerId}/query
    ?f=json
    &where=1%3D1
    &outFields=*
    &resultOffset=0
    &resultRecordCount=1000
    &returnGeometry=true
    &token=...
```

Supported parameters:

* **`f`** — `json` (default), `pjson`, or `geojson`. Use `geojson` to
  pull the layer straight into QGIS or a Mapbox style.
* **`where`** — must be `1=1`. Custom predicates are not supported on
  this read-only mirror; subscribe to a sub-layer instead.
* **`outFields`** — `*` for all attributes, or a comma-separated list
  of field names.
* **`objectIds`** — comma-separated list of `OBJECTID` values to fetch.
* **`resultOffset` / `resultRecordCount`** — pagination. The maximum
  page size is 1000 records.
* **`returnGeometry`** — set `false` to omit geometry (handy for
  attribute-only joins).
* **`returnCountOnly`** — returns `{ count: N }` and skips the
  feature payload.
* **`returnIdsOnly`** — returns the array of `OBJECTID` values only.

## Attribute schemas

### Hazards (`/0`, `/5`, `/6`)

| Field                       | Type     | Notes                                                 |
| --------------------------- | -------- | ----------------------------------------------------- |
| `OBJECTID`                  | OID      |                                                       |
| `hazard_uuid`               | String   | Stable UUID.                                          |
| `type`                      | String   | Hazard category (pothole, crack, …).                  |
| `priority`                  | Double   | Priority score.                                       |
| `status`                    | String   | `unfiled` / `scheduled` / `complete`.                 |
| `last_detected_at`          | String   | ISO 8601.                                             |
| `detection_count`           | Integer  | Detections rolled into this hazard.                   |
| `owned_by_org`              | String   | Org display name.                                     |
| `viewer_redacted`           | SmallInt | `1` when the viewer org has read-but-redacted access. |
| `latitude`                  | Double   |                                                       |
| `longitude`                 | Double   |                                                       |
| `address` … `address_state` | String   | When known.                                           |

### Coverage cells (`/1`, `/7`, `/8`, `/9`)

| Field                  | Type     | Notes                                 |
| ---------------------- | -------- | ------------------------------------- |
| `OBJECTID`             | OID      |                                       |
| `h3_cell`              | String   | 15-char hex H3 index at resolution 9. |
| `h3_res`               | SmallInt | Always 9 in this release.             |
| `reads_count`          | Integer  | Total reads in the lookback window.   |
| `last_read_at`         | String   | ISO 8601. `null` for gap cells.       |
| `days_since_last_read` | Integer  | `-1` for gap cells.                   |

### Detections (`/3`, `/10`, `/11`)

| Field            | Type     | Notes                                           |
| ---------------- | -------- | ----------------------------------------------- |
| `OBJECTID`       | OID      |                                                 |
| `detection_uuid` | String   |                                                 |
| `type`           | String   | Detection category.                             |
| `confidence`     | Double   | Model confidence score.                         |
| `is_approved`    | SmallInt | `1` when an operator approved.                  |
| `captured_at`    | String   | ISO 8601.                                       |
| `device_uuid`    | String   |                                                 |
| `hazard_uuid`    | String   | Parent hazard, or `null` for orphan detections. |
| `model_version`  | String   |                                                 |
| `latitude`       | Double   |                                                 |
| `longitude`      | Double   |                                                 |

### Service area (`/4`)

| Field      | Type   | Notes                              |
| ---------- | ------ | ---------------------------------- |
| `OBJECTID` | OID    |                                    |
| `org_uuid` | String |                                    |
| `org_name` | String |                                    |
| `area_km2` | Double | Polygon area in square kilometres. |

## Adding the service to ArcGIS Online

1. In ArcGIS Online, open **Map Viewer** and choose **Add → Add layer
   from URL**.
2. Paste the service URL with your token appended:
   `https://app.arterial.us/api/arcgis/arterial/FeatureServer?token=artgis_…`
3. ArcGIS reads the manifest and shows the layer list. Add as many as
   you want — top-level layers, sub-layers, or both.
4. Save the map. When the underlying data updates, refresh the layer
   from the layer settings menu.

## Adding the service to QGIS

1. Open **Layer → Add Layer → Add ArcGIS REST Server Layer**.
2. Use **New Connection**, paste the service URL (without `/0` —
   FeatureServer root only), and store the token in the query string.
3. Choose the layers you need from the connection's layer list.

## Multi-layer shapefile export

The dashboard's **GIS exports** panel issues an export job that
produces a single `.zip` file containing one shapefile (`.shp`,
`.shx`, `.dbf`, `.prj`) per requested layer, plus a top-level
`manifest.json` describing the bundle:

```json theme={null}
{
  "version": 1,
  "generatedAt": "2026-05-10T17:42:00.000Z",
  "layers": [
    {
      "id": 0,
      "key": "hazards.all",
      "name": "Hazards",
      "geometryType": "esriGeometryPoint",
      "featureCount": 1842
    },
    {
      "id": 4,
      "key": "service_area.all",
      "name": "Service Area",
      "geometryType": "esriGeometryPolygon",
      "featureCount": 1
    }
  ]
}
```

Each layer's shapefile lives under a folder named after the stable
layer key with dots replaced by underscores
(`hazards_all/`, `coverage_cells_fresh/`, …) so unzip-into-folder
workflows produce a clean per-layer directory tree.

The legacy single-layer export (when no `layers` field is supplied)
keeps producing `arterial-hazards.zip` with the same shape it always
had — your existing automation does not need to change.

## Multi-layer ESRI JSON export

When the export format is `esri_json` and more than one layer is
requested, the response is a versioned envelope:

```json theme={null}
{
  "version": 1,
  "exportedAt": "2026-05-10T17:42:00.000Z",
  "layers": [
    {
      "id": 0,
      "key": "hazards.all",
      "name": "Hazards",
      "geometryType": "esriGeometryPoint",
      "fields": [{ "name": "OBJECTID", "alias": "Object ID", "type": "esriFieldTypeOID" }, …],
      "result": {
        "exceededTransferLimit": false,
        "totalCount": 1842,
        "features": [{ "attributes": { … }, "geometry": { … } }, …]
      }
    },
    …
  ]
}
```

Single-layer ESRI JSON exports continue to use the flat
`{ features: [...], fields: [...], … }` shape for back-compat.

## Limits and quirks

* Read-only — the service does not support `applyEdits`.
* `where=1=1` is the only supported predicate. Filter by subscribing to
  the relevant sub-layer instead.
* Page size is capped at **1000** records. Iterate `resultOffset` to
  stream large layers.
* Coverage cells query against an H3 resolution-9 grid clipped to your
  service area. The lookback window for a single query is one year.
* Orphan detections are scoped to your org by checking each
  detection's `(latitude, longitude)` against your service-area
  polygon — a detection that drifts outside the polygon will not
  appear in `/11`.
