Skip to content

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
  1. POST creates a job.
  2. GET status until status is completed and downloadReady is true.
  3. 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)