Files
openjsp/UPSTREAM-API-DOCUMENTATION.md

557 lines
16 KiB
Markdown

# 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