research/transjakarta-live-data/analysis.md
hermes 3a518be39f Add transjakarta-live-data research topic
Verified TransJakarta realtime stack: anonymous MQTT bus feed (wss://mqtt.tj.co.id:8084/mqtt), guest REST API (tijeapi.transjakarta.co.id), official GTFS. Cross-capture verification vs third-party tracker, connection recipes, snapshot tool.
2026-10-12 09:34:23 +00:00

4.8 KiB
Raw Permalink Blame History

Analysis — architecture and verification evidence

How the stack works

  1. Buses carry GPS devices; vehicles are identified by fleet body numbers (MYS-…, TJ-…, KWK-…, DMR-…, PRM-…; the prefix appears to be operator/depot).
  2. TransJakarta's backend ingests positions and publishes each bus to its MQTT broker; REST state backs the official app.
  3. Client flow: login (guest works) → POST /v1/bus with your location → you receive nearby buses plus MQTT topic wildcards for the map cells around you → subscribe those on the broker → live positions stream in. REST covers routes/schedules; GTFS covers static geometry.
  4. The upstream path from vehicle hardware into the broker is inferred — it is not observable from outside.

The MQTT feed

  • Broker: wss://mqtt.tj.co.id:8084/mqtt — MQTT 3.1.1 over WebSocket Secure, anonymous (no username/password; verified with independent clients).
  • Topic schema: /mobile_armada/<grid-cell>/<route-code>/<trip-id>/<bus-body-no>
    • Real captured example: /mobile_armada/0021-0014/JAK.49/JAK.49-L03/KWK-1518
    • <grid-cell> = {lat-index}-{lng-index}, a ~0.01° (~1.1 km) map cell. Verified by probing the discovery API with offset coordinates: ±0.1° of latitude/longitude shifts the matching index by ±10 (index grows southward and eastward).
  • Retained messages: subscribing delivers ~2,000+ last-known states within seconds, then live updates continue. Per-bus refresh observed at ~8–75 s (some active vehicles faster; many ~25–37 s); delivery is bursty.
  • Fleet size observed live: 3,300–3,722 buses.

Payload schema (complete key list, captured verbatim)

latitude, longitude, snap_latitude, snap_longitude, bearing, estimated_time_next_stops, estimated_distance_next_stops, curr_stops, next_stops, prev_stops, passengers_status, route_code, route_long_name, route_color, route_text_color, trip_short_name, trip_headsign, bus_type, type, trip_id, bus_body_no, stops[], timestamp, last_update, eta_by_parent_stop_id, next_parent_gate, next_parent_gate_eta, next_gate, next_gate_eta, direction, eta.

  • stops[] items: name, stop_id, latitude, longitude, stop_sequence, time_from_prev, time_wait, headsign, parent_stop_id, parent_stop_name, eta.
  • timestamp is epoch milliseconds (e.g. 1791797549000). Stop IDs (B00436P-style) match GTFS.
  • Some fields are null for some vehicles (e.g. gate fields, passengers_status, last_update).

The REST API

  • POST /v1/auth/login/guest → token + refresh_token + user{email: <random>@guest.tije.com, is_guest: true}. Token lifetime ~24 h. Headers used: User-Agent: okhttp/4.12.0, X-App-OS: android, X-App-Version: 2.10.2, X-Device-ID: <string>.
  • POST /v1/bus body {latitude, longitude, radius, event: "change_loc"} → data { buses[], polyline, session_id, stops[], topics[] }. topics = the MQTT wildcards for nearby cells (e.g. /mobile_armada/0018-0022/# …). This is the app's discovery mechanism.
  • GET /v1/bus?latitude=&longitude=&radius= → array of nearby bus records (~15 at radius=1 near Monas; radius units unverified).

Verification case: oyenza serves this same data

Simultaneous captures (node WS client on oyenza's stream + node MQTT client on TJ's broker):

  • 1,794 buses common to both (of 1,823 seen on the broker in the same window).
  • 100% agreement on trip_id and route_code; 99.7% on headsign.
  • Position deltas: median 8.9 m, p90 42 m, 1,783/1,794 ≤ 100 m — consistent with reading the same moving buses a few seconds apart; no other explanation fits.
  • Fleet overlap 97.8% over a 75 s window (2,869/2,933); remainder = capture-window skew.
  • Category taxonomy maps 1:1, including the edge case: the app's whole "other" bucket (2 buses) = the only two untyped buses in the feed.

oyenza's own layer (what it adds): fleet-state backend; browser protocol hello/feed/snap/delta with positions packed as integers ×10⁵; deltas every 3.00 s (~1,000 position records each); full snapshot 3,722 buses; its own REST extras; a replay mode. Static layer from the official GTFS (240 routes, 698 shapes, 7,493 stops at capture). Its About panel states: "Posisi bus — feed langsung TransJakarta" and "Tidak berafiliasi".

Confidence

  • Established (verified by direct capture): guest token; both bus endpoints; anonymous MQTT access; topic schema + grid decoding; payload schema; retained behavior; GTFS file; Traccar-based bustrack; oyenza data identity.
  • Community-documented (not independently re-verified): ~105 REST endpoint count; Tara chatbot WS; Kong gateway; refresh-token flow.
  • Inferred: vehicle→broker ingest path; oyenza's exact subscription method (whole-tree vs per-cell — data identity holds either way); radius units.