# GTFS API Documentation Comprehensive API documentation for the Skopje Bus Tracker GTFS-based API. ## Table of Contents - [Overview](#overview) - [OpenAPI Specification](#openapi-specification) - [Base URL](#base-url) - [GTFS Static Data Endpoints](#gtfs-static-data-endpoints) - [Real-Time Data Endpoints](#real-time-data-endpoints) - [Historical Data Endpoints](#historical-data-endpoints) - [Statistics & Analytics Endpoints](#statistics--analytics-endpoints) - [Data Models](#data-models) - [Error Handling](#error-handling) - [Rate Limiting](#rate-limiting) --- ## Overview This API provides access to Skopje public transport data based on the **GTFS (General Transit Feed Specification)** standard. It combines static GTFS data with real-time information and historical analytics stored in TimescaleDB. ### Key Features - **GTFS-compliant** static data (stops, routes) - **Real-time** vehicle positions and arrival predictions - **Historical** time-series data for analytics - **Statistical** analysis of delays and patterns ### Upstream API This application consumes data from the **ModeShift GTFS API**. For documentation on the upstream API endpoints: - See **[UPSTREAM-API-DOCUMENTATION.md](UPSTREAM-API-DOCUMENTATION.md)** --- ## OpenAPI Specification A complete **OpenAPI 3.0** specification is available at [openapi.yaml](openapi.yaml). This machine-readable specification includes: - All API endpoints with parameters and responses - Request/response schemas and data models - Examples for all operations - Error response formats ### Using the OpenAPI Spec **Interactive Documentation (Local):** Once the server is running, visit: ``` http://localhost:3000/api-docs.html ``` This provides an interactive Swagger UI where you can: - Browse all API endpoints - View request/response schemas - Try out API calls directly from the browser - Download the OpenAPI specification **Interactive Documentation with Swagger UI (Alternative):** ```bash # Using Docker docker run -p 8080:8080 -e SWAGGER_JSON=/openapi.yaml -v $(pwd)/openapi.yaml:/openapi.yaml swaggerapi/swagger-ui # Or use online editor # Visit: https://editor.swagger.io/ # Then paste the contents of openapi.yaml ``` **Generate Client Libraries:** ```bash # Install OpenAPI Generator npm install -g @openapitools/openapi-generator-cli # Generate TypeScript client openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o ./client/typescript # Generate Python client openapi-generator-cli generate -i openapi.yaml -g python -o ./client/python # Generate Go client openapi-generator-cli generate -i openapi.yaml -g go -o ./client/go ``` **VS Code Integration:** Install the "OpenAPI (Swagger) Editor" extension to view and edit the spec with validation and IntelliSense. --- ## Base URL ``` http://localhost:3000 ``` For production, replace with your deployed server URL. --- ## GTFS Static Data Endpoints ### Get Configuration Retrieve configuration including default stop and route information. **Endpoint:** `GET /api/config` **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `stopId` | string | No | Specific stop ID to query (defaults to configured default) | | `routeId` | string | No | Specific route ID to query (defaults to configured default) | **Response:** ```json { "stop": { "id": "1563", "code": "363", "name": "KARPOS II - STIV NAUMOV", "lat": 41.97964, "lon": 21.41283 }, "route": { "id": "125", "shortName": "7", "longName": "Centar - GTC - Karpos 4 - Karposh 2" }, "defaults": { "stopId": "1563", "routeId": "125" } } ``` **Example:** ```bash curl "http://localhost:3000/api/config?stopId=1571&routeId=125" ``` --- ### Get All Stops Retrieve all GTFS stops in the system. **Endpoint:** `GET /api/stops` **Query Parameters:** None **Response:** ```json [ { "id": "1563", "code": "363", "name": "KARPOS II - STIV NAUMOV", "lat": 41.97964, "lon": 21.41283 }, { "id": "1571", "code": "371", "name": "AMERICAN COLLEGE - VASIL GLAVINOV", "lat": 42.00437, "lon": 21.39687 } ] ``` **Example:** ```bash curl "http://localhost:3000/api/stops" ``` **Use Cases:** - Populate stop selection dropdowns - Build stop search functionality - Generate maps with all stops --- ### Get All Routes Retrieve all GTFS routes in the system. **Endpoint:** `GET /api/routes` **Query Parameters:** None **Response:** ```json [ { "id": "125", "shortName": "7", "longName": "Centar - GTC - Karpos 4 - Karposh 2" }, { "id": "128", "shortName": "2", "longName": "Kisela Voda - Centar - 11 Oktomvri - Avtokomanda" } ] ``` **Example:** ```bash curl "http://localhost:3000/api/routes" ``` **Use Cases:** - Display available routes - Filter by route number - Route selection interfaces --- ## Real-Time Data Endpoints ### Get Real-Time Arrivals Get upcoming bus arrivals at a specific stop with real-time predictions. **Endpoint:** `GET /api/arrivals` **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `stopId` | string | No | Stop ID to query (defaults to configured default) | | `routeId` | string | No | Filter by specific route ID (defaults to configured default) | **Response:** ```json [ { "arrivalTime": "2026-02-07T14:35:00.000Z", "scheduledTime": "2026-02-07T14:33:00.000Z", "minutesUntil": 8, "delaySeconds": 120, "headsign": "Karpos 2", "isRealtime": true, "realtimeState": "UPDATED" }, { "arrivalTime": "2026-02-07T14:48:00.000Z", "scheduledTime": "2026-02-07T14:48:00.000Z", "minutesUntil": 21, "delaySeconds": 0, "headsign": "Centar", "isRealtime": true, "realtimeState": "SCHEDULED" } ] ``` **Fields:** - `arrivalTime`: Predicted arrival time (ISO 8601) - `scheduledTime`: Originally scheduled time (ISO 8601) - `minutesUntil`: Minutes until arrival (can be negative if bus passed) - `delaySeconds`: Delay in seconds (positive = late, negative = early) - `headsign`: Destination shown on bus - `isRealtime`: Whether prediction is based on real-time data - `realtimeState`: `SCHEDULED`, `UPDATED`, or `CANCELED` **Example:** ```bash curl "http://localhost:3000/api/arrivals?stopId=1563&routeId=125" ``` **Notes:** - Returns arrivals from 2 minutes ago up to configured minutes ahead - Results are sorted by arrival time - Data is also logged to database for historical analysis --- ### Get Real-Time Vehicle Positions Get current positions of all vehicles on a specific route. **Endpoint:** `GET /api/vehicles` **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `routeId` | string | No | Route ID to filter vehicles (defaults to configured default) | **Response:** ```json [ { "id": 12345, "vehicleId": "MK-SK-1234", "label": "1234", "lat": 41.9981, "lon": 21.4254, "bearing": 87.5, "speed": 15.3, "timestamp": "2026-02-07T14:27:15Z", "tripId": "trip_789", "currentStopSequence": 0, "currentStatus": "IN_TRANSIT_TO" } ] ``` **Fields:** - `id`: Internal vehicle database ID - `vehicleId`: Vehicle identification number (license plate) - `label`: Bus inventory number (shown on bus) - `lat`/`lon`: Current GPS coordinates - `bearing`: Direction of travel (0-360 degrees, 0=North) - `speed`: Current speed in km/h - `timestamp`: Last position update time - `tripId`: GTFS trip ID vehicle is currently serving - `currentStatus`: `IN_TRANSIT_TO`, `STOPPED_AT`, or `UNKNOWN` **Example:** ```bash curl "http://localhost:3000/api/vehicles?routeId=125" ``` **Notes:** - Combines GTFS-RT trip updates with JSON vehicle API - Vehicle positions are logged to database - Updates typically every 10-30 seconds from source --- ## Historical Data Endpoints ### Get Database Statistics Get overview statistics about stored historical data. **Endpoint:** `GET /api/stats/db` **Query Parameters:** None **Response:** ```json { "vehiclePositions": { "totalRecords": 1523847, "oldestRecord": "2026-01-15T08:00:00.000Z", "newestRecord": "2026-02-07T14:27:30.000Z", "dataRetentionDays": 90 }, "arrivalRecords": { "totalRecords": 456231, "oldestRecord": "2026-01-15T08:00:00.000Z", "newestRecord": "2026-02-07T14:27:30.000Z", "dataRetentionDays": 90 }, "databaseSize": "2.3 GB", "status": "healthy" } ``` **Example:** ```bash curl "http://localhost:3000/api/stats/db" ``` --- ### Get Vehicle Position History Retrieve historical GPS positions for a specific vehicle. **Endpoint:** `GET /api/history/vehicle/:vehicleId` **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `vehicleId` | string | Yes | Vehicle identification number or label | **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `hours` | integer | No | 24 | Number of hours to look back | **Response:** ```json [ { "time": "2026-02-07T10:00:00.000Z", "vehicleId": "MK-SK-1234", "routeId": "125", "tripId": "trip_789", "latitude": 41.9981, "longitude": 21.4254, "speed": 15.3, "bearing": 87.5, "currentStatus": "IN_TRANSIT_TO" } ] ``` **Example:** ```bash curl "http://localhost:3000/api/history/vehicle/1234?hours=12" ``` **Use Cases:** - Visualize vehicle path on map - Analyze driving patterns - Calculate actual travel times --- ### Get Route Vehicle History Get historical positions of all vehicles that served a specific route. **Endpoint:** `GET /api/history/route/:routeId/vehicles` **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `routeId` | string | Yes | GTFS route ID | **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `hours` | integer | No | 24 | Number of hours to look back | **Response:** ```json [ { "time": "2026-02-07T10:00:00.000Z", "vehicleId": "1234", "routeId": "125", "tripId": "trip_789", "latitude": 41.9981, "longitude": 21.4254, "speed": 15.3, "bearing": 87.5 } ] ``` **Example:** ```bash curl "http://localhost:3000/api/history/route/125/vehicles?hours=6" ``` **Use Cases:** - Service frequency analysis - Route coverage visualization - Fleet utilization tracking --- ### Get Stop Arrival History Retrieve historical arrival records for a specific stop. **Endpoint:** `GET /api/history/stop/:stopId/arrivals` **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `stopId` | string | Yes | GTFS stop ID | **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `routeId` | string | No | (all) | Filter by specific route | | `hours` | integer | No | 24 | Number of hours to look back | **Response:** ```json [ { "time": "2026-02-07T10:15:00.000Z", "stopId": "1563", "routeId": "125", "scheduledTime": "2026-02-07T10:15:00.000Z", "predictedTime": "2026-02-07T10:17:30.000Z", "delaySeconds": 150, "isRealtime": true, "headsign": "Karpos 2" } ] ``` **Example:** ```bash curl "http://localhost:3000/api/history/stop/1563/arrivals?routeId=125&hours=48" ``` --- ## Statistics & Analytics Endpoints ### Get Route Delay Statistics Get delay statistics and performance metrics for a route. **Endpoint:** `GET /api/stats/route/:routeId/delays` **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `routeId` | string | Yes | GTFS route ID | **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `hours` | integer | No | 24 | Time window for analysis | **Response:** ```json { "routeId": "125", "periodHours": 24, "statistics": { "totalArrivals": 287, "avgDelaySeconds": 62, "medianDelaySeconds": 45, "minDelaySeconds": -30, "maxDelaySeconds": 420, "stdDeviation": 78.5, "onTimePercentage": 68.3, "earlyPercentage": 8.7, "latePercentage": 23.0, "veryLatePercentage": 4.2 }, "thresholds": { "onTime": "±5 minutes", "veryLate": ">10 minutes" } } ``` **Example:** ```bash curl "http://localhost:3000/api/stats/route/125/delays?hours=72" ``` **Definitions:** - **On-time**: Within ±5 minutes of schedule - **Early**: More than 5 minutes early - **Late**: 5-10 minutes late - **Very Late**: More than 10 minutes late --- ### Get Stop Delay Statistics Get delay statistics for a specific stop. **Endpoint:** `GET /api/stats/stop/:stopId/delays` **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `stopId` | string | Yes | GTFS stop ID | **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `hours` | integer | No | 24 | Time window for analysis | **Response:** ```json { "stopId": "1563", "periodHours": 24, "statistics": { "totalArrivals": 145, "avgDelaySeconds": 72, "medianDelaySeconds": 60, "onTimePercentage": 62.1, "routeBreakdown": [ { "routeId": "125", "routeName": "7", "arrivals": 89, "avgDelaySeconds": 65 }, { "routeId": "128", "routeName": "2", "arrivals": 56, "avgDelaySeconds": 82 } ] } } ``` **Example:** ```bash curl "http://localhost:3000/api/stats/stop/1563/delays?hours=48" ``` **Use Cases:** - Compare reliability across stops - Identify problematic locations - Plan transfer reliability --- ### Get Route Hourly Pattern Analyze delay patterns by hour of day for a route. **Endpoint:** `GET /api/stats/route/:routeId/hourly` **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `routeId` | string | Yes | GTFS route ID | **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `days` | integer | No | 7 | Number of days to analyze | **Response:** ```json { "routeId": "125", "periodDays": 7, "hourlyPattern": [ { "hour": 6, "arrivals": 42, "avgDelaySeconds": 15, "onTimePercentage": 85.7 }, { "hour": 7, "arrivals": 89, "avgDelaySeconds": 45, "onTimePercentage": 71.9 }, { "hour": 8, "arrivals": 124, "avgDelaySeconds": 125, "onTimePercentage": 52.4 } ] } ``` **Example:** ```bash curl "http://localhost:3000/api/stats/route/125/hourly?days=14" ``` **Use Cases:** - Identify rush hour patterns - Compare morning vs evening reliability - Optimize schedules based on actual performance --- ## Data Models ### GTFS Stop ```typescript interface GtfsStop { stop_id: string; // Unique stop identifier stop_code: string; // Public-facing stop code stop_name: string; // Stop name stop_lat: number; // Latitude (WGS84) stop_lon: number; // Longitude (WGS84) } ``` ### GTFS Route ```typescript interface GtfsRoute { route_id: string; // Unique route identifier route_short_name: string; // Public route number (e.g., "7") route_long_name: string; // Full route name route_type?: string; // GTFS route type (0=Tram, 3=Bus, etc.) } ``` ### Vehicle Position ```typescript interface VehiclePosition { time: Date; // Timestamp (ISO 8601) vehicle_id: string; // Vehicle identifier route_id: string; // Route being served trip_id?: string; // Current trip ID latitude: number; // GPS latitude longitude: number; // GPS longitude speed?: number; // Speed in km/h bearing?: number; // Direction (0-360°) current_status: string; // IN_TRANSIT_TO, STOPPED_AT, UNKNOWN } ``` ### Arrival Record ```typescript interface ArrivalRecord { time: Date; // Record timestamp stop_id: string; // Stop ID route_id: string; // Route ID scheduled_time: Date; // Scheduled arrival time predicted_time?: Date; // Real-time predicted time actual_time?: Date; // Actual arrival time (if observed) delay_seconds: number; // Delay in seconds is_realtime: boolean; // Based on real-time data headsign?: string; // Trip headsign } ``` --- ## Error Handling All endpoints return standard HTTP status codes: | Status Code | Description | |-------------|-------------| | `200` | Success | | `404` | Resource not found (e.g., invalid stop/route ID) | | `500` | Server error | **Error Response Format:** ```json { "error": "Description of the error", "details": "Additional error details (when available)" } ``` **Example Error:** ```bash curl "http://localhost:3000/api/config?stopId=invalid" ``` Response (404): ```json { "error": "Stop invalid not found" } ``` --- ## Rate Limiting Currently, there is no rate limiting implemented. For production deployments, consider: - Implementing per-IP rate limits - Using caching for frequently accessed endpoints - Load balancing for high-traffic scenarios **Recommended Polling Intervals:** - Real-time arrivals: 10-30 seconds - Vehicle positions: 10-30 seconds - Historical data: As needed (not time-sensitive) - Statistics: 5-60 minutes (depending on use case) --- ## GTFS Data Update The GTFS static data (stops, routes, schedules) is loaded from the `gtfs/` directory on server startup. **To update GTFS data:** ```bash bun run setup-gtfs.ts ``` This downloads the latest GTFS feed and extracts it. Restart the server to load the new data. --- ## TimescaleDB Setup Historical data and analytics require TimescaleDB. See the [README.md](README.md) for setup instructions. **Quick start:** ```bash cd infrastructure docker compose up -d ``` The database automatically: - Creates hypertables for time-series data - Sets up 90-day data retention - Builds continuous aggregates for hourly metrics - Maintains indexes for fast queries --- ## Example Use Cases ### Building a Real-Time Tracker ```javascript // 1. Get all stops const stops = await fetch('/api/stops').then(r => r.json()); // 2. Get all routes const routes = await fetch('/api/routes').then(r => r.json()); // 3. Poll for arrivals every 15 seconds setInterval(async () => { const arrivals = await fetch('/api/arrivals?stopId=1563&routeId=125') .then(r => r.json()); updateUI(arrivals); }, 15000); // 4. Poll for vehicle positions setInterval(async () => { const vehicles = await fetch('/api/vehicles?routeId=125') .then(r => r.json()); updateMap(vehicles); }, 15000); ``` ### Building Analytics Dashboard ```javascript // Get delay statistics for route const stats = await fetch('/api/stats/route/125/delays?hours=168') .then(r => r.json()); // Get hourly patterns const pattern = await fetch('/api/stats/route/125/hourly?days=30') .then(r => r.json()); // Get stop performance comparison const stopStats = await fetch('/api/stats/stop/1563/delays?hours=168') .then(r => r.json()); // Display charts with the data renderDelayChart(stats); renderHourlyPattern(pattern); renderStopComparison(stopStats); ``` --- ## Additional Resources - **OpenAPI Specification**: [openapi.yaml](openapi.yaml) - Machine-readable API spec - **Upstream API Documentation**: [UPSTREAM-API-DOCUMENTATION.md](UPSTREAM-API-DOCUMENTATION.md) - ModeShift GTFS API docs - **Swagger UI Demo**: https://editor.swagger.io/ (paste openapi.yaml content) - **GTFS Specification**: https://gtfs.org/ - **GTFS-RT Reference**: https://gtfs.org/realtime/ - **TimescaleDB Documentation**: https://docs.timescale.com/ - **Project README**: [README.md](README.md) --- ## Support For issues or questions: 1. Check the [README.md](README.md) for setup instructions 2. Review this documentation 3. Check the source code in [server.ts](server.ts) and [lib/gtfs.ts](lib/gtfs.ts) --- **Last Updated:** February 7, 2026 **API Version:** 1.0.0