Data API reference¶
All paths are relative to your API base URL (for example https://analytics.example.com:9006/api). Authenticate with Authorization: Bearer $RDA_API_KEY as described in API integration.
In the examples below, set:
export RDA_API_BASE_URL=https://analytics.example.com:9006/api
export RDA_API_KEY=rda_…
export SCAN_ID=<scan-id-from-/data/assets>
Sample JSON bodies are illustrative (ids and values abbreviated). Field names match the live API.
Pagination¶
List endpoints return a page of rows plus an optional cursor:
{
"data": [ … ],
"nextCursor": "507f1f77bcf86cd799439011"
}
When nextCursor is a string, request the next page with ?cursor=<that value>. When it is null, you have reached the end.
| Endpoint family | What the cursor encodes |
|---|---|
| Tracks | time|trackUuid |
| Metrics | time|elementId|classGroupId |
| Assets | Location id |
Example — walk all track pages:
CURSOR=""
while true; do
if [ -z "$CURSOR" ]; then
RESP=$(curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/tracks?limit=1000")
else
RESP=$(curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/tracks?limit=1000&cursor=$CURSOR")
fi
echo "$RESP" | jq '.data | length'
CURSOR=$(echo "$RESP" | jq -r '.nextCursor // empty')
[ -z "$CURSOR" ] && break
done
Asset catalog¶
Discover zone-scoped locations, nested sources, and scans before you call track or metric endpoints.
GET /data/assets?limit=100&cursor=<locationId>
| Param | Default | Max | Notes |
|---|---|---|---|
limit |
100 | 500 | Locations per page |
cursor |
— | — | Location id from previous nextCursor |
globalSources (sources without a location) appear on the first page only.
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/assets" | jq .
Sample response:
{
"data": [
{
"id": "507f1f77bcf86cd799439011",
"name": "Main junction",
"zoneId": "507f191e810c19729de860ea",
"sources": [
{
"id": "66a1b2c3d4e5f678901234ab",
"name": "Camera A — north approach",
"scans": [
{
"id": "66b2c3d4e5f678901234abcd",
"name": "Vehicle counting",
"status": "COMPLETED"
}
]
}
]
}
],
"globalSources": [],
"nextCursor": null
}
Use a scan "id" from this tree as SCAN_ID for the endpoints below.
Metadata¶
GET /data/class-groups
GET /data/class-vocabulary
GET /data/metrics/definitions
| Endpoint | Returns |
|---|---|
| class-groups | Configured class group labels and member mappings (404 if not configured yet) |
| class-vocabulary | Raw detector class names from installed modules |
| metrics/definitions | Catalog of metric domains and series kinds you can query |
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/class-groups" | jq .
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/metrics/definitions" | jq .
Sample — GET /data/class-groups:
{
"groups": {
"1": {
"label": "Light vehicles",
"color": "#6400c8",
"order": 0
},
"3": {
"label": "Heavy vehicles",
"color": "#ff9800",
"order": 1
}
},
"members": {
"car": "1",
"pickup": "1",
"van": "1",
"truck": "3",
"bus": "3"
}
}
Sample — GET /data/class-vocabulary:
{
"all": ["bicycle", "bus", "car", "motorcycle", "pickup", "truck", "van"]
}
Sample — GET /data/metrics/definitions:
{
"metrics": [
{
"domain": "density",
"description": "Density metrics",
"stats": ["avg", "max", "min"],
"unit": "count",
"seriesKinds": ["total", "class"]
},
{
"domain": "speed",
"description": "Speed metrics",
"stats": ["avg", "max", "min", "p50", "p85"],
"unit": "km/h",
"seriesKinds": ["total", "class"]
},
{
"domain": "dwell",
"description": "Dwell metrics",
"stats": ["avg", "max", "min"],
"unit": "s",
"seriesKinds": ["total", "class"]
}
]
}
Tracks¶
Tracks are detected objects (with timing, class, lines, optional plate fields) for one scan.
List tracks¶
GET /data/scans/{scanId}/tracks
| Param | Description |
|---|---|
from, to |
Time range (defaults to source file duration) |
limit |
Page size (default 1000, max 5000) |
cursor |
Pagination cursor from nextCursor |
since, since_id |
Incremental sync watermark (only rows after this time/id) |
class_group_id |
Repeatable filter by class group |
line_first, line_last |
Counting-line filters |
include_plate |
true to include license plate fields |
# First page
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/tracks?limit=1000" | jq .
# Next page (paste nextCursor from the previous response)
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/tracks?limit=1000&cursor=2025-01-01T10:15:00%7C7c9e6679e2b04c4c8f4e0a1b2c3d4e5f"
# Incremental sync since a watermark
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/tracks?since=2025-01-01T10:00:00&since_id=7c9e6679e2b04c4c8f4e0a1b2c3d4e5f"
Sample response (without include_plate):
{
"data": [
{
"id": "7c9e6679e2b04c4c8f4e0a1b2c3d4e5f",
"trackId": 42,
"timestamp": "2025-01-01T10:15:00",
"classGroupId": "1",
"classGroupLabel": "Light vehicles",
"lines": "A>B",
"speed": 48.2,
"make": "Toyota",
"model": "Corolla",
"imageUrl": "/api/media/66b2c3d4e5f678901234abcd/42"
}
],
"nextCursor": "2025-01-01T10:15:00|7c9e6679e2b04c4c8f4e0a1b2c3d4e5f"
}
With include_plate=true, each row can also include "licensePlate" and "plateImageUrl" (path ends with ?variant=plate).
Track "id" values are 32-character hex strings (uuid4().hex), not dashed UUIDs. Location / source / scan / export ids are MongoDB ObjectIds (24 hex characters).
Track detail¶
GET /data/scans/{scanId}/tracks/{trackUuid}
Full payload: events, trajectory points, and attributes.
imageUrl and plateImageUrl are path-relative under {API_V1_PREFIX}/media/{scanId}/{trackId}. Media GET does not require auth — append those paths to your server origin when downloading images.
TRACK_UUID=7c9e6679e2b04c4c8f4e0a1b2c3d4e5f
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/tracks/$TRACK_UUID" | jq .
Sample response:
{
"id": "7c9e6679e2b04c4c8f4e0a1b2c3d4e5f",
"trackId": 42,
"timestamp": "2025-01-01T10:15:00",
"ptsMs": 615000,
"classGroupId": "1",
"classGroupLabel": "Light vehicles",
"attributes": {
"lines": ["A", "B"],
"speed": 48.2,
"make": "Toyota",
"model": "Corolla",
"classGroupId": "1"
},
"events": [
{
"eventId": "f47ac10b58cc4372a5670e02b2c3d479",
"name": "line_cross",
"ptsMs": 614800,
"time": "2025-01-01T10:14:58",
"properties": {
"elementId": "line_a",
"lineName": "A",
"classGroupId": "1"
}
},
{
"eventId": "9b1deb4d3b7d4bad9bdd00b8d1693e91",
"name": "line_cross",
"ptsMs": 615000,
"time": "2025-01-01T10:15:00",
"properties": {
"elementId": "line_b",
"lineName": "B",
"classGroupId": "1"
}
}
],
"track": {
"points": [[420, 610], [480, 540], [550, 480]],
"age": 37
},
"imageUrl": "/api/media/66b2c3d4e5f678901234abcd/42",
"plateImageUrl": ""
}
Event time values are formatted like list timestamps (YYYY-MM-DDTHH:MM:SS) when stored as datetimes. track.points are pixel [x, y] pairs. Exact attributes and event properties depend on the modules used in the scan.
Streaming export (moderate size)¶
Streams the filtered track set as one response — good for scripts when the result fits a single download.
GET /data/scans/{scanId}/tracks/export?format=csv|ndjson
Same filters as the list endpoint. CSV is UTF-8 with BOM.
CSV columns (without plate): ID, Track ID, Time, Class Group ID, Class, Lines, Speed, Make, Model, Image URL. With include_plate=true, adds License Plate and Plate Image URL.
NDJSON uses one JSON object per line with the same fields as the track list rows.
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/tracks/export?format=csv" \
-o tracks.csv
Sample CSV (header + one row):
ID,Track ID,Time,Class Group ID,Class,Lines,Speed,Make,Model,Image URL
7c9e6679e2b04c4c8f4e0a1b2c3d4e5f,42,2025-01-01T10:15:00,1,Light vehicles,A>B,48.2,Toyota,Corolla,/api/media/66b2c3d4e5f678901234abcd/42
Metrics¶
Aggregated time series for a scan (density counts, speed in km/h, dwell in seconds).
GET /data/scans/{scanId}/metrics?domain=<name>&stat=avg|max|min&series_kind=total|class
| Domain | Unit | Stats on each row |
|---|---|---|
density |
count | avg, max, min, n |
speed |
km/h | avg, max, min, n (also p50, p85 when stored) |
dwell |
s | avg, max, min, n |
| Param | Notes |
|---|---|
domain |
Required — one of the domains above |
series_kind |
total or class |
stat |
Reserved for future filtering; responses currently return all measures |
from, to |
Time range |
element_id, class_group_id |
Optional series filters |
cursor, limit |
Pagination (default limit 1000, max 5000) |
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/metrics?domain=density&series_kind=total&limit=1000" | jq .
Sample response:
{
"data": [
{
"time": "2025-01-01T10:00:00",
"domain": "density",
"seriesKind": "total",
"elementId": "junction_area",
"elementName": "Junction area",
"classGroupId": null,
"bucketSeconds": 60,
"avg": 12.4,
"max": 18,
"min": 7,
"n": 60,
"unit": "count"
}
],
"nextCursor": null
}
The API adds "unit" from the domain catalog. Optional percentile fields (p50, p85) appear when present for that domain (especially speed). For series_kind=class, rows include a non-empty classGroupId (for example "1").
Async exports (very large scans)¶
For large datasets (for example 1M+ rows), create a background job, poll until ready, then download.
POST /data/scans/{scanId}/exports/tracks?format=csv|ndjson
GET /data/exports/{jobId}
GET /data/exports/{jobId}/download
- POST creates a job.
- GET status until
statusiscompletedanddownloadReadyistrue. - GET download returns the file body (same CSV/NDJSON shape as streaming export).
Job statuses: pending, running, completed, failed.
# Create
curl -s -X POST -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/scans/$SCAN_ID/exports/tracks?format=ndjson"
export JOB_ID=…
# Poll
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/exports/$JOB_ID" | jq .
# Download when downloadReady is true
curl -s -H "Authorization: Bearer $RDA_API_KEY" \
"$RDA_API_BASE_URL/data/exports/$JOB_ID/download" \
-o tracks.ndjson
Sample — create (POST …/exports/tracks):
{
"jobId": "66c3d4e5f678901234abcdef",
"status": "pending"
}
Sample — status while running (GET /data/exports/{jobId}):
{
"jobId": "66c3d4e5f678901234abcdef",
"scanId": "66b2c3d4e5f678901234abcd",
"format": "ndjson",
"status": "running",
"includePlate": false,
"rowCount": 0,
"createdTime": "2025-01-01T12:00:00+00:00",
"updatedTime": "2025-01-01T12:00:05+00:00",
"completedTime": null,
"error": null,
"downloadReady": false
}
Sample — status when ready:
{
"jobId": "66c3d4e5f678901234abcdef",
"scanId": "66b2c3d4e5f678901234abcd",
"format": "ndjson",
"status": "completed",
"includePlate": false,
"rowCount": 152340,
"createdTime": "2025-01-01T12:00:00+00:00",
"updatedTime": "2025-01-01T12:02:11+00:00",
"completedTime": "2025-01-01T12:02:11+00:00",
"error": null,
"downloadReady": true
}
Error responses¶
Structured errors usually look like:
{ "code": "INVALID_ID", "detail": "Invalid scan_id" }
Query validation failures may return plain { "detail": "…" } with HTTP 400.
| HTTP | Typical cause |
|---|---|
| 401 | Bad or revoked API key |
| 403 | Scan/location outside the key’s zones |
| 404 | Unknown id, or class-groups not configured (GET /data/class-groups) |
| 400 | Invalid query (domain, cursor, time range) |
Related pages¶
- API integration — overview, auth, quick start
- API keys (System UI)
- Class groups