STIB is the main public transport operator in Brussels. It has its own open data platform
which provides different data sets. The GTFS feed is used to provide the schedule of the
vehicles. Contrary to the SNCB and other operators, the STIB does not provide a GTFS-RT feed.
It provides different proprietary APIs to get the real-time data. The MobilityTwin.Brussels
platform provides a vehicle_position endpoint which provides the estimated positions of
the vehicles based on the GTFS feed and the proprietary APIs. It also provides the vehicle_schedule
endpoint which provides the schedule of the vehicles per stop for a given period of time.
import gtfs_kit as gk
import requests
import tempfile
url = "https://api.mobilitytwin.brussels/stib/gtfs"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).content
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as f:
f.write(data)
feed = gk.read_feed(f.name, 'm')
# Explore the STIB schedule
print(f"Routes: {len(feed.routes)}")
print(f"Stops: {len(feed.stops)}")
print(f"Trips: {len(feed.trips)}")
print("\nRoutes:")
print(feed.routes[['route_short_name', 'route_long_name', 'route_type']])
GTFS (Parquet)
/stib/gtfs-parquet
GTFS-PARQUETAPPLICATION/ZIP
The GTFS feed of STIB/MIVB converted to Apache Parquet format (zip archive of .parquet files). Parquet uses columnar storage with zstd compression and strong typing, resulting in 40-75% smaller files compared to the original GTFS zip. This format enables extremely efficient data transfer and near-zero RAM overhead when reading specific columns via Polars or DuckDB, making it ideal for analytical workloads and large-scale processing. Produced using gtfs-parquet.
From
2024-08-24 06:20:01
To
2026-04-07 06:20:02
Records
580
import requests
import tempfile
from gtfs_parquet import read_parquet
url = "https://api.mobilitytwin.brussels/stib/gtfs-parquet"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).content
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as f:
f.write(data)
feed = read_parquet(f.name)
# List all routes (Polars DataFrame — zero-copy, extremely low RAM usage)
print(feed.routes.select("route_short_name", "route_long_name"))
# Count trips per route
print(feed.trips.group_by("route_id").len().sort("len", descending=True))
Segments
/stib/segments
GEOJSONAPPLICATION/JSON
The segments of the STIB/MIVB network
From
2024-08-21 14:48:24
To
2026-04-07 05:20:01
Records
502
Stops
/stib/stops
GEOJSONAPPLICATION/JSON
The stops of STIB/MIVB. The data was enriched and cleaned for easier use.
From
2024-08-21 14:48:23
To
2026-04-07 02:20:01
Records
107
import requests
import geopandas as gpd
url = "https://api.mobilitytwin.brussels/stib/stops"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
gdf = gpd.GeoDataFrame.from_features(data["features"])
# List stops for a specific line
line = "1"
line_stops = gdf[gdf['route_short_name'] == line].sort_values('stop_sequence')
print(f"Stops on line {line}:")
for _, stop in line_stops.iterrows():
print(f" {stop['stop_sequence']}. {stop['stop_name']}")
# Plot all stops on a map
gdf.plot(figsize=(10, 8), markersize=3, color="red")
Vehicle distance
/stib/vehicle-distance
JSONAPPLICATION/JSON
This endpoint provides the raw data of the STIB/MIVB proprietary API which returns the distance of each vehicle since the last stop.
From
2023-02-24 17:55:46
To
2026-04-07 22:11:39
Records
4,409,828
import requests
import pandas as pd
url = "https://api.mobilitytwin.brussels/stib/vehicle-distance"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
# Convert to DataFrame for analysis
df = pd.DataFrame(data)
# Group by line and compute average distance from stop
avg_distance = df.groupby('lineId')['distanceFromPoint'].mean()
print("Average distance from stop per line:")
print(avg_distance.sort_values(ascending=False))
Vehicle position
/stib/vehicle-position
GEOJSONAPPLICATION/JSON
The estimated positions of the vehicles based on the GTFS feed and the proprietary APIs. Because the STIB/MIVB proprietary API does not provide the identity of the vehicles, the MobilityTwin.Brussels platform also performs computations to attribute a unique identity to each vehicle along a given trip. These ids do not correspond to the ids of the GTFS feed but are rather generated randomly. The ids are unique for a given trip
From
2024-08-21 14:54:58
To
2026-04-07 22:10:39
Records
2,368,200
import requests
import geopandas as gpd
url = "https://api.mobilitytwin.brussels/stib/vehicle-position"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
gdf = gpd.GeoDataFrame.from_features(data["features"])
# Count vehicles per line
vehicles_per_line = gdf.groupby('lineId').size().sort_values(ascending=False)
print("Active vehicles per line:")
print(vehicles_per_line)
# Plot vehicles colored by line
gdf.plot(figsize=(10, 8), column='color', legend=True, markersize=8)
Speed
/stib/speed
JSONAPPLICATION/JSON
The average speed of the vehicles of STIB/MIVB on a 20 seconds interval, per line, stop and direction.
From
2024-08-21 14:54:58
To
2026-04-07 22:11:39
Records
2,449,563
import requests
import pandas as pd
url = "https://api.mobilitytwin.brussels/stib/speed"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
df = pd.DataFrame(data)
# Average speed per line (km/h)
speed_per_line = df.groupby('lineId')['speed'].mean().sort_values()
print("Average speed per line (km/h):")
print(speed_per_line)
# Find the slowest segments
slowest = df.nsmallest(5, 'speed')[['lineId', 'pointId', 'speed']]
print("\nSlowest segments:")
print(slowest)
Aggregated speed
/stib/aggregated-speed
JSONAPPLICATION/JSON
The average speed of the vehicles of STIB/MIVB on a 10 minutes interval, per line, stop and direction.
From
2024-08-21 15:01:24
To
2026-04-07 22:11:39
Records
2,153,938
import requests
import pandas as pd
url = "https://api.mobilitytwin.brussels/stib/aggregated-speed"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
df = pd.DataFrame(data)
# Compare average speed across lines over 10-minute intervals
speed_by_line = df.groupby('lineId')['speed'].agg(['mean', 'min', 'max'])
print("Speed statistics per line (km/h):")
print(speed_by_line.sort_values('mean'))
Trips
/stib/trips
MF-JSONAPPLICATION/JSON
All the trips of STIB/MIVB for the specified period of time. This is an aggregate of the GeoJSON files returned by the vehicle-position endpoint of MobilityTwin.Brussels.
Availability depends on source data
import requests
import movingpandas as mpd
url = "https://api.mobilitytwin.brussels/stib/trips"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
# Load as a MovingPandas TrajectoryCollection
tc = mpd.io.read_mf_dict(data, traj_id_property="uuid")
# Compute speed and distance for each vehicle trajectory
for traj in tc.trajectories[:5]:
print(f"Vehicle {traj.id}: {traj.get_length():.0f}m, duration: {traj.get_duration()}")
# Plot all vehicle trajectories
tc.plot(figsize=(12, 8), linewidth=0.5)
Shapefile
/stib/shapefile
GEOJSONAPPLICATION/JSON
The shapefile of STIB/MIVB
From
2024-08-21 14:48:24
To
2026-04-07 05:20:01
Records
502
import requests
import geopandas as gpd
url = "https://api.mobilitytwin.brussels/stib/shapefile"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
gdf = gpd.GeoDataFrame.from_features(data["features"])
# Plot the full STIB network colored by line
gdf.plot(figsize=(12, 10), color=gdf['couleur_hex'], linewidth=1)
# List all unique lines
print("STIB lines:", sorted(gdf['ligne'].unique()))
Stop details
/stib/stop-details
JSONAPPLICATION/JSON
Detailed information about each STIB/MIVB stop including GPS coordinates and names in French and Dutch.
From
2026-03-31 02:20:00
To
2026-04-07 02:20:00
Records
8
import requests
import json
url = "https://api.mobilitytwin.brussels/stib/stop-details"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
# Parse and display stop details
for stop in data['results'][:10]:
name = json.loads(stop['name'])
coords = json.loads(stop['gpscoordinates'])
print(f"Stop {stop['id']}: {name['fr']} / {name['nl']} ({coords['latitude']}, {coords['longitude']})")
Stops by line
/stib/stops-by-line
JSONAPPLICATION/JSON
The ordered list of stops for each STIB/MIVB line, per direction.
From
2026-03-31 02:20:00
To
2026-04-07 02:20:01
Records
8
import requests
import json
url = "https://api.mobilitytwin.brussels/stib/stops-by-line"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
# Display the route of a specific line
for route in data['results']:
if route['lineid'] == '1':
dest = json.loads(route['destination'])
stops = json.loads(route['points'])
print(f"Line {route['lineid']} → {dest['fr']} ({route['direction']})")
print(f" {len(stops)} stops: {stops[0]['id']} → {stops[-1]['id']}")
Waiting times
/stib/waiting-times
JSONAPPLICATION/JSON
Real-time waiting times at STIB/MIVB stops with expected arrival times per line and destination.
From
2026-03-30 11:00:43
To
2026-04-07 22:11:49
Records
29,330
import requests
import json
from datetime import datetime
url = "https://api.mobilitytwin.brussels/stib/waiting-times"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
# Show next arrivals at each stop
for entry in data['results'][:10]:
times = json.loads(entry['passingtimes'])
for t in times:
dest = t['destination']['fr']
arrival = datetime.fromisoformat(t['expectedArrivalTime'])
print(f"Stop {entry['pointid']} — Line {entry['lineid']} → {dest} at {arrival:%H:%M}")
Travellers information
/stib/travellers-information
JSONAPPLICATION/JSON
Real-time traveller information messages and service alerts for STIB/MIVB lines and stops.
From
2026-03-30 11:15:23
To
2026-04-07 21:59:19
Records
480
import requests
import json
url = "https://api.mobilitytwin.brussels/stib/travellers-information"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
# Display active service alerts sorted by priority
for alert in sorted(data['results'], key=lambda x: x['priority'], reverse=True):
content = json.loads(alert['content'])
lines = json.loads(alert['lines'])
text = content[0]['text'][0]['en']
line_ids = ', '.join(l['id'] for l in lines)
print(f"[Priority {alert['priority']}] Lines {line_ids}: {text}")