Files
openjsp/UPSTREAM-API-DOCUMENTATION.md

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 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:

[
  {
    "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:

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

  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).



Support

For issues with the ModeShift API:

For issues with this tracker application:


Last Updated: February 7, 2026
API Provider: ModeShift
Coverage: Skopje, North Macedonia