Guides
Docs
UK bus times in Python
This guide takes a UK postcode, finds the bus stops nearest to it, and lists
what's leaving each one in the next hour. It works anywhere in England,
Scotland and Wales. It uses three endpoints and the requests
library.
What you need
- Python 3.9 or later, and
pip install requests - A TransCAPI key: sign up free,
then
export TRANSCAPI_KEY=your_key
The code
import os
from datetime import datetime
from zoneinfo import ZoneInfo
import requests
API = "https://api.transcapi.com/v1"
HEADERS = {"X-Api-Key": os.environ["TRANSCAPI_KEY"]}
UK = ZoneInfo("Europe/London")
def uk_time(iso):
"""API times are UTC; show them as a UK clock time."""
return datetime.fromisoformat(iso).astimezone(UK).strftime("%H:%M")
# 1. Turn a postcode into a point
place = requests.get(f"{API}/places.json", params={"query": "CB2 3QZ", "type": "postcode"},
headers=HEADERS).json()["member"][0]
# 2. Find the nearest bus stops, closest first
stops = requests.get(f"{API}/bus/stops.json",
params={"lat": place["latitude"], "lon": place["longitude"], "radius": 400},
headers=HEADERS).json()["member"]
# 3. What's leaving each of the three nearest in the next hour
for stop in stops[:3]:
print(f"\n{stop['name']} ({stop['indicator']}), towards {stop['towards']}")
board = requests.get(f"{API}/bus/stop_timetables/{stop['atcocode']}.json",
params={"to_offset": 60}, headers=HEADERS).json()
for d in board["departures"][:5]:
when = uk_time(d.get("expected_departure_time") or d["aimed_departure_time"])
late = f" ({round(d['delay_seconds'] / 60)} min late)" if d.get("delay_seconds") else ""
print(f" {when} {d['line']:>4} {d['destination']}{late}")
if not board["departures"]:
print(" nothing in the next hour")
Run it:
$ python bus_times.py
Bene't Street (opp), towards Dane Drive
nothing in the next hour
St Andrew's Street (Stop S3), towards Arbury Campkin Road
16:32 1 Arbury Campkin Road
16:40 3 Thorpe Way 1St Stop
16:44 1 Arbury Campkin Road
16:55 3 Thorpe Way 1St Stop
16:56 1 Arbury Campkin Road
St Andrew's Street (Stop S2), towards Milton Tesco
16:28 2 Milton Tesco
16:48 2 Milton Tesco
17:08 2 Milton Tesco
Real output from central Cambridge, 29 September 2026.
How it works
1. Postcode to a point
/v1/places.json answers towns, villages, postcodes and stops.
type=postcode keeps it to postcodes. Full postcodes come from
Ordnance Survey's Code-Point Open, and a partial one like CB2
lists the postcodes it starts. Leave type off to let people type
a town name instead.
2. The nearest stops
/v1/bus/stops.json with lat, lon and a
radius in metres returns stops closest first. Each has
towards, the next significant place its buses head for. Show it
beside the name: most stops come in pairs on opposite sides of the road, and
towards is how a passenger tells them apart.
3. The departure board
/v1/bus/stop_timetables/{atcocode}.json lists departures in a
window you set with from_offset and to_offset (minutes
from now). Every time is ISO 8601 in UTC, which is why the example converts to
Europe/London. Printing the raw string would be an hour out
all summer.
Where TransCAPI tracks a bus against its timetable, the departure carries
expected_departure_time and delay_seconds. Where it
doesn't, you get the timetabled time and source: "gtfs_scheduled",
so you can label the two differently. Tracked delays currently cover northern
England and West Sussex. Elsewhere, add live=true to get the
position of the bus running the trip where the operator reports it.
Taking it further
type=arrivalslists what's arriving, including buses that terminate at the stop.- Each departure's
trip_idopens the whole journey at/v1/bus/services/{trip_id}.json: every stop, and the live bus if it's running. /v1/bus/vehicles.json?lat=&lon=&radius=gives every bus reporting its position nearby, for a map./v1/disruptions.json?atcocode=tells you about roadworks and diversions affecting the stop.
All the parameters are in the bus docs.
Try it. A free key gives you 1,000 requests a day with no card. Every endpoint covers Great Britain: see coverage for the parts that are regional.