advanced
MDS
Provider
JWT

MDS 2.0 Provider-Einrichtung

Vollstaendige Referenz fuer die MDS 2.0 Provider-Endpunkte, JWT-Signierung, JWKS-Discovery, Cursor-Paginierung und Validator-Konfiguration.

Levy Fleets TeamMay 18, 202618 min read

MDS 2.0 Provider-Einrichtung

Levy Fleets implementiert die Mobility Data Specification (MDS) 2.0 Provider-API vollstaendig. Alle Endpunkte liegen unter /api/mds/[subaccountId]/provider/v2/... und werden mit RS256-JWTs signiert, deren oeffentliche Schluessel pro Subaccount unter .well-known/jwks.json veroeffentlicht werden.

Spec-Version

Wir setzen auf MDS 2.0.1. Das version-Feld im Response-Envelope meldet 2.0.1, und der Content-Type-Header ist application/vnd.mds+json;version=2.0. Staedte, die einen mds-provider-validator gegen diese Basis laufen lassen, sollten ohne Schema-Warnungen bestehen.

Endpunkt-Katalog

Alle Routen haben das Praefix /api/mds/{subaccountId}/provider/v2/. Die subaccountId ist die UUID des Betreiber-Subaccounts (bei den meisten Betreibern einer pro Stadt).

EndpunktMethodeZweckPaginierungCache
/vehiclesGETAlle dem Subaccount zugeordneten Fahrzeugekeine (eine Seite)60s Edge
/vehicles/statusGETAktueller Zustand pro Fahrzeug (eine Zeile pro Fahrzeug)Cursor60s Edge
/vehicles/{device_id}GETEinzelfahrzeug-Detailn/akeine
/tripsGETFahrten, die im ?end_time-Fenster endenCursor nach ended_atkeine
/eventsGETStatusaenderungs-Ereignisse im FensterCursor nach event_timekeine
/telemetry/{vehicle_id}GETGPS-/Batterieproben fuer Fensterkeinekeine
/stopsGETVom Betreiber definierte Parkstellplaetze + Ladestoppskeine60s Edge
/reportsGETVorab berechnete monatliche Aggregatekeine60s Edge
/.well-known/jwks.jsonGETOeffentliches JWK-Set zur Verifikation der Response-JWTsn/a60s Edge

Fahrzeugliste -- /vehicles

Gibt jedes mit dem Subaccount verknuepfte Fahrzeug zurueck. Eine Zeile pro 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-Objekte */ ]
}

Fahrzeugstatus -- /vehicles/status

Cursor-paginiert. Standard-Seitengroesse ist 100, max 500. Der Cursor kodiert den letzten Update-Zeitstempel + ID, sodass Folgeseiten ueber Schreibvorgaenge hinweg stabil sind.

curl -H "Authorization: Bearer <token>" \
  "https://fleets.levyelectric.com/api/mds/<subaccountId>/provider/v2/vehicles/status?cursor=<opaque>&limit=200"

Response enthaelt next_cursor, falls weitere Seiten existieren:

{
  "version": "2.0.1",
  "last_updated": "2026-05-18T12:00:00Z",
  "vehicles_status": [ /* Status-Objekte, enthaelt current_speed_limit_kph */ ],
  "next_cursor": "eyJ0aW1lIjoiMjAyNi0wNS0xOFQxMjowMDowMFoiLCJpZCI6IjQyIn0="
}

Das current_speed_limit_kph-Feld jeder Statuszeile spiegelt die niedrigste durchsetzbare Geschwindigkeit an der letzten bekannten Position des Fahrzeugs wider, unter Beruecksichtigung von Betreiberzonen und aktiven Policy-Geofences. Siehe Echtzeit-Geschwindigkeitsdurchsetzung.

Einzelfahrzeug -- /vehicles/{device_id}

Gibt ein MDS Vehicle-Objekt zurueck. Nuetzlich fuer Validatoren, die einzelne Eintraege pruefen, bevor sie die vollstaendige Liste crawlen.

Fahrten -- /trips

Cursor-Paginierung sortiert nach ended_at aufsteigend. Erforderlicher Query-Parameter end_time ist ein 1-Stunden-Fenster in ISO 8601:

GET /trips?end_time=2026-05-18T00:00:00Z

Der Endpunkt gibt Fahrten zurueck, deren ended_at in [end_time, end_time + 1h) faellt. Offene Fahrten werden ausgeschlossen.

Ereignisse -- /events

Cursor-Paginierung sortiert nach event_time aufsteigend. Gleiche end_time-Fenster-Semantik wie /trips. Emittiert Statusaenderungen (available, reserved, on_trip, non_operational, removed usw.).

Telemetrie -- /telemetry/{vehicle_id}

Gibt GPS- + Batterieproben fuer ein einzelnes Fahrzeug in einem Zeitfenster zurueck. Falls die vehicle_telemetry-Tabelle nicht existiert (einige aeltere Subaccounts), greift die Route automatisch auf vehicle_events zurueck.

Stopps -- /stops

Vom Betreiber definierte Parkstellplaetze und Ladestationen, als MDS Stop-Objekte. Enthaelt vehicle_type_capacity gemaess MDS 2.0-Schema.

Berichte -- /reports

Vorab berechnete monatliche Aggregate: Gesamtfahrten, gesamt aktive Fahrzeuge, durchschnittliche Fahrtdistanz, durchschnittliche Fahrtdauer. Nuetzlich fuer die quartalsweise Stadtpruefung ohne /trips zu crawlen.

Authentifizierung

Levy Fleets akzeptiert MDS-Anfragen mit Bearer-Token. Staedte koennen zusaetzlich den signierten JWT verifizieren, den wir bei jeder Antwort zurueckgeben.

Bearer-Tokens

Ausgestellt aus Einstellungen -> API & Integrationen -> MDS-Tokens. Typischerweise ein Token pro Stadt pro Subaccount. Das Token wird bei jeder Anfrage gegen mds_city_tokens geprueft.

Authorization: Bearer <opaque_token>

Signierter JWT bei jeder Antwort

Jede erfolgreiche Antwort enthaelt einen MDS-JWT-Header mit einem RS256-signierten JWT. Die JWT-Payload enthaelt den sha256 des Response-Body, die Subaccount-ID, den Issuer (https://fleets.levyelectric.com) und den Issued-at-Zeitstempel. Staedte, die einen Integritaetsnachweis benoetigen, koennen den JWT gegen die JWKS-URL verifizieren.

Der Signaturschluessel ist die aktive Zeile in mds_jwks_keys fuer den Subaccount. Wird beim ersten Aufruf lazy generiert. Siehe JWKS-Schluesselverwaltung fuer die Rotation.

Optionale HMAC-Body-Signatur

Fuer Staedte, die einen HMAC-Integritaetsnachweis statt (oder zusaetzlich zum) JWT benoetigen, wird der MDS-Signature-Header in der Antwort gesetzt. Das gemeinsame Geheimnis wird pro Stadt-Token-Zeile konfiguriert.

JWKS-Discovery

Das oeffentliche JWK-Set liegt unter:

GET /api/mds/{subaccountId}/.well-known/jwks.json

Response-Form:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "mds-2026-05",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

Die kid entspricht dem kid-Feld im MDS-JWT-Header, sodass Staedte waehrend einer Rotation den richtigen Schluessel auswaehlen koennen. Wir veroeffentlichen immer den aktiven Schluessel plus alle Schluessel, die noch im Rotations-Karenzfenster sind (Standard 7 Tage).

Wenn eine Stadt die JWKS-URL aufruft, bevor die erste Anfrage signiert wurde, generiert die Route das erste Schluesselpaar lazy. Das bedeutet, dass neue Betreiber keine manuelle Aktion ausfuehren muessen -- das erste Ping von Populus genuegt.

Response-Envelope

Jede MDS 2.0-Antwort verwendet dieselbe aeussere Form:

{
  "version": "2.0.1",
  "last_updated": "2026-05-18T12:00:00Z",
  "ttl": 60000,
  "<data_key>": [ /* Array von MDS-Objekten */ ],
  "next_cursor": "..."
}
  • last_updated ist immer RFC3339 UTC (kein POSIX-Epoch -- MDS 2.0 verlangt RFC3339).
  • ttl entspricht dem Cache-Control: max-age-Header (in Millisekunden -- MDS-Spec nutzt ms, HTTP nutzt s).
  • next_cursor ist nur vorhanden, wenn weitere Seiten existieren.

Cursor-Paginierung

Cursors sind opake base64-kodierte JSON { "time": <ISO>, "id": <last_id> }. Sie sind stabil ueber Schreibvorgaenge: Eine neu eingefuegte Zeile nach dem Crawl-Beginn erscheint auf einer kuenftigen Seite, niemals auf dem aktuellen Cursor.

Staedte sollten:

  1. Initialanfrage ohne cursor stellen.
  2. next_cursor aus der Antwort lesen.
  3. Bei der naechsten Anfrage zurueckgeben: ?cursor=<value>.
  4. Stoppen, wenn next_cursor fehlt.

Seitengroesse standardmaessig 100. ?limit=<n> zum Ueberschreiben (max 500).

Edge-Caching

Wir cachen /vehicles, /vehicles/status, /stops, /reports und /.well-known/jwks.json am Vercel-Edge fuer 60 Sekunden. Fahrten, Ereignisse und Telemetrie werden nicht gecacht -- sie sind durch Query zeitgebunden, sodass jede Anfrage ohnehin einen eindeutigen Cache-Schluessel hat.

Der Cache-Control: public, s-maxage=60-Header wird auf gecachten Endpunkten automatisch gesetzt.

Rate-Limiting

Jedes Bearer-Token ist auf 60 Anfragen pro Minute mit einem Burst von 600 Anfragen begrenzt. Ueberschreitung gibt HTTP 429 mit Retry-After zurueck. Validator-Crawls (10-15 Endpunkte hintereinander) bleiben deutlich innerhalb des Bursts.

Wenn eine Stadt das Limit konsequent ueberschreitet, erhoehen Sie es ueber Einstellungen -> API & Integrationen -> MDS-Tokens -> Rate-Limit-Override.

Validator-Konfiguration

Der OMF-mds-provider-validator (und das interne Pendant von Populus) erwartet:

CheckWert
Basis-URLhttps://fleets.levyelectric.com/api/mds/<subaccountId>/provider/v2/
AuthAuthorization: Bearer <token>
JWKShttps://fleets.levyelectric.com/api/mds/<subaccountId>/.well-known/jwks.json
Spec-Version2.0.1
Content-Typeapplication/vnd.mds+json;version=2.0

Fuehren Sie den Validator selbst aus, bevor Sie an eine Stadt uebergeben:

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"

Ein sauberer Lauf meldet Passed: all endpoints conform. Etwaige Fehler werden mit dem betroffenen Feld und Endpunkt angezeigt und sollten behoben werden, bevor die URL an eine Stadt geht.

Fehlerbehebung

  • 401 bei jeder Anfrage -- Bearer-Token fehlt oder wurde widerrufen. Neues aus Einstellungen ausstellen.
  • Validator scheitert am version-Feld -- bestaetigen, dass der Response-Envelope 2.0.1 meldet, nicht 2.0.0. Die Version ist eine Konstante in src/lib/mds/v2/response.ts.
  • MDS-JWT-Header fehlt in einer Antwort -- das JWKS-Schluesselpaar konnte nicht generiert werden. /api/mds/<subaccountId>/.well-known/jwks.json direkt aufrufen, um eine Lazy-Mint zu erzwingen, dann erneut versuchen.
  • current_speed_limit_kph immer 0 -- das Fahrzeug hat keine letzte bekannte Position, oder keine Betreiberzonen / Policy-Geofences enthalten es. Pruefen, dass eine aktuelle GPS-Probe in vehicle_events existiert.

Siehe Fehlerbehebung fuer die vollstaendige Checkliste.

Was kommt als naechstes?