# Vehicle Data Platform API Contract

## Response Envelope

All platform APIs return the same envelope:

```json
{
  "data": {},
  "traceId": "trace-20260704000000.000000",
  "timestamp": 1783094400000
}
```

Pagination uses:

```json
{
  "items": [],
  "total": 0,
  "limit": 20,
  "offset": 0
}
```

## Vehicle Keyword

The platform is vehicle-first. Query APIs should accept `keyword` as the user-facing vehicle selector.

`keyword` may be:

- VIN
- plate
- JT808 phone

The BFF resolves `keyword` through vehicle identity data before querying realtime, history, RAW, or mileage data. `vin` remains accepted as a compatibility alias on data query APIs, but new UI and integrations should send `keyword`.

If a keyword cannot be resolved to a VIN, data APIs must not fabricate a VIN. They should return empty result pages for vehicle data, while `/api/vehicle-service` exposes `lookupResolved=false` and quality issues for follow-up binding work.

## Core Query APIs

### Unified Metric Catalog

```http
GET /api/v2/metrics
```

Returns the server-owned metric whitelist used by rule configuration and future telemetry/history surfaces. Production definitions come from `vehicle_metric_definition` plus `vehicle_metric_protocol_mapping`; migration `005_metric_catalog.sql` only seeds missing rows, so later database configuration is not overwritten by releases. Each metric includes `key`, localized `label`, `unit`, `category`, `valueType`, supported `protocols`, per-protocol `sourceFields`, and `searchable/chartable/alertable` capabilities. Alert rule writes reject unknown, disabled, non-alertable, type-mismatched, or evaluator-unsupported metrics; clients must not invent metric keys or derive SQL fields from user input. `/api/v2/history/metrics` remains the category/column compatibility catalog for the current history page.

### Latest vehicle telemetry

`GET /api/v2/vehicles/{vin}/telemetry/latest`

The path accepts the same VIN, plate or terminal identity resolution used by the vehicle service. After resolving identity once, the server reads `GB32960`, `JT808` and `YUTONG_MQTT` independently and concurrently, at most five newest frames per protocol and 15 frames overall, without running pagination counts. It then returns the newest scalar value for every unified metric or remaining source field. A field that matches `/api/v2/metrics.sourceFields` uses the unified `key`, localized label, unit, type, description and category while retaining its exact `sourceField`. Manufacturer/protocol extensions remain visible under a server-owned dynamic category instead of being discarded or classified by the browser.

Every value includes `protocol`, optional `sourceEndpoint`, `frameId`, device and receive times, `freshnessSeconds`, optional `dataDelaySeconds`, and `quality=good|stale|warning` with an explicit `qualityReason`. `good` requires a successful parser status and a receive time within five minutes; `stale` means the newest value is older than five minutes; parse failures, missing/implausibly future receive times or device/receive divergence over five minutes are `warning`. The response also returns populated categories with counts, `scannedFrames`, `staleAfterSeconds`, `asOf` and selection evidence. Object/array fields are deliberately excluded from the compact latest-value surface and remain available through RAW evidence APIs.

### Dashboard Summary

```http
GET /api/dashboard/summary
```

### V2 Global Monitor

```http
GET /api/v2/monitor/summary?keyword=粤A&protocol=JT808&status=driving
GET /api/v2/monitor/map?zoom=5
GET /api/v2/monitor/map?zoom=13&bounds=113,22,114,24&status=online
```

`summary` and `map` share keyword, protocol and `online|offline|driving|idle` status semantics. `map` accepts `bounds=minLongitude,minLatitude,maxLongitude,maxLatitude`; malformed, non-finite, reversed or out-of-world bounds return `MONITOR_BOUNDS_INVALID`. Below zoom 11 the response remains clustered or mixed. At zoom 11 and above it returns lightweight MassMarks only when the visible point set is at most 2,000; otherwise it adaptively coarsens cells until no more than 2,000 clusters remain. It never returns full telemetry JSON in a point. The browser keeps the normal list at 200 rows, requests map data independently, waits 300ms after `moveend/zoomend`, and drops stale viewport bounds when a direct vehicle search must locate and focus its unique result.

Returns vehicle service KPIs, source distribution, vehicle service status distribution, link health, and realtime backlog.

### Vehicle Service

```http
GET /api/vehicle-service?keyword=粤AG18312
```

Returns one vehicle service view with identity, realtime summary, source coverage, history preview, RAW preview, mileage preview, and quality issues.

The response also carries `profile`, the supplemental business master record from `vehicle_profile`. Gateway-owned VIN, plate and OEM identity remain authoritative and are never overwritten by this record. Profile completeness is calculated across model, vehicle type, company, operation status, access provider, first access time and cumulative runtime.

```http
GET /api/v2/vehicles/{vin}/profile
PUT /api/v2/vehicles/{vin}/profile
POST /api/v2/vehicle-profiles/sync
```

Profile reads require `viewer`; manual writes and bulk sync require `admin`. A manual write uses the returned `version` for optimistic concurrency, rejects unknown VINs and invalid status/time/runtime values, changes the source to `manual`, and creates a versioned `vehicle_profile_audit` snapshot. Supported operation statuses are `unknown`, `active`, `inactive`, `maintenance`, and `retired`.

The controlled sync endpoint accepts 1–500 complete supplemental profiles per request with required `sourceSystem` and `sourceVersion`, `conflictPolicy=preserve|overwrite`, and `dryRun`. VIN identity must already exist in the gateway-owned `vehicle_identity_binding`; missing identities are reported per item and never invented. The default `preserve` policy skips manual records and records owned by another external system. `overwrite` is an explicit administrator takeover. Repeating the same source/version and identical payload is `unchanged`; changing a payload under the same source/version is `conflict_source_version`, preventing a mutable upstream snapshot from silently defeating idempotency. Created and updated profiles receive `source_system`, `source_version`, `synced_at`, optimistic profile version, authenticated actor, and an immutable `sync_created` or `sync_updated` audit snapshot. `dryRun=true` uses the same identity, ownership, validation and locking path but rolls back all writes.

The admin UI accepts UTF-8 CSV with the exact header below, parses quoted fields and validates duplicate VIN, status and integer runtime before upload. It renders only aggregate counts plus the first 20 conflicts, so a 500-row batch does not create a large React table.

```text
vin,modelName,vehicleType,companyName,operationStatus,accessProvider,firstAccessAt,runtimeSeconds
```

`/api/vehicles/detail` remains available as a compatibility alias. New UI and integrations should use `/api/vehicle-service` to make the vehicle-first boundary explicit.

`sourceConsistency` is the vehicle-level diagnosis across GB32960, JT808, and Yutong MQTT sources. The three protocols are source evidence for one vehicle service, so callers should display the diagnosis result instead of asking users to compare protocol rows manually.

```json
{
  "sourceConsistency": {
    "scope": "detail",
    "sourceCount": 3,
    "onlineSourceCount": 2,
    "locatedSourceCount": 2,
    "missingProtocols": ["YUTONG_MQTT"],
    "mileageDeltaKm": 0.8,
    "sourceTimeDeltaSeconds": 42,
    "status": "degraded",
    "severity": "warning",
    "title": "来源不完整",
    "detail": "2/3 个来源在线，车辆服务可用但缺少 YUTONG_MQTT 来源。"
  }
}
```

`missingProtocols` lists canonical source evidence that is completely absent for the vehicle service, so callers can distinguish a missing source from an offline-but-known source. `status` values are `consistent`, `degraded`, `offline`, `single_source`, `no_source`, `mileage_divergent`, and `time_divergent`. `severity` values are `ok`, `warning`, and `error`. `/api/vehicle-service/overview` and `/api/vehicle-service/overviews` also return `sourceConsistency`; overview responses may only include lightweight counts and diagnosis, while detail responses include location, mileage, and source-time deltas when realtime source rows are available.

### Realtime Vehicles

```http
GET /api/realtime/vehicles?keyword=粤AG18312&protocol=JT808&online=online&serviceStatus=degraded&limit=50&offset=0
```

Returns VIN-level realtime rows with canonical vehicle-level `serviceStatus`. Protocol is a source filter, not a product boundary.

### Vehicle Coverage

```http
GET /api/vehicles/coverage?keyword=粤AG18312&serviceStatus=degraded&limit=20&offset=0
```

Returns VIN-level source coverage rows for the vehicle service list. Each row includes the canonical vehicle-level `serviceStatus` so frontend, exports, and external integrations share the same health definition. `serviceStatus` accepts `healthy`, `degraded`, `offline`, `no_data`, and `identity_required`.

Coverage summary also exposes `noDataVehicles`, so UI can show vehicles that exist in identity binding but have no GB32960, JT808, or Yutong MQTT source evidence. `/api/vehicles/coverage?serviceStatus=no_data` returns those bound vehicles for follow-up source onboarding.

Coverage rows also include lightweight `sourceConsistency` so list views can show the same vehicle-level source diagnosis without issuing per-row detail requests.

### History Locations

```http
GET /api/history/locations?keyword=粤AG18312&protocol=JT808&dateFrom=2026-07-03%2000:00:00&dateTo=2026-07-03%2023:59:59&limit=20&offset=0
```

Returns historical location points from TDengine, enriched with plate where possible.

Location points include `socPercent`, `directionDeg` and `alarmFlag` when the source protocol supplies them. `socAvailable` distinguishes a real zero from an unavailable historical value. Device and receive timestamps are returned as absolute RFC3339 instants; query bounds must include an explicit offset (for example `+08:00`) so TDengine session timezone cannot shift the requested window.

For trajectory replay, the BFF should eventually return two shapes from the same evidence set:

- `items`: paginated table rows for audit and export.
- `mapPoints`: bounded or sampled points for map rendering and playback.

This keeps table pagination precise while preventing large routes from overloading the browser map.

### History Series And Controlled Export

```http
GET /api/v2/history/series?keyword=粤AG18312&dateFrom=2026-07-14T00:00:00%2B08:00&dateTo=2026-07-14T06:00:00%2B08:00&targetPoints=240

POST /api/v2/exports
Content-Type: application/json

{
  "keywords": ["粤AG18312"],
  "category": "location",
  "protocol": "JT808",
  "dateFrom": "2026-07-14T00:00:00+08:00",
  "dateTo": "2026-07-14T06:00:00+08:00",
  "metrics": ["speed_kmh", "total_mileage_km"],
  "format": "csv"
}

GET /api/v2/exports
GET /api/v2/exports/{id}/download
```

The series endpoint returns server-aggregated location telemetry with explicit bucket coverage and missingness. Requests are limited to 31 days and 60–600 target points; timestamps must identify an absolute window rather than relying on a database session timezone.

Export creation and listing require `operator` or `admin`. A task accepts 1–5 vehicles, `location`, `raw`, or `mileage`, CSV format, at most 31 days and 32 metrics. The service runs one export at a time and enforces a 1,000,000-row and 30-minute ceiling. Location and RAW data use stable forward-only cursors; the implementation does not use growing `OFFSET` scans.

Job status values are `queued`, `running`, `completed`, and `failed`. During execution, clients should display the server-owned `processedRows`, `totalRows`, `progress`, and `evidence` fields rather than estimate progress from elapsed time. On completion, `rowCount`, `processedRows`, and `totalRows` are equal, `fileSizeBytes` and `completedAt` are populated, and `downloadUrl` becomes available. The final CSV is exposed only after its temporary `.part` file has been flushed, synchronized, closed, and atomically renamed. Completed jobs survive API restarts; jobs interrupted while queued or running become explicit failures and may be recreated.

### RAW Frames

Short GET query:

```http
GET /api/history/raw-frames?keyword=粤AG18312&protocol=GB32960&limit=20&offset=0&includeFields=true
```

Large field-filter query:

```http
POST /api/history/raw-frames/query
Content-Type: application/json

{
  "keyword": "粤AG18312",
  "protocol": "GB32960",
  "dateFrom": "2026-07-03 00:00:00",
  "dateTo": "2026-07-03 23:59:59",
  "fields": ["gb32960.vehicle.speed_kmh", "gb32960.vehicle.total_mileage_km"],
  "includeFields": true,
  "limit": 20,
  "offset": 0
}
```

Use POST when `fields` may be long. `parsedFields` is returned only when `includeFields=true` or specific `fields` are requested.

### Mileage

```http
GET /api/mileage/daily?keyword=粤AG18312&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03&limit=20&offset=0
GET /api/mileage/summary?keyword=粤AG18312&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03
```

Returns daily mileage rows and aggregate mileage summary.

The production projection table stores `daily_mileage_km` and `latest_total_mileage_km`; it deliberately no longer stores a duplicate first-total column. The API derives `startMileageKm = latestTotalMileageKm - dailyMileageKm` and normalizes a missing latest value to zero, matching the current gateway storage contract.

### Alert Events

Current API surface:

```http
GET /api/alert-events?keyword=粤AG18312&protocol=JT808&issueType=NO_SOURCE&limit=20&offset=0
GET /api/alert-events/summary?keyword=粤AG18312&protocol=JT808&issueType=NO_SOURCE
GET /api/alert-events/notification-plan?keyword=粤AG18312&protocol=JT808&issueType=NO_SOURCE&limit=20
```

`/api/quality/issues`, `/api/quality/summary`, and `/api/quality/notification-plan` remain compatibility aliases. New UI and integrations should use `/api/alert-events*` so the API language matches the vehicle data management center.

Alert event rows are vehicle-first and evidence-backed. Each event includes vehicle identity, issue type, severity, protocol/source when relevant, last seen time, source endpoint, detail, and links generated by the BFF/UI to realtime, trajectory, RAW, mileage, vehicle detail, or notification rule evidence.

Initial alert types:

- `NO_SOURCE`
- `VIN_MISSING`
- `LINK_GAP`
- `FIELD_MISSING`
- `CAPACITY_RISK`

These legacy aliases remain read-only quality projections. The durable V2 alert center is a separate truth surface:

```http
POST /api/v2/alerts/summary
POST /api/v2/alerts/events
GET  /api/v2/alerts/events/{id}
POST /api/v2/alerts/events/{id}/actions
GET  /api/v2/alerts/rules
POST /api/v2/alerts/rules
PUT  /api/v2/alerts/rules/{id}
PUT  /api/v2/alerts/rules/{id}/enabled
GET  /api/v2/alerts/notifications?unreadOnly=true&limit=20&offset=0
POST /api/v2/alerts/notifications/read
```

Rule operators are type-aware: numeric metrics support `gt/gte/lt/lte/eq/neq/between/outside`; Boolean metrics support `eq/neq/changed`. Range rules carry `threshold` and `thresholdHigh`. Scopes support `scopeProtocols`, `scopeVins`, authoritative `scopeOems`, and `scopeModels/scopeCompanies` joined from `vehicle_profile`. Each scope list is capped at 500 normalized values with a 128-character per-value bound, protecting evaluator latency. Vehicles without the requested master-data dimension do not match a model/company-scoped rule. `changed` rules persist the previous Boolean state per rule/VIN/protocol, and repeat suppression uses `repeatIntervalSec` against the latest event fingerprint.

The rule editor reads its selectable metrics from `GET /api/v2/metrics`. The API repeats the same catalog validation on every create/update, so a stale or manipulated client cannot configure an unsupported evaluator field.

`summary` and `events` accept the same JSON filter: `keyword, severity, status, ruleId, protocol, dateFrom, dateTo, limit, offset`. Event states are `unprocessed, processing, recovered, closed, ignored`; event changes require the current `version` and actions are `acknowledge, close, ignore`. Every action writes an immutable timeline item with actor, before/after state, note, and timestamp. Stale event or rule writes return HTTP 409 with a `*_VERSION_CONFLICT` code.

Rules support numeric/Boolean values, bounded duration and repeat interval, recovery hysteresis, protocol/VIN scope, enable state, and a versioned audit snapshot. For telemetry metrics, duration is the difference between distinct, monotonic source event observations rather than time spent rereading one MySQL snapshot; `freshness_sec` is the explicit exception because staleness changes with platform time. Duplicate and late observations are counted but cannot advance a candidate. The evaluator deduplicates active rule+vehicle+protocol fingerprints and automatically recovers only when the configured recovery condition is true. Disabling a rule atomically clears its candidate and Boolean state together with the versioned disable audit. Station notifications have real unread/read state. `sms`, `email`, and `wecom` records are explicitly `reserved`; they are not reported as sent.

### Online And Completeness Statistics

Planned API surface:

```http
GET /api/statistics/online-rate?keyword=粤AG18312&dateFrom=2026-07-01&dateTo=2026-07-03&limit=20&offset=0
GET /api/statistics/completeness?keyword=粤AG18312&dateFrom=2026-07-01&dateTo=2026-07-03&limit=20&offset=0
GET /api/statistics/source-consistency?keyword=粤AG18312&dateFrom=2026-07-01&dateTo=2026-07-03&limit=20&offset=0
```

These APIs should link back to `/api/history/locations` or `/api/history/raw-frames/query` evidence. The UI must not show statistics as untraceable numbers.

### V2 Access Management

```http
POST /api/v2/access/summary
Content-Type: application/json

{"protocol":"JT808","onlineState":"offline","delayState":"abnormal"}

POST /api/v2/access/vehicles
Content-Type: application/json

{"keyword":"粤AG18312","protocol":"JT808","model":"ZK6122","provider":"G7s","firstSeenFrom":"2026-07-14T00:00:00+08:00","latestSeenFrom":"2026-07-14T08:00:00+08:00","limit":50,"offset":0}

POST /api/v2/access/unresolved-identities
Content-Type: application/json

{"keyword":"","protocol":"JT808","limit":20,"offset":0}

GET /api/v2/access/thresholds
PUT /api/v2/access/thresholds
```

`summary` and `vehicles` consume the same filter shape, so KPI, protocol/OEM distribution, and rows share one scope. Besides keyword, protocol, OEM and status, both accept substring filters `model` and `provider`, plus paired `firstSeenFrom`/`firstSeenTo` and `latestSeenFrom`/`latestSeenTo` bounds. A supplied pair must be ordered; malformed or reversed timestamps return `ACCESS_TIME_INVALID` or `ACCESS_TIME_RANGE_INVALID`. `onlineState` values are `online`, `offline`, `never_reported`, and `unknown`. Online state is calculated on every request from latest receive/update freshness and the current global/protocol threshold version; the stored snapshot boolean is not reused as the answer.

The authoritative access-management population is the distinct VIN set in `vehicle_identity_binding`; realtime rows without a bound VIN stay in `unresolved-identities` and never inflate the main-vehicle count. Each main vehicle is returned once with the standard expected source slots `GB32960`, `JT808`, and `YUTONG_MQTT`. `expectedProtocols`, `actualProtocols`, `missingProtocols`, and `protocolStatuses` make the expected-versus-actual difference explicit. Every protocol status independently carries provider, first-seen evidence, latest event/receive time, report interval, freshness, delay, and online state. `connectionState` is one of `healthy`, `incomplete`, `degraded`, `offline`, or `not_connected`; the additional query value `attention` returns every non-healthy vehicle for the difference-first operations list.

`unresolved-identities` returns terminals that have real access evidence but no authoritative VIN binding. The current implementation reads JT808 registration evidence and excludes terminals already present in `vehicle_identity_binding`. It exposes only a stable hashed ID and masked terminal identifier, never the raw phone number. Resolve `missing_vin_jt808` by verifying registration evidence and maintaining an authoritative `phone -> VIN` binding; do not infer or fabricate a VIN.

Access rows expose both event and receive time, `dataDelaySec`, `freshnessSec`, the applied `thresholdSec`, latest realtime message semantics, event ID, source table, model/company master data, and evidence notes. The gateway snapshot upsert maintains `access_first_seen_at`, `access_previous_received_at`, `access_latest_received_at`, `access_report_interval_ms`, `access_sample_count`, and the latest received event ID independently from device event-time ordering. A strictly newer receipt with a different event ID advances the projection atomically; duplicates, equal timestamps, replay and older receipts do not change the interval or sample count. The API rounds milliseconds to `reportIntervalSec` and returns `reportSampleCount`, `firstSeenSource`, `firstSeenEvidence`, and `reportIntervalEvidence`. `live_writer` means first observation after the writer began maintaining the row. `snapshot_backfill` is only a deployment baseline reconstructed from the previous current-state row and must not be described as the vehicle's historical first-ever access.

Threshold configuration is stored in `vehicle_access_threshold_config`; every successful update increments `version`, requires the caller's previous version, and writes `vehicle_access_threshold_audit`. Stale updates return `ACCESS_THRESHOLD_VERSION_CONFLICT`. Valid ranges are bounded server-side.

## Compatibility Rule

Existing callers may still send `vin`. The BFF treats `vin` as a vehicle lookup value for compatibility, so `vin=粤AG18312` still resolves through identity binding. New code should use `keyword` to avoid implying the value is already a real VIN.
## Track Replay Evidence

`GET /api/v2/tracks` accepts `keyword|vin`, optional `protocol`, `dateFrom`, `dateTo` and `maxPoints`.

- If either time bound is provided, both are required, the end must be later than the start, and the range is limited to seven days. Omitting both retains the bounded latest-slice compatibility mode.
- The TDengine source read remains capped at 5,000 latest points per request. `coverage.complete=false` and `coverage.evidence` explicitly state when summary metrics describe only that slice rather than the whole requested window.
- `coverage` reports total, fetched, processed and returned point counts plus `latest_source_slice`, `quality_filter` and `map_sampling` reasons.
- `quality` reports invalid coordinates, exact duplicates, short-interval >220 km/h implied drift, source switches and gaps over ten minutes. Filtered points never silently disappear without these counters.
- When multiple protocols report the same VIN, coordinate quality is evaluated within each protocol and the map selects one primary source (explicit `protocol`, otherwise highest point count then latest observation). Alternate valid points remain visible in `sources`, `quality.alternateSourcePoints`, and the `primary_source_projection` coverage reason; parallel source coordinates are never stitched into one route.
- `segments` are inferred as `moving`, `stopped` or `gap`. A stop requires at least three minutes of consecutive <=3 km/h and <=150 m interval movement. The evidence explicitly says this is GPS inference and not ignition state.
- Positive subsecond intervals remain continuous telemetry even when their displayed whole-second duration rounds to zero; only invalid/non-increasing timestamps or intervals over ten minutes become `gap`.
- Sampling preserves the route endpoints, event points, stop centers and every retained segment boundary. `sampledIndex` fields point directly into the returned `points` array; clients must not approximate an original index by ratio when this field is present.
- Playback points and events carry synchronized speed, SOC availability/value, direction and alarm fields. The browser resolves only the paused/current coordinate through the cached reverse-geocode endpoint; playback never triggers one address request per point.
