MDS 2.0 Provider Setup
Levy Fleets implements the Mobility Data Specification (MDS) 2.0 Provider API end-to-end. All endpoints live under /api/mds/[subaccountId]/provider/v2/... and are signed with RS256 JWTs whose public keys are published at .well-known/jwks.json per subaccount.
Spec version
We pin to MDS 2.0.1. The version field in the response envelope reports 2.0.1, and the Content-Type header is application/vnd.mds+json;version=2.0. Cities running an mds-provider-validator against this base should pass with no schema warnings.
Endpoint catalogue
All routes are prefixed with /api/mds/{subaccountId}/provider/v2/. The subaccountId is the UUID of the operator subaccount (one per city for most operators).
| Endpoint | Method | Purpose | Pagination | Cache |
|---|---|---|---|---|
/vehicles | GET | All vehicles registered to the subaccount | none (single page) | 60s edge |
/vehicles/status | GET | Current state per vehicle (one row each) | cursor | 60s edge |
/vehicles/{device_id} | GET | Single vehicle detail | n/a | none |
/trips | GET | Trips ending within ?end_time window | cursor by ended_at | none |
/events | GET | State change events in window | cursor by event_time | none |
/telemetry/{vehicle_id} | GET | GPS / battery samples for window | none | none |
/stops | GET | Operator-defined parking corrals + charging stops | none | 60s edge |
/reports | GET | Pre-computed monthly aggregates | none | 60s edge |
/.well-known/jwks.json | GET | Public JWK Set for verifying response JWTs | n/a | 60s edge |
Vehicles list — /vehicles
Returns every vehicle linked to the subaccount. One row per device_id.
curl -H "Authorization: Bearer <token>" \
https://fleets.levyelectric.com/api/mds/<subaccountId>/provider/v2/vehicles
Response envelope:
{
"version": "2.0.1",
"last_updated": "2026-05-18T12:00:00Z",
"ttl": 60000,
"vehicles": [ /* MDS Vehicle objects */ ]
}
Vehicles status — /vehicles/status
Cursor-paginated. Default page size is 100, max 500. The cursor encodes the last vehicle's update timestamp + ID so subsequent pages are stable across writes.
curl -H "Authorization: Bearer <token>" \
"https://fleets.levyelectric.com/api/mds/<subaccountId>/provider/v2/vehicles/status?cursor=<opaque>&limit=200"
Response includes next_cursor when more pages exist:
{
"version": "2.0.1",
"last_updated": "2026-05-18T12:00:00Z",
"vehicles_status": [ /* status objects, includes current_speed_limit_kph */ ],
"next_cursor": "eyJ0aW1lIjoiMjAyNi0wNS0xOFQxMjowMDowMFoiLCJpZCI6IjQyIn0="
}
The current_speed_limit_kph field on each status row reflects the lowest enforceable speed at the vehicle's last known position, factoring in both operator zones and active policy geofences. See Real-Time Speed Enforcement.
Single vehicle — /vehicles/{device_id}
Returns one MDS Vehicle object. Useful for validators that probe individual records before crawling the full list.
Trips — /trips
Cursor pagination ordered by ended_at ascending. Required query parameter end_time is a 1-hour window in ISO 8601:
GET /trips?end_time=2026-05-18T00:00:00Z
The endpoint returns trips whose ended_at falls in [end_time, end_time + 1h). Open trips are excluded.
Events — /events
Cursor pagination ordered by event_time ascending. Same end_time window semantics as /trips. Emits state changes (available, reserved, on_trip, non_operational, removed, etc.).
Telemetry — /telemetry/{vehicle_id}
Returns GPS + battery samples for a single vehicle in a time window. If the vehicle_telemetry table is absent (some operator subaccounts predate it), the route falls back to vehicle_events automatically.
Stops — /stops
Operator-defined parking corrals and charging stations, surfaced as MDS Stop objects. Includes vehicle_type_capacity per MDS 2.0 schema.
Reports — /reports
Pre-computed monthly aggregates: total trips, total active vehicles, average trip distance, average trip duration. Useful for the city's quarterly review without crawling /trips.
Authentication
Levy Fleets accepts MDS requests with a bearer token. Cities can additionally verify the signed JWT we return on every response.
Bearer tokens
Issued from Settings -> API & Integrations -> MDS Tokens. One token per city per subaccount is typical. The token is checked against mds_city_tokens on every request.
Authorization: Bearer <opaque_token>
Signed JWT on every response
Every successful response includes an MDS-JWT header containing an RS256-signed JWT. The JWT's payload includes the sha256 of the response body, the subaccount ID, the issuer (https://fleets.levyelectric.com), and the issued-at timestamp. Cities that require integrity proof can verify the JWT against the JWKS URL.
The signing key is the active row in mds_jwks_keys for the subaccount. Lazy-minted on first request. See JWKS Key Management for rotation.
Optional HMAC body signature
For cities that require an HMAC integrity proof instead of (or in addition to) the JWT, set the MDS-Signature header on the response. The shared secret is configured per city-token row.
JWKS discovery
The public JWK Set lives at:
GET /api/mds/{subaccountId}/.well-known/jwks.json
Response shape:
{
"keys": [
{
"kty": "RSA",
"kid": "mds-2026-05",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}
The kid matches the kid field in the MDS-JWT header so cities can pick the right key during rotation. We always publish the active key plus any keys still in their rotation grace window (default 7 days).
If a city hits the JWKS URL before the first request has been signed, the route lazy-mints the first keypair. This means fresh operators don't have to take any manual action — the first ping from Populus is enough.
Response envelope
Every MDS 2.0 response uses the same outer shape:
{
"version": "2.0.1",
"last_updated": "2026-05-18T12:00:00Z",
"ttl": 60000,
"<data_key>": [ /* array of MDS objects */ ],
"next_cursor": "..."
}
last_updatedis always RFC3339 UTC (no POSIX epoch — MDS 2.0 mandates RFC3339).ttlmatches theCache-Control: max-ageheader (in milliseconds — MDS spec uses ms, HTTP uses s).next_cursoris present only when more pages exist.
Cursor pagination
Cursors are opaque base64-encoded JSON { "time": <ISO>, "id": <last_id> }. They are stable across writes: a new row inserted after you started crawling will appear on a future page, never on the current cursor.
Cities should:
- Make an initial request with no
cursor. - Read
next_cursorfrom the response. - Pass it back on the next request:
?cursor=<value>. - Stop when
next_cursoris absent.
Page size defaults to 100. Pass ?limit=<n> to override (max 500).
Edge caching
We cache /vehicles, /vehicles/status, /stops, /reports, and /.well-known/jwks.json at the Vercel edge for 60 seconds. Trips, events, and telemetry are not cached — they're time-bound by query, so each request has a unique cache key anyway.
The Cache-Control: public, s-maxage=60 header is set automatically on cached endpoints.
Rate limiting
Each bearer token is limited to 60 requests per minute with a 600-request burst. Exceeding the limit returns HTTP 429 with Retry-After set. Validator crawls (10-15 endpoints back-to-back) stay well within burst.
If a city consistently exceeds the limit, raise it via Settings -> API & Integrations -> MDS Tokens -> Rate Limit Override.
Validator configuration
The OMF mds-provider-validator (and Populus' internal equivalent) expects:
| Check | Value |
|---|---|
| Base URL | https://fleets.levyelectric.com/api/mds/<subaccountId>/provider/v2/ |
| Auth | Authorization: Bearer <token> |
| JWKS | https://fleets.levyelectric.com/api/mds/<subaccountId>/.well-known/jwks.json |
| Spec version | 2.0.1 |
| Content type | application/vnd.mds+json;version=2.0 |
Run the validator yourself before handing off to a city:
mds-provider-validator \
--base "https://fleets.levyelectric.com/api/mds/<subaccountId>/provider/v2" \
--auth "Bearer <token>" \
--version 2.0.1 \
--jwks "https://fleets.levyelectric.com/api/mds/<subaccountId>/.well-known/jwks.json"
A clean run reports Passed: all endpoints conform. Any failures are surfaced with the offending field and endpoint, and should be fixed before sharing the URL with a city.
Troubleshooting
- 401 on every request — bearer token missing or revoked. Issue a fresh one from Settings.
- Validator fails on
versionfield — confirm the response envelope reports2.0.1, not2.0.0. The version is a constant insrc/lib/mds/v2/response.ts. MDS-JWTheader missing on a response — the JWKS keypair failed to mint. Visit/api/mds/<subaccountId>/.well-known/jwks.jsondirectly to force a lazy mint, then retry.current_speed_limit_kphalways 0 — the vehicle has no last-known position, or no operator zones / policy geofences contain it. Verify a recent GPS sample exists invehicle_events.
See Troubleshooting for the full checklist.
What's next
- JWKS Key Management — rotation, kid, and grace windows.
- GBFS 3.0 Feeds — the public feed alongside the MDS Provider API.
- Policy Ingestion from Cities — pulling rules from the city's Policy feed.