# Sparrow DroneID API Reference

**Version:** 1.0.0
**Base URL:** `http://<host>:<port>/api`
**Default Port:** 8097
**Content Type:** All API responses are `application/json` unless otherwise noted.

## Table of Contents

- [Authentication](#authentication)
- [Error Handling](#error-handling)
- [System](#system)
- [Monitoring](#monitoring)
- [Detections (Live)](#detections-live)
- [History / Replay](#history--replay)
- [Export](#export)
- [Alerts](#alerts)
- [Geozones](#geozones)
- [GPS](#gps)
- [Cursor on Target (CoT)](#cursor-on-target-cot)
- [Map Tiles](#map-tiles)
- [Data Maintenance](#data-maintenance)
- [Settings](#settings)
- [Data Models](#data-models)

---

## Authentication

Authentication is optional and controlled by two mechanisms in settings:

1. **IP/Subnet Allowlist** (`allowed_ips`): Comma-separated list of IPs or CIDR subnets. When set, connections from unlisted addresses receive `403 Forbidden`. Empty = allow all.

2. **Bearer Token** (`auth_token`): A static API token configured in settings. When set, all API requests must include:
   ```
   Authorization: Bearer <token>
   ```
   Empty = no token required.

Both mechanisms can be used independently or together. When both are set, a request must satisfy both (correct IP AND valid token).

**Unauthorized responses:**

```json
{
  "errcode": 401,
  "errmsg": "Authentication required"
}
```

```json
{
  "errcode": 403,
  "errmsg": "Connections not authorized from your IP address"
}
```

---

## Error Handling

All API errors return a JSON object with `errcode` and `errmsg` fields. HTTP status codes are used appropriately (200, 400, 401, 403, 404, 500).

**Standard error response:**

```json
{
  "errcode": <int>,
  "errmsg": "<human-readable error message>"
}
```

**Common error codes:**

| errcode | Meaning |
|---------|---------|
| 0 | Success (no error) |
| 1 | Bad request / invalid parameters |
| 2 | Resource not found |
| 3 | Operation not permitted in current state |
| 5 | Internal server error |

---

## System

### GET /api/status

Returns the current application state.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "version": "1.0.0",
  "monitoring": false,
  "monitor_interface": "wlan0mon",
  "monitor_channel": 6,
  "monitor_duration_seconds": 0,
  "frame_count": 0,
  "ble_enabled": true,
  "ble_frame_count": 0,
  "active_drone_count": 0,
  "total_drones_seen": 0,
  "gps_fix": true,
  "receiver_lat": 38.8977,
  "receiver_lon": -77.0365,
  "receiver_alt": 15.0,
  "uptime_seconds": 3600,
  "db_size_bytes": 1048576,
  "retention_days": 14
}
```

---

## Monitoring

### GET /api/interfaces

Enumerate available WiFi interfaces and their capabilities.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "interfaces": [
    {
      "name": "wlan0",
      "mac_address": "aa:bb:cc:dd:ee:ff",
      "mode": "managed",
      "monitor_capable": true,
      "driver": "ath9k_htc",
      "phy": "phy0"
    },
    {
      "name": "wlan1",
      "mac_address": "11:22:33:44:55:66",
      "mode": "monitor",
      "monitor_capable": true,
      "driver": "rtl8812au",
      "phy": "phy1"
    }
  ]
}
```

### POST /api/monitor/start

Start Remote ID capture on the specified interface. Switches the interface to monitor mode on channel 6 if not already in monitor mode. BLE capture is started automatically alongside WiFi capture (no additional configuration required).

**Request body:**

```json
{
  "interface": "wlan0"
}
```

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "interface": "wlan0mon",
  "channel": 6,
  "status": "monitoring"
}
```

**Error conditions:**
- Interface not found → `errcode: 2`
- Interface does not support monitor mode → `errcode: 3`
- Already monitoring → `errcode: 3`
- tcpdump failed to start → `errcode: 5`

### POST /api/monitor/stop

Stop the current capture session. Restores the interface to managed mode.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "status": "stopped",
  "session_duration_seconds": 1847,
  "frames_captured": 23456,
  "drones_detected": 3
}
```

### GET /api/monitor/status

Returns current monitoring session status.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "monitoring": true,
  "interface": "wlan0mon",
  "channel": 6,
  "started_at": "2026-03-17T14:30:00Z",
  "duration_seconds": 1847,
  "frame_count": 23456,
  "droneid_frame_count": 142,
  "ble_enabled": true,
  "ble_frame_count": 38,
  "capture_errors": 0
}
```

---

## Detections (Live)

### GET /api/drones

Returns all currently active (non-aged-out) drones with the latest data and derived fields.

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `max_age` | int | 180 | Maximum seconds since last seen to include. 0 = all tracked |

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "receiver": {
    "lat": 38.8977,
    "lon": -77.0365,
    "alt": 15.0,
    "gps_fix": true,
    "source": "gpsd"
  },
  "drones": [
    {
      "serial_number": "1FA123456789ABCD",
      "registration_id": "",
      "id_type": 1,
      "id_type_name": "Serial Number (ANSI/CTA-2063-A)",
      "ua_type": 3,
      "ua_type_name": "Helicopter (or Multirotor)",
      "drone_lat": 38.8984,
      "drone_lon": -77.0352,
      "drone_alt_geo": 57.2,
      "drone_alt_baro": 55.8,
      "drone_height_agl": 42.0,
      "speed": 3.2,
      "direction": 22.5,
      "vertical_speed": 0.0,
      "operator_lat": 38.8971,
      "operator_lon": -77.0368,
      "operator_alt": 14.0,
      "operator_id": "",
      "self_id_text": "Survey flight",
      "mac_address": "aa:bb:cc:dd:ee:ff",
      "rssi": -68,
      "rssi_trend": "stable",
      "protocol": "astm_nan",
      "first_seen": "2026-03-17T14:33:12Z",
      "last_seen": "2026-03-17T14:36:45Z",
      "time_in_area_seconds": 213,
      "derived": {
        "range_m": 152.4,
        "bearing_deg": 47.2,
        "bearing_cardinal": "NE",
        "operator_range_m": 7.3,
        "operator_bearing_deg": 198.5,
        "operator_bearing_cardinal": "SSW",
        "altitude_class": "MEDIUM",
        "state": "active"
      }
    }
  ],
  "counts": {
    "active": 1,
    "aging": 0,
    "stale": 0
  },
  "timestamp": "2026-03-17T14:36:47Z"
}
```

**`protocol` field values:**

| Value | Description |
|-------|-------------|
| `astm_nan` | ASTM F3411 via Wi-Fi NAN action frames |
| `astm_beacon` | ASTM F3411 via Wi-Fi beacon vendor-specific IE (OUI FA:0B:BC) |
| `astm_ble` | ASTM F3411 via BLE Legacy advertising (BT4/BT5, ASTM F3411-22a §A3) |
| `dji_proprietary` | DJI proprietary DroneID via beacon vendor-specific IE (OUI 26:37:12) |

**`id_type` values (ASTM F3411):**

| Value | Name |
|-------|------|
| 0 | None |
| 1 | Serial Number (ANSI/CTA-2063-A) |
| 2 | CAA Assigned Registration ID |
| 3 | UTM Assigned UUID |
| 4 | Specific Session ID |

**`ua_type` values (ASTM F3411):**

| Value | Name |
|-------|------|
| 0 | None / Not Declared |
| 1 | Aeroplane |
| 2 | Helicopter (or Multirotor) |
| 3 | Gyroplane |
| 4 | Hybrid Lift (VTOL) |
| 5 | Ornithopter |
| 6 | Glider |
| 7 | Kite |
| 8 | Free Balloon |
| 9 | Captive Balloon |
| 10 | Airship |
| 11 | Free Fall / Parachute |
| 12 | Rocket |
| 13 | Tethered Powered Aircraft |
| 14 | Ground Obstacle |
| 15 | Other |

**`altitude_class` values (derived from drone_height_agl):**

| Value | AGL Range | Description |
|-------|-----------|-------------|
| `GROUND` | < 3m | On ground or launching/landing |
| `LOW` | 3-30m | Below treeline |
| `MEDIUM` | 30-120m | Normal flight |
| `HIGH` | 120-400m | Near legal ceiling |
| `ILLEGAL` | > 400m (122m) | Above FAA 400ft limit |

**`state` values (derived from last_seen age):**

| Value | Age | Description |
|-------|-----|-------------|
| `active` | 0-30s | Currently broadcasting |
| `aging` | 30-90s | Recently seen |
| `stale` | 90-180s | May have departed |

### GET /api/drones/{serial}

Returns detailed information and recent track for a specific drone.

**Path parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `serial` | string | URL-encoded serial number or registration ID |

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `track_minutes` | int | 5 | Minutes of track history to include |

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "drone": {
    "...": "(same fields as in /api/drones list)"
  },
  "track": [
    {
      "drone_lat": 38.8984,
      "drone_lon": -77.0352,
      "drone_alt_geo": 57.2,
      "drone_height_agl": 42.0,
      "speed": 3.2,
      "direction": 22.5,
      "rssi": -68,
      "timestamp": "2026-03-17T14:36:45Z"
    },
    {
      "drone_lat": 38.8983,
      "drone_lon": -77.0351,
      "drone_alt_geo": 56.8,
      "drone_height_agl": 41.6,
      "speed": 3.1,
      "direction": 24.0,
      "rssi": -69,
      "timestamp": "2026-03-17T14:36:44Z"
    }
  ]
}
```

---

## History / Replay

### GET /api/history

Returns detection records for a time range. Used for replay and forensic review.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from` | string | Yes | ISO 8601 UTC start time |
| `to` | string | Yes | ISO 8601 UTC end time |
| `serial` | string | No | Filter by serial number |
| `limit` | int | No | Maximum records to return (default 10000) |
| `offset` | int | No | Pagination offset (default 0) |

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "records": [
    {
      "serial_number": "1FA123456789ABCD",
      "ua_type": 3,
      "drone_lat": 38.8984,
      "drone_lon": -77.0352,
      "drone_height_agl": 42.0,
      "speed": 3.2,
      "direction": 22.5,
      "operator_lat": 38.8971,
      "operator_lon": -77.0368,
      "rssi": -68,
      "protocol": "astm_nan",
      "receiver_lat": 38.8977,
      "receiver_lon": -77.0365,
      "timestamp": "2026-03-17T14:36:45Z"
    }
  ],
  "total_count": 4523,
  "returned_count": 4523
}
```

### GET /api/history/serials

Returns a list of unique drone serial numbers seen in a time range, with summary statistics.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from` | string | Yes | ISO 8601 UTC start time |
| `to` | string | Yes | ISO 8601 UTC end time |

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "serials": [
    {
      "serial_number": "1FA123456789ABCD",
      "ua_type": 3,
      "ua_type_name": "Helicopter (or Multirotor)",
      "protocol": "astm_nan",
      "first_seen": "2026-03-17T14:33:12Z",
      "last_seen": "2026-03-17T14:36:45Z",
      "detection_count": 213,
      "max_rssi": -62,
      "self_id_text": "Survey flight"
    }
  ]
}
```

### GET /api/history/timeline

Returns time-bucketed summary data for rendering a replay timeline.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from` | string | Yes | ISO 8601 UTC start time |
| `to` | string | Yes | ISO 8601 UTC end time |
| `bucket_seconds` | int | No | Time bucket size (default 10) |

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "buckets": [
    {
      "timestamp": "2026-03-17T14:33:10Z",
      "active_drones": 1,
      "serials": ["1FA123456789ABCD"]
    },
    {
      "timestamp": "2026-03-17T14:33:20Z",
      "active_drones": 2,
      "serials": ["1FA123456789ABCD", "9CC4500001234567"]
    }
  ]
}
```

---

## Export

### GET /api/export/kml

Downloads a KML file containing drone tracks and operator positions for a time range.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from` | string | Yes | ISO 8601 UTC start time |
| `to` | string | Yes | ISO 8601 UTC end time |
| `serial` | string | No | Filter by serial number. Omit for all drones |

**Response:**

- **Content-Type:** `application/vnd.google-earth.kml+xml`
- **Content-Disposition:** `attachment; filename="sparrow_droneid_export_<from>_<to>.kml"`

The KML file contains:
- A folder per drone serial number
- Drone flight track as a `LineString` placemark
- Individual position placemarks with timestamps (for time-enabled playback in Google Earth)
- Operator position placemarks (where available)
- Receiver position placemark
- Altitude data encoded for 3D visualization (`altitudeMode: absolute`)

---

## Alerts

### GET /api/alerts/config

Returns the current alert configuration.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "audio_enabled": true,
  "visual_enabled": true,
  "script_enabled": false,
  "script_path": "",
  "rules": [
    {
      "id": 1,
      "name": "New drone detected",
      "type": "new_drone",
      "enabled": true,
      "audio_sound": "chime",
      "params": {}
    },
    {
      "id": 2,
      "name": "Altitude violation",
      "type": "altitude_max",
      "enabled": true,
      "audio_sound": "alert",
      "params": {
        "max_altitude_m": 122
      }
    },
    {
      "id": 3,
      "name": "Speed violation",
      "type": "speed_max",
      "enabled": true,
      "audio_sound": "alert",
      "params": {
        "max_speed_mps": 44.7
      }
    },
    {
      "id": 4,
      "name": "Signal lost",
      "type": "signal_lost",
      "enabled": true,
      "audio_sound": "chime",
      "params": {
        "timeout_seconds": 180
      }
    }
  ]
}
```

### PUT /api/alerts/config

Update alert configuration.

**Request body:** Same structure as the GET response.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": ""
}
```

### GET /api/alerts/log

Returns the alert event history.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from` | string | No | ISO 8601 UTC start time |
| `to` | string | No | ISO 8601 UTC end time |
| `state` | string | No | Filter by alert state: `ACTIVE`, `ACKNOWLEDGED`, or `RESOLVED`. Omit to return all states |
| `limit` | int | No | Maximum records (default 100) |
| `offset` | int | No | Pagination offset |

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "alerts": [
    {
      "id": 42,
      "timestamp": "2026-03-17T14:33:12Z",
      "alert_type": "new_drone",
      "serial_number": "1FA123456789ABCD",
      "detail": "New drone detected: Helicopter (or Multirotor), RSSI -68 dBm",
      "drone_lat": 38.8984,
      "drone_lon": -77.0352,
      "drone_height_agl": 42.0,
      "state": "ACKNOWLEDGED",
      "acknowledged_by": "OPS1",
      "acknowledged_at": "2026-03-17T14:35:00Z",
      "resolved_at": null
    }
  ],
  "total_count": 7
}
```

**`state` field values:**

| Value | Description |
|-------|-------------|
| `ACTIVE` | Alert is unacknowledged and the condition has not cleared |
| `ACKNOWLEDGED` | Alert has been acknowledged by an operator |
| `RESOLVED` | The triggering condition has cleared (e.g. drone departed) |

**External script invocation:**

When `script_enabled` is true and `script_path` is set, the script is called on each alert event:

```bash
/path/to/script.sh '<json_payload>'
```

The JSON payload matches the alert log entry structure above.

### PUT /api/alerts/acknowledge

Bulk-acknowledge all currently ACTIVE alerts. Sets their state to `ACKNOWLEDGED` and records the operator callsign.

**Request body:**

```json
{
  "operator": "OPS1"
}
```

**Response:**

```json
{
  "errcode": 0,
  "count": 3
}
```

`count` is the number of alerts that were transitioned from `ACTIVE` to `ACKNOWLEDGED`.

### PUT /api/alerts/{alert_id}/acknowledge

Acknowledge a single alert by ID.

**Path parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `alert_id` | int | Alert ID from the alert log |

**Request body:**

```json
{
  "operator": "OPS1"
}
```

**Response:**

```json
{
  "errcode": 0
}
```

**Error conditions:**
- Alert not found → `errcode: 2`
- Alert is not in ACTIVE state → `errcode: 3`

---

## Geozones

Provides airport proximity lookup and FAA no-fly zone data for map overlay and proximity alerting.

### GET /api/geozones/airports

Returns airports within a specified radius of a coordinate.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `lat` | float | Yes | Center latitude (WGS84) |
| `lon` | float | Yes | Center longitude (WGS84) |
| `radius_mi` | float | No | Search radius in miles (default 50) |

**Response:**

```json
{
  "errcode": 0,
  "airports": [
    {
      "ident": "KJFK",
      "name": "John F Kennedy International Airport",
      "type": "large_airport",
      "lat": 40.63972,
      "lon": -73.77889,
      "country": "US"
    },
    {
      "ident": "KLGA",
      "name": "LaGuardia Airport",
      "type": "large_airport",
      "lat": 40.77724,
      "lon": -73.87261,
      "country": "US"
    }
  ]
}
```

**`type` field values:** `large_airport`, `medium_airport`, `small_airport`, `heliport`, `seaplane_base`, `closed`

### GET /api/geozones/nofly

Returns FAA Prohibited and Restricted airspace zones that intersect with the area near the specified coordinate, as GeoJSON features.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `lat` | float | Yes | Query latitude (WGS84) |
| `lon` | float | Yes | Query longitude (WGS84) |

**Response:**

```json
{
  "errcode": 0,
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "P-56A",
        "type": "prohibited",
        "lower_alt": "SFC",
        "upper_alt": "UNLIMITED"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [[[-77.0364, 38.8976], ["..."]]]
      }
    }
  ]
}
```

Each element of `features` is a standard GeoJSON Feature object. The `geometry` may be `Polygon` or `MultiPolygon`. `properties.type` is `prohibited` or `restricted`.

---

## GPS

### GET /api/gps

Returns the current receiver GPS position and source.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "mode": "gpsd",
  "fix": true,
  "latitude": 38.8977,
  "longitude": -77.0365,
  "altitude": 15.0,
  "speed": 0.0
}
```

**`mode` values:** `none`, `gpsd`, `static`

When `mode` is `none`, `fix` will always be `false` and coordinates will be `0.0`. Derived fields on drones (range, bearing) will not be calculated.

---

## Cursor on Target (CoT)

### GET /api/cot/status

Returns the current CoT output configuration and status.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "enabled": false,
  "address": "239.2.3.1",
  "port": 6969,
  "events_sent": 0
}
```

### PUT /api/cot/config

Update CoT output configuration.

**Request body:**

```json
{
  "enabled": true,
  "address": "239.2.3.1",
  "port": 6969
}
```

**Response:**

```json
{
  "errcode": 0,
  "errmsg": ""
}
```

**CoT event format:**

Each active drone emits a CoT event on every position update:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<event version="2.0"
       uid="sparrow-droneid-1FA123456789ABCD"
       type="a-n-A-C-F-q"
       time="2026-03-17T14:36:45Z"
       start="2026-03-17T14:36:45Z"
       stale="2026-03-17T14:36:55Z"
       how="m-f">
  <point lat="38.8984" lon="-77.0352" hae="57.2" ce="10.0" le="15.0"/>
  <detail>
    <track course="22.5" speed="3.2"/>
    <remarks>
      SN:1FA123456789ABCD UA:Multirotor AGL:42.0m Self-ID:Survey flight
    </remarks>
    <contact callsign="DRONE-1FA1"/>
    <__droneid
      serial="1FA123456789ABCD"
      ua_type="3"
      height_agl="42.0"
      operator_lat="38.8971"
      operator_lon="-77.0368"
      operator_id=""
      self_id="Survey flight"
      rssi="-68"
      protocol="astm_nan"/>
  </detail>
</event>
```

**CoT type field:** `a-n-A-C-F-q` = atom, neutral, air, civilian, fixed-wing, UAV. Can be overridden per-drone if threat classification is implemented in a future version.

**Stale time:** 10 seconds after event time (configurable, should be > 2x expected update interval).

---

## Map Tiles

### GET /api/tiles/{source}/{z}/{x}/{y}.png

Proxies and caches map tiles from the configured tile source. The UI requests tiles through this endpoint to enable offline caching.

**Path parameters:**

| Parameter | Description |
|-----------|-------------|
| `source` | Tile source: `osm`, `esri_satellite` |
| `z` | Zoom level |
| `x` | Tile column |
| `y` | Tile row |

**Response:**

- **Content-Type:** `image/png` (or `image/jpeg` for Esri satellite)
- Returns the tile image directly

**Caching behavior:**
- Tiles are cached in `<data_dir>/tiles/<source>/<z>/<x>/<y>.png`
- Cached tiles are served directly without contacting the upstream CDN
- If upstream is unreachable and the tile is cached, the cached version is served
- If upstream is unreachable and the tile is NOT cached, returns `503 Service Unavailable`

**Tile sources:**

| Source | Upstream URL Pattern |
|--------|---------------------|
| `osm` | `https://tile.openstreetmap.org/{z}/{x}/{y}.png` |
| `esri_satellite` | `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}` |

---

## Data Maintenance

### GET /api/data/stats

Returns database statistics.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "db_size_bytes": 1048576,
  "detection_count": 45230,
  "alert_count": 7,
  "unique_serials": 12,
  "oldest_record": "2026-03-03T08:15:00Z",
  "newest_record": "2026-03-17T14:36:45Z",
  "tile_cache_size_bytes": 52428800,
  "retention_days": 14
}
```

### POST /api/data/purge

Manually purge detection and alert data older than a specified date.

**Request body:**

```json
{
  "before": "2026-03-10T00:00:00Z"
}
```

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "detections_deleted": 12450,
  "alerts_deleted": 3
}
```

### POST /api/data/purge-tiles

Purge cached map tiles.

**Request body:**

```json
{
  "source": "osm"
}
```

Omit `source` to purge all cached tiles.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "tiles_deleted": 1523,
  "bytes_freed": 52428800
}
```

---

## Settings

### GET /api/settings

Returns all application settings.

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "settings": {
    "port": 8097,
    "bind_address": "127.0.0.1",
    "https_enabled": false,
    "https_cert_path": "",
    "https_key_path": "",
    "auth_token": "",
    "allowed_ips": "",
    "gps_mode": "none",
    "gps_static_lat": 0.0,
    "gps_static_lon": 0.0,
    "gps_static_alt": 0.0,
    "retention_days": 14,
    "cot_enabled": false,
    "cot_address": "239.2.3.1",
    "cot_port": 6969,
    "alert_audio_enabled": true,
    "alert_visual_enabled": true,
    "alert_script_enabled": false,
    "alert_script_path": "",
    "tile_cache_enabled": true,
    "monitor_interface": "",
    "airport_geozone_radius_mi": 2.0,
    "operator_name": ""
  }
}
```

### PUT /api/settings

Update one or more settings. Only include the keys you want to change.

**Request body:**

```json
{
  "gps_mode": "static",
  "gps_static_lat": 38.8977,
  "gps_static_lon": -77.0365
}
```

**Response:**

```json
{
  "errcode": 0,
  "errmsg": "",
  "settings": { "...(full updated settings object)" },
  "restart_required": false
}
```

**`restart_required`** will be `true` if any of these settings were changed: `port`, `bind_address`, `https_enabled`, `https_cert_path`, `https_key_path`.

**New settings added in this version:**

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `airport_geozone_radius_mi` | float | 2.0 | Radius of airport exclusion zone circles drawn on the map (miles) |
| `operator_name` | string | `""` | Operator callsign pre-filled in alert acknowledgment dialogs and recorded in `acknowledged_by` |

**Note:** The `auth_token` value is write-only. GET responses will return `"auth_token": "(set)"` or `"auth_token": ""` to indicate whether a token is configured, but will never return the actual token value.

---

## Data Models

### DroneIDDevice

The core data model representing a detected drone. All drone-related endpoints return objects with these fields.

| Field | Type | Description |
|-------|------|-------------|
| `serial_number` | string | UAS serial number from Basic ID message |
| `registration_id` | string | CAA registration ID (alternative to serial) |
| `id_type` | int | ID type enum (see [id_type values](#get-apidrones)) |
| `id_type_name` | string | Human-readable ID type |
| `ua_type` | int | UA type enum (see [ua_type values](#get-apidrones)) |
| `ua_type_name` | string | Human-readable UA type |
| `drone_lat` | float | Drone latitude (WGS84) |
| `drone_lon` | float | Drone longitude (WGS84) |
| `drone_alt_geo` | float | Drone geometric altitude (m, WGS84 HAE) |
| `drone_alt_baro` | float | Drone barometric altitude (m) |
| `drone_height_agl` | float | Drone height above ground level (m) |
| `speed` | float | Ground speed (m/s) |
| `direction` | float | Track direction (degrees, 0=North, CW) |
| `vertical_speed` | float | Vertical speed (m/s, positive=up) |
| `operator_lat` | float | Operator latitude (WGS84) |
| `operator_lon` | float | Operator longitude (WGS84) |
| `operator_alt` | float | Operator altitude (m) |
| `operator_id` | string | Operator registration / license ID |
| `self_id_text` | string | Free-form text description from operator |
| `mac_address` | string | Transmitter MAC address (may be randomized) |
| `rssi` | int | Received signal strength (dBm) |
| `rssi_trend` | string | `strengthening`, `stable`, `weakening` |
| `protocol` | string | Detection protocol — `astm_nan`, `astm_beacon`, `astm_ble`, or `dji_proprietary` (see [protocol values](#get-apidrones)) |
| `first_seen` | string | ISO 8601 UTC timestamp of first detection |
| `last_seen` | string | ISO 8601 UTC timestamp of most recent detection |
| `time_in_area_seconds` | int | Duration since first detection |
| `derived` | object | Computed fields (see below) |

### Derived Fields

Computed from drone position, receiver position, and temporal data. Only populated when receiver GPS is available (except `state`).

| Field | Type | Description |
|-------|------|-------------|
| `range_m` | float | Distance from receiver to drone (meters) |
| `bearing_deg` | float | Bearing from receiver to drone (degrees) |
| `bearing_cardinal` | string | Cardinal direction (N, NE, NNE, etc.) |
| `operator_range_m` | float | Distance from receiver to operator (meters) |
| `operator_bearing_deg` | float | Bearing from receiver to operator (degrees) |
| `operator_bearing_cardinal` | string | Cardinal direction to operator |
| `altitude_class` | string | Altitude classification (see values above) |
| `state` | string | Age-based state: `active`, `aging`, `stale` |

### Alert Event

| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Auto-increment ID |
| `timestamp` | string | ISO 8601 UTC |
| `alert_type` | string | `new_drone`, `altitude_max`, `speed_max`, `signal_lost` |
| `serial_number` | string | Drone that triggered the alert |
| `detail` | string | Human-readable alert description |
| `drone_lat` | float | Drone position at time of alert |
| `drone_lon` | float | Drone position at time of alert |
| `drone_height_agl` | float | Drone AGL altitude at time of alert |
| `state` | string | `ACTIVE`, `ACKNOWLEDGED`, or `RESOLVED` |
| `acknowledged_by` | string | Operator callsign that acknowledged the alert, or `null` |
| `acknowledged_at` | string | ISO 8601 UTC timestamp of acknowledgment, or `null` |
| `resolved_at` | string | ISO 8601 UTC timestamp when the condition cleared, or `null` |

---

## Rate Limits

No server-side rate limiting is implemented. Clients polling for live data should use a 1-2 second interval. More frequent polling provides no benefit as drone broadcasts arrive approximately once per second.

## WebSocket

Not implemented. All live data is accessed via polling. This ensures compatibility with any HTTP client for integration purposes.

## Versioning

The API does not use URL versioning. Breaking changes will be communicated via the `version` field in `/api/status`. Non-breaking additions (new fields, new endpoints) may be added without version changes.
