# ModeShift GTFS API Documentation Documentation for the upstream ModeShift GTFS API used by this Skopje Bus Tracker application. ## Table of Contents - [Overview](#overview) - [OpenAPI Specification](#openapi-specification) - [Base URL](#base-url) - [Authentication](#authentication) - [Endpoints](#endpoints) - [GTFS-RT Trip Updates](#gtfs-rt-trip-updates) - [Public Vehicles (JSON)](#public-vehicles-json) - [Nearby Stop Times](#nearby-stop-times) - [Data Models](#data-models) - [Usage Examples](#usage-examples) --- ## Overview The ModeShift API provides real-time transit data for Skopje public transport based on the **GTFS (General Transit Feed Specification)** and **GTFS-Realtime** standards. **Provider:** ModeShift **API Type:** GTFS + GTFS-Realtime **Data Format:** JSON and Protocol Buffers **Coverage:** Skopje, North Macedonia --- ## OpenAPI Specification A complete **OpenAPI 3.0** specification for the ModeShift API is available at [upstream-openapi.yaml](upstream-openapi.yaml). This machine-readable specification includes: - All upstream API endpoints - Request/response schemas - Parameter validation - Examples for each operation You can view it interactively using: - [Swagger Editor](https://editor.swagger.io/) - Paste the YAML content - [Redoc](https://redocly.github.io/redoc/) - For a clean documentation view - VS Code with OpenAPI extensions --- ## Base URL ``` https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e ``` The base URL includes a tenant/agency identifier (`9814b106-2afe-47c8-919b-bdec6a5e521e`) specific to Skopje's transit system. --- ## Authentication Currently, the API appears to be publicly accessible without authentication tokens. Requests can be made directly to the endpoints. --- ## Endpoints ### GTFS-RT Trip Updates Get real-time trip updates in GTFS-Realtime format (Protocol Buffers). **Endpoint:** `GET /transport/gtfsrt/tripupdates.pb` **Full URL:** ``` https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/gtfsrt/tripupdates.pb ``` **Response Format:** Protocol Buffers (binary) **Content-Type:** `application/octet-stream` **Description:** Returns a GTFS-Realtime FeedMessage containing TripUpdate entities. This follows the official [GTFS-Realtime specification](https://gtfs.org/realtime/). **Response Structure (Protobuf):** ```protobuf message FeedMessage { FeedHeader header = 1; repeated FeedEntity entity = 2; } message FeedEntity { required string id = 1; optional TripUpdate trip_update = 3; } message TripUpdate { optional TripDescriptor trip = 1; optional VehicleDescriptor vehicle = 3; repeated StopTimeUpdate stop_time_update = 2; } ``` **Example Usage (JavaScript):** ```javascript import GtfsRealtimeBindings from 'gtfs-realtime-bindings'; const response = await fetch( 'https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/gtfsrt/tripupdates.pb' ); const buffer = await response.arrayBuffer(); const feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode( new Uint8Array(buffer) ); for (const entity of feed.entity) { if (entity.tripUpdate) { console.log('Trip:', entity.tripUpdate.trip.tripId); console.log('Route:', entity.tripUpdate.trip.routeId); console.log('Vehicle:', entity.tripUpdate.vehicle?.id); } } ``` **Key Fields:** - `trip.tripId`: GTFS trip identifier - `trip.routeId`: GTFS route identifier - `vehicle.id`: Vehicle identification number - `vehicle.label`: Public-facing vehicle number - `stopTimeUpdate[]`: Array of stop time predictions --- ### Public Vehicles (JSON) Get current positions and status of all active vehicles in JSON format. **Endpoint:** `GET /transport/public/vehicles` **Full URL:** ``` https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/public/vehicles ``` **Response Format:** JSON **Description:** Returns an array of all currently active vehicles with their real-time GPS positions, speed, bearing, and status information. **Response Structure:** ```json [ { "id": 12345, "identificationNumber": "MK-SK-1234", "inventoryNumber": 1234, "positionLatitude": 41.9981, "positionLongitude": 21.4254, "positionBearing": 87.5, "positionSpeed": 15.3, "positionModifiedAt": "2026-02-07T14:27:15Z", "status": 2 }, { "id": 12346, "identificationNumber": "MK-SK-5678", "inventoryNumber": 5678, "positionLatitude": 42.0012, "positionLongitude": 21.4301, "positionBearing": 180.0, "positionSpeed": 0.0, "positionModifiedAt": "2026-02-07T14:27:10Z", "status": 1 } ] ``` **Fields:** | Field | Type | Description | |-------|------|-------------| | `id` | integer | Internal database identifier for the vehicle | | `identificationNumber` | string | Vehicle license plate or identification (e.g., "MK-SK-1234") | | `inventoryNumber` | integer | Public-facing bus number shown on the vehicle | | `positionLatitude` | number | Current GPS latitude (WGS84) | | `positionLongitude` | number | Current GPS longitude (WGS84) | | `positionBearing` | number | Direction of travel in degrees (0-360, 0=North, 90=East) | | `positionSpeed` | number | Current speed in km/h | | `positionModifiedAt` | string | ISO 8601 timestamp of last position update | | `status` | integer | Vehicle status code (1=stopped, 2=in transit) | **Status Codes:** - `1` - Vehicle is stopped - `2` - Vehicle is in transit **Example Usage:** ```bash curl "https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/public/vehicles" ``` **Use Cases:** - Display all vehicles on a map - Match vehicles to routes using trip update data - Track real-time vehicle movements --- ### Nearby Stop Times Get arrival predictions for stops near a specific location. **Endpoint:** `GET /transport/planner/stops/nearbyTimes` **Full URL:** ``` https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/planner/stops/nearbyTimes ``` **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `latitude` | number | Yes | Search center latitude (WGS84) | | `longitude` | number | Yes | Search center longitude (WGS84) | | `radius` | integer | Yes | Search radius in meters | **Response Format:** JSON **Description:** Returns stops within the specified radius along with their upcoming arrivals, including real-time predictions. **Response Structure:** ```json [ { "id": 1563, "code": "363", "name": "KARPOS II - STIV NAUMOV", "latitude": 41.97964, "longitude": 21.41283, "patterns": [ { "routeId": 125, "routeShortName": "7", "routeLongName": "Centar - GTC - Karpos 4 - Karposh 2", "headsign": "Karpos 2", "stopTimes": [ { "serviceDay": 1738972800, "scheduledArrival": 52380, "realtimeArrival": 52500, "arrivalDelay": 120, "realtime": true, "realtimeState": "UPDATED", "headsign": "Karpos 2" }, { "serviceDay": 1738972800, "scheduledArrival": 53280, "realtimeArrival": 53280, "arrivalDelay": 0, "realtime": true, "realtimeState": "SCHEDULED", "headsign": "Karpos 2" } ] } ] } ] ``` **Fields:** **Stop Level:** | Field | Type | Description | |-------|------|-------------| | `id` | integer | GTFS stop ID | | `code` | string | Public stop code | | `name` | string | Stop name | | `latitude` | number | Stop latitude | | `longitude` | number | Stop longitude | | `patterns[]` | array | Array of route patterns serving this stop | **Pattern Level:** | Field | Type | Description | |-------|------|-------------| | `routeId` | integer | GTFS route ID | | `routeShortName` | string | Public route number (e.g., "7") | | `routeLongName` | string | Full route name | | `headsign` | string | Direction/destination | | `stopTimes[]` | array | Array of upcoming arrivals | **Stop Time Level:** | Field | Type | Description | |-------|------|-------------| | `serviceDay` | integer | Unix timestamp (seconds) of service day start (midnight) | | `scheduledArrival` | integer | Seconds since midnight for scheduled arrival time | | `realtimeArrival` | integer | Seconds since midnight for predicted arrival time | | `arrivalDelay` | integer | Delay in seconds (positive=late, negative=early) | | `realtime` | boolean | Whether prediction is based on real-time data | | `realtimeState` | string | State: `SCHEDULED`, `UPDATED`, `CANCELED` | | `headsign` | string | Trip headsign (destination) | **Calculating Arrival Time:** ```javascript const arrival = new Date( (stopTime.serviceDay + stopTime.realtimeArrival) * 1000 ); ``` **Example Usage:** ```bash # Get arrivals near American College curl "https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/planner/stops/nearbyTimes?latitude=42.00437&longitude=21.39687&radius=50" ``` **Use Cases:** - Display upcoming arrivals at a stop - Show real-time delay information - Calculate minutes until arrival - Filter by specific routes --- ## Data Models ### GTFS Stop ```typescript interface Stop { id: number; // GTFS stop_id (as integer) code: string; // Public stop code name: string; // Stop name latitude: number; // WGS84 latitude longitude: number; // WGS84 longitude patterns: Pattern[]; // Routes serving this stop } ``` ### Route Pattern ```typescript interface Pattern { routeId: number; // GTFS route_id (as integer) routeShortName: string; // Public route number routeLongName: string; // Full route name headsign: string; // Direction/destination stopTimes: StopTime[]; // Upcoming arrivals } ``` ### Stop Time ```typescript interface StopTime { serviceDay: number; // Unix timestamp of service day start scheduledArrival: number; // Seconds since midnight (scheduled) realtimeArrival: number; // Seconds since midnight (predicted) arrivalDelay: number; // Delay in seconds realtime: boolean; // Based on real-time data realtimeState: string; // SCHEDULED, UPDATED, CANCELED headsign: string; // Trip headsign } ``` ### Vehicle ```typescript interface Vehicle { id: number; // Internal ID identificationNumber: string; // License plate inventoryNumber: number; // Bus number positionLatitude: number; // GPS latitude positionLongitude: number; // GPS longitude positionBearing: number; // Direction (0-360°) positionSpeed: number; // Speed in km/h positionModifiedAt: string; // ISO 8601 timestamp status: number; // 1=stopped, 2=in_transit } ``` --- ## Usage Examples ### Complete Real-Time Tracker Flow ```javascript // 1. Get a specific stop's arrivals const stopLat = 41.97964; const stopLon = 21.41283; const radius = 50; const nearbyResponse = await fetch( `https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/planner/stops/nearbyTimes?latitude=${stopLat}&longitude=${stopLon}&radius=${radius}` ); const stops = await nearbyResponse.json(); // 2. Process arrivals for a specific route const targetRouteId = 125; const now = new Date(); for (const stop of stops) { for (const pattern of stop.patterns) { if (pattern.routeId === targetRouteId) { for (const stopTime of pattern.stopTimes) { const arrivalTime = new Date( (stopTime.serviceDay + stopTime.realtimeArrival) * 1000 ); const minutesUntil = Math.floor( (arrivalTime.getTime() - now.getTime()) / 60000 ); console.log(`Route ${pattern.routeShortName} - ${minutesUntil} min`); console.log(`Delay: ${stopTime.arrivalDelay} seconds`); } } } } // 3. Get all vehicle positions const vehiclesResponse = await fetch( 'https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/public/vehicles' ); const vehicles = await vehiclesResponse.json(); // 4. Get trip updates to match vehicles to routes const tripUpdatesResponse = await fetch( 'https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/gtfsrt/tripupdates.pb' ); const buffer = await tripUpdatesResponse.arrayBuffer(); const feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode( new Uint8Array(buffer) ); // 5. Build vehicle-to-route mapping const vehicleRouteMap = new Map(); for (const entity of feed.entity) { if (entity.tripUpdate?.vehicle?.id && entity.tripUpdate?.trip?.routeId) { vehicleRouteMap.set( entity.tripUpdate.vehicle.id, entity.tripUpdate.trip.routeId ); } } // 6. Filter vehicles by route const routeVehicles = vehicles.filter(v => { const vehicleKey = v.identificationNumber || v.inventoryNumber.toString(); return vehicleRouteMap.get(vehicleKey) == targetRouteId; }); console.log(`Found ${routeVehicles.length} vehicles on route ${targetRouteId}`); ``` ### Polling for Updates ```javascript // Recommended polling intervals const VEHICLE_UPDATE_INTERVAL = 10000; // 10 seconds const ARRIVAL_UPDATE_INTERVAL = 15000; // 15 seconds const TRIP_UPDATE_INTERVAL = 10000; // 10 seconds // Poll for vehicle positions setInterval(async () => { const vehicles = await fetch(vehiclesEndpoint).then(r => r.json()); updateMap(vehicles); }, VEHICLE_UPDATE_INTERVAL); // Poll for arrival predictions setInterval(async () => { const arrivals = await fetch(nearbyTimesEndpoint).then(r => r.json()); updateArrivalBoard(arrivals); }, ARRIVAL_UPDATE_INTERVAL); ``` --- ## GTFS Static Data The API uses GTFS static data which should be downloaded separately. Static data includes: - **stops.txt**: All stop locations and names - **routes.txt**: All route definitions - **trips.txt**: Trip schedules - **stop_times.txt**: Stop time schedules - **calendar.txt**: Service calendar - **shapes.txt**: Route shapes (geographic paths) **Note:** The static GTFS feed download URL should be obtained from ModeShift or the transit agency. --- ## Rate Limiting & Best Practices 1. **Polling Frequency**: - Don't poll more frequently than every 10 seconds - Cache responses when appropriate - Use conditional requests when possible 2. **Radius Parameter**: - Use smallest radius that meets your needs (typically 50-100m) - Larger radius = more data returned = slower response 3. **Error Handling**: - Handle network errors gracefully - Implement exponential backoff for failures - Parse protobuf data carefully (can be empty) 4. **Data Volume**: - Vehicle positions endpoint returns ALL vehicles (100+) - Filter client-side rather than making multiple requests - Consider caching vehicle positions --- ## Common Issues & Solutions ### Empty GTFS-RT Feed **Problem:** `tripupdates.pb` returns empty or zero-length buffer **Solution:** This can happen during off-peak hours or system maintenance. Handle gracefully and retry later. ### Vehicle Matching **Problem:** Vehicles in JSON don't match trip updates **Solution:** Try matching by both `identificationNumber` and `inventoryNumber`. Some vehicles may not have active trips. ### Time Zone Issues **Problem:** Arrival times are incorrect **Solution:** The API uses Europe/Skopje timezone. Service day timestamps are at midnight local time. ### Delay Calculation **Problem:** Delays seem wrong **Solution:** Use `realtimeArrival - scheduledArrival` (already provided as `arrivalDelay` field). --- ## Related Documentation - **GTFS Specification**: https://gtfs.org/ - **GTFS-Realtime**: https://gtfs.org/realtime/ - **Protocol Buffers**: https://protobuf.dev/ - **gtfs-realtime-bindings**: https://www.npmjs.com/package/gtfs-realtime-bindings --- ## Support For issues with the ModeShift API: - Contact ModeShift support - Check ModeShift documentation at https://www.modeshift.app/ For issues with this tracker application: - See [README.md](README.md) - Check [API-DOCUMENTATION.md](API-DOCUMENTATION.md) for the local API --- **Last Updated:** February 7, 2026 **API Provider:** ModeShift **Coverage:** Skopje, North Macedonia