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. STIB does not publish a GTFS-RT feed: its real-time API gives anonymous positions (a line,
a destination, the last stop passed and the metres since), with no vehicle or trip identifier.
The MobilityTwin.Brussels platform reconstructs both, live: vehicle-trip gives every vehicle
with its GTFS trip and delay, and gtfs-rt-vehicle-position and gtfs-rt-trip-update
publish the same as standard GTFS-RT feeds, see STIB GTFS Realtime.
import gtfs_kit as gk
import requests
import tempfile
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/gtfs"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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 v0.4.0.
Refresh: Daily (regenerated from the GTFS feed)
From
2024-04-05 06:20:01
To
2026-10-06 06:20:01
Records
689
# pip install gtfs-parquet>=0.4.0
import requests
import tempfile
from gtfs_parquet import read_parquet
from gtfs_parquet.ops.network import describe
from gtfs_parquet.ops.calendar import get_first_week, compute_busiest_date
from gtfs_parquet.ops.routes import compute_route_stats
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/gtfs-parquet"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).content
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as f:
f.write(data)
feed = read_parquet(f.name)
# Quick summary of the feed
print(describe(feed))
# Find the busiest date in the first week
week = get_first_week(feed)
busiest = compute_busiest_date(feed, week)
print(f"Busiest date: {busiest}")
# Compute per-route statistics for that week
route_stats = compute_route_stats(feed, week)
print(route_stats.sort("num_trips", descending=True))
Segments
/stib/segments
GEOJSONAPPLICATION/JSON
The segments of the STIB/MIVB network
Refresh: Daily (derived from the STIB shapefile and stops)
From
2024-08-21 14:48:24
To
2026-10-06 05:20:01
Records
660
import requests
import geopandas as gpd
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/segments"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).json()
gdf = gpd.GeoDataFrame.from_features(data["features"], crs=4326)
# One stop-to-stop segment per feature; `distance` orders them along the line
line = gdf[(gdf['line_id'] == '71') & (gdf['direction'] == 1)].sort_values('distance')
line['length_m'] = line.to_crs(31370).geometry.length # Belgian Lambert 72, metres
print(line[['start', 'end', 'length_m']].round(0).to_string(index=False))
# Plot the whole network in the lines' own colours
gdf.plot(figsize=(12, 10), color=gdf['color'], linewidth=1)
Stops
/stib/stops
GEOJSONAPPLICATION/JSON
The stops of STIB/MIVB. The data was enriched and cleaned for easier use.
Refresh: Daily (derived from stops-by-line and stop-details)
From
2024-08-21 14:48:23
To
2026-10-06 02:20:00
Records
290
import requests
import geopandas as gpd
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/stops"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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. For long time ranges, download the history as Parquet files through /parquetized (see Bulk history).
Refresh: Every 20 seconds
From
2023-02-24 17:55:46
To
2026-10-07 02:00:20
Records
5,093,864
import requests
import pandas as pd
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/vehicle-distance"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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. To track a vehicle across consecutive queries, use the uuid field which remains stable for the duration of a trip.
Refresh: Every 20 seconds (computed from vehicle-distance)
From
2024-08-21 14:54:58
To
2026-10-07 01:59:57
Records
2,971,267
import requests
import geopandas as gpd
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/vehicle-position"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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)
Vehicle position with trip
/stib/vehicle-trip
GEOJSONAPPLICATION/JSON
Every STIB/MIVB vehicle on the road, every 20 seconds, with the GTFS trip it is running and its delay. STIB's real-time API names neither vehicles nor trips. This endpoint tracks each vehicle from poll to poll under the constraint that vehicles on a line do not overtake each other, reads its stop calls as they happen, and pairs the vehicles on the road with the timetabled trips that could be running by the smallest total deviation between observed and scheduled times. It is the live form of the method behind the punctuality table. Each feature carries uuid (stable for as long as the vehicle is tracked on one run), lineId, directionId (the destination stop), direction (GTFS direction_id), pointId and distanceFromPoint, routeId, and where a trip was established tripId, startDate, startTime, delay (seconds late at the last stop called at, negative when early), stopId and stopSequence (the stop the vehicle is at or heading to) and status (STOPPED_AT or IN_TRANSIT_TO). A vehicle gets its trip after its second stop call, about a minute after leaving its first stop; a vehicle still waiting at its first stop has none, since which waiting vehicle makes the next departure is not knowable from the feed. Measured over a full replayed day, 91% of the vehicles that had left their first stop carried a trip, and 93% of those trips were the ones the daily method assigns with the whole day in hand (88% at the morning peak, 97% off-peak). For long time ranges, download the history as Parquet files through /parquetized (see Bulk history).
Refresh: Every 20 seconds (computed from vehicle-distance)
From
2025-10-07 06:21:17
To
2026-10-07 01:59:57
Records
1,329,699
import requests
url = "https://api.mobilitytwin.brussels/stib/vehicle-trip"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).json()
# Every vehicle on the road, with its GTFS trip and delay where established
for feature in data["features"]:
p = feature["properties"]
if p["tripId"]:
print(f"Line {p['lineId']} trip {p['tripId']} ({p['startTime']}): "
f"{p['delay']:+d} s, {p['status']} stop {p['stopId']}")
else:
print(f"Line {p['lineId']} vehicle {p['uuid'][:8]}: no trip yet")
GTFS-RT Vehicle positions
/stib/gtfs-rt-vehicle-position
GTFS-RTAPPLICATION/OCTET-STREAM
STIB/MIVB vehicle positions as a standard GTFS Realtime feed, built from vehicle-trip — STIB itself publishes no GTFS-RT. One VehiclePosition per vehicle, with trip_id, start_date and start_time where the trip was established, and otherwise only route_id and direction_id. vehicle.id is the tracking uuid, stable for one run. IDs match the gtfs endpoint of the same day.
Refresh: On request, from the latest vehicle-trip snapshot (every 20 seconds)
Availability depends on source data
import requests
from google.transit import gtfs_realtime_pb2
url = "https://api.mobilitytwin.brussels/stib/gtfs-rt-vehicle-position"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).content
feed = gtfs_realtime_pb2.FeedMessage()
feed.ParseFromString(data)
# List all vehicle positions (trip_id is empty for a vehicle not yet matched)
for entity in feed.entity:
v = entity.vehicle
print(f"Route {v.trip.route_id} trip {v.trip.trip_id or '-'}: "
f"({v.position.latitude:.4f}, {v.position.longitude:.4f}), "
f"stop {v.stop_id or '-'} seq {v.current_stop_sequence}")
GTFS-RT Trip updates
/stib/gtfs-rt-trip-update
GTFS-RTAPPLICATION/OCTET-STREAM
Predicted arrivals for STIB/MIVB as a standard GTFS Realtime feed, built from vehicle-trip — STIB itself publishes no GTFS-RT. One TripUpdate per trip that has a vehicle on it: the delay measured at the last stop the vehicle called at is applied to every stop still ahead of it (arrival and departure carry both delay and absolute time). Trips with no vehicle on them are left out rather than declared on time, and there are no cancellations: the source has no signal for them.
Refresh: On request, from the latest vehicle-trip snapshot (every 20 seconds)
Availability depends on source data
import requests
from google.transit import gtfs_realtime_pb2
from datetime import datetime
url = "https://api.mobilitytwin.brussels/stib/gtfs-rt-trip-update"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}).content
feed = gtfs_realtime_pb2.FeedMessage()
feed.ParseFromString(data)
# Predicted arrival at the next stops of every trip with a vehicle on it
for entity in feed.entity:
update = entity.trip_update
upcoming = [(s.stop_id, datetime.fromtimestamp(s.arrival.time).strftime("%H:%M"))
for s in update.stop_time_update[:3]]
print(f"Trip {update.trip.trip_id} ({update.delay:+d} s): {upcoming}")
Punctuality
/stib/punctuality
PARQUETAPPLICATION/OCTET-STREAM
Daily punctuality table for STIB/MIVB, reconstructed from the anonymous vehicle-distance feed. STIB publishes no GTFS-RT trip updates and no vehicle identity: the source is a set of positions every 20 seconds, each carrying only a line, a direction, the stop point last passed and the metres since. This table is therefore *derived*, not reported. Vehicles are recovered by aligning consecutive polls under the constraint that vehicles on a line do not overtake each other; stop calls are read where a vehicle's distance along the line crosses a stop; and GTFS trip ids are assigned by a second order-preserving alignment of the day's journeys against the day's timetable. Columns match the other operators' punctuality tables, with seven appended: `route_gtfs_id`, `journey_id`, `match_deviation_minutes`, `observed`, `inferred`, `missing_reason` and `dwell_share`. Two limits are structural and must be read before use. **`cancelled` is always false** — the feed carries no cancellation signal at all, and an unmatched trip is equally evidence that the tracker lost the vehicle, so no cancellation is claimed rather than guessed. Do not compute a STIB cancellation rate from this table. **`observed` marks a measured time and `inferred` a derived one**; they are mutually exclusive, and a consumer wanting measurement only should filter on `observed`. Rows with neither carry a `missing_reason`. `trip_schedule_relationship` is 0 for a timetabled trip and 1 (ADDED) for a run matched to none, whose `trip_id` starts with `added-`. Typical quality, measured across 146 days spanning April 2024 to August 2026: about 92% of timetabled trips matched on days without a feed outage, 88% of scheduled stop calls carrying a measured time and 94% once inference is included, with a median match deviation of 1.3 minutes. Quality does not degrade with age. The denominator is the timetable, and since no cancellation signal exists, a trip that was short-turned or never ran keeps all of its scheduled calls.
Refresh: Daily (one file per Brussels service day)
From
2024-04-06 00:00:00
To
2026-10-07 00:00:00
Records
909
# pip install polars requests
import io
import requests
import polars as pl
from datetime import datetime, timedelta, timezone
# Yesterday's Brussels-day file (the harvester anchors windows to local midnight,
# so any timestamp inside that day returns the same parquet).
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {"timestamp": yesterday_noon.isoformat()} # ISO accepted; epoch also works
url = "https://api.mobilitytwin.brussels/stib/punctuality"
data = requests.get(url, headers={
"Authorization": "Bearer [MY_API_KEY]"
}, params=params).content
df = pl.read_parquet(io.BytesIO(data))
print(f"rows: {df.height:,}, trips: {df['trip_id'].n_unique():,}")
# STIB publishes no realtime feed: every time is reconstructed from vehicle
# positions. Keep the measured ones (observed) of timetabled trips.
measured = df.filter(pl.col("observed") & (pl.col("trip_schedule_relationship") == 0))
delays = (
measured.group_by("route_id") # route_id is the line number
.agg([
pl.col("arrival_delay").median().alias("median_delay_s"),
(pl.col("arrival_delay") > 300).mean().round(3).alias("share_over_5_min"),
pl.len().alias("stop_calls"),
])
.sort("median_delay_s", descending=True)
)
print("\nLines by median arrival delay:")
print(delays.head(10))
Speed
/stib/speed
JSONAPPLICATION/JSON
The speed of the vehicles of STIB/MIVB over their last move, per line, stop and direction: STIB refreshes a vehicle's distance about every 40 seconds, and a move is timed from when the previous distance first appeared to when the new one did.
Refresh: Every 20 seconds (computed from vehicle-distance)
From
2024-08-21 02:00:04
To
2026-10-07 02:00:20
Records
3,145,467
import requests
import pandas as pd
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/speed"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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.
Refresh: Every 20 seconds (10-minute rolling average)
From
2024-08-21 04:23:11
To
2026-10-07 02:00:20
Records
2,727,674
import requests
import pandas as pd
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/aggregated-speed"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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.
Refresh: On request — aggregated from vehicle-position over the queried interval
Availability depends on source data
import requests
import movingpandas as mpd
from datetime import datetime, timedelta, timezone
end = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
start = end - timedelta(hours=1)
params = {
"start_timestamp": start.isoformat(), # ISO accepted; epoch also works
"end_timestamp": end.isoformat(),
}
url = "https://api.mobilitytwin.brussels/stib/trips"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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
Refresh: Daily (derived from the STIB shapefile)
From
2024-08-21 14:48:24
To
2026-10-06 05:20:01
Records
714
import requests
import geopandas as gpd
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/shapefile"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).json()
gdf = gpd.GeoDataFrame.from_features(data["features"])
# Plot the full STIB network colored by line
gdf.plot(figsize=(12, 10), color=gdf['color_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.
Refresh: Daily
From
2026-04-29 11:31:03
To
2026-10-06 02:20:00
Records
107
import requests
import json
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/stop-details"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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.
Refresh: Daily
From
2026-04-29 11:31:02
To
2026-10-06 02:20:00
Records
107
import requests
import json
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/stops-by-line"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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.
Refresh: Every 60 seconds
From
2026-03-30 11:00:43
To
2026-10-07 02:00:28
Records
343,184
import requests
import json
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/waiting-times"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).json()
# Show next arrivals at each stop
for entry in data['results'][:10]:
times = json.loads(entry['passingtimes'])
for t in times:
dest = t.get('destination', {}).get('fr', '?') # absent on a "last departure" message
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.
Refresh: Every 15 minutes
From
2026-03-30 11:15:23
To
2026-10-07 01:55:55
Records
13,405
import requests
import json
from datetime import datetime, timedelta, timezone
yesterday_noon = (datetime.now(timezone.utc) - timedelta(days=1)).replace(
hour=12, minute=0, second=0, microsecond=0
)
params = {
"timestamp": yesterday_noon.isoformat(), # ISO accepted; epoch also works
}
url = "https://api.mobilitytwin.brussels/stib/travellers-information"
data = requests.get(url, headers={
'Authorization': 'Bearer [MY_API_KEY]'
}, params=params).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].get('en') or content[0]['text'][0]['fr'] # a few alerts are French/Dutch only
line_ids = ', '.join(l['id'] for l in lines)
print(f"[Priority {alert['priority']}] Lines {line_ids}: {text}")