16 KiB
ModeShift GTFS API Documentation
Documentation for the upstream ModeShift GTFS API used by this Skopje Bus Tracker application.
Table of Contents
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.
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 - Paste the YAML content
- 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.
Response Structure (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):
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 identifiertrip.routeId: GTFS route identifiervehicle.id: Vehicle identification numbervehicle.label: Public-facing vehicle numberstopTimeUpdate[]: 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:
[
{
"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 stopped2- Vehicle is in transit
Example Usage:
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:
[
{
"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:
const arrival = new Date(
(stopTime.serviceDay + stopTime.realtimeArrival) * 1000
);
Example Usage:
# 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
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
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
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
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
// 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
// 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
-
Polling Frequency:
- Don't poll more frequently than every 10 seconds
- Cache responses when appropriate
- Use conditional requests when possible
-
Radius Parameter:
- Use smallest radius that meets your needs (typically 50-100m)
- Larger radius = more data returned = slower response
-
Error Handling:
- Handle network errors gracefully
- Implement exponential backoff for failures
- Parse protobuf data carefully (can be empty)
-
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
- Check API-DOCUMENTATION.md for the local API
Last Updated: February 7, 2026
API Provider: ModeShift
Coverage: Skopje, North Macedonia