TfL Live Arrivals: London Tube, Bus & Rail Times, Line Status avatar

TfL Live Arrivals: London Tube, Bus & Rail Times, Line Status

Pricing

from $1.40 / 1,000 row returneds

Go to Apify Store
TfL Live Arrivals: London Tube, Bus & Rail Times, Line Status

TfL Live Arrivals: London Tube, Bus & Rail Times, Line Status

Live arrival predictions for any London bus stop, Tube, DLR, Overground, Elizabeth line, tram or river pier from Transport for London's open API, plus current line status with disruption reasons. Find stops by name or coordinates; every row carries the expected arrival in UTC and minutes to go.

Pricing

from $1.40 / 1,000 row returneds

Rating

0.0

(0)

Developer

Samat Makatov

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

14 hours ago

Last modified

Share

Live arrival predictions for any London bus stop, Tube, DLR, Overground, Elizabeth line, tram or river pier from Transport for London's open API, plus current line status with disruption reasons. Find stops by name or coordinates; every row carries the expected arrival in UTC and minutes to go.

Ask one of three questions and get rows back: when does the next vehicle reach this stop, is this line running normally, and which stop ids are near this place or name. The data is Transport for London's own live feed — the same predictions the countdown signs use — so a row is a forecast made seconds ago, not a timetable. No account, no API key, no proxy and no browser: the actor makes a handful of plain JSON requests to api.tfl.gov.uk.

Use cases

  • A stop display in a window or hallway — an e-ink panel or kiosk that shows the next two buses per route at your stop. Give the stop id, set perLineLimit to 2 and withinMinutes to 20, and schedule the run every minute or two.
  • "When is my next train?" in a chat or voice agent — the agent gets a station name from the user, passes it as stopQuery, and reads back line, destination, platform and minutes to go.
  • A commuter alert when a line stops running normally — lineStatus with onlyDisrupted returns nothing while everything is fine and one row per problem when it is not; with onlyStatusChanges the run emits only what changed since last time, so a schedule can post straight to chat.
  • Route-level monitoring and bunching analysis — one request with lineIds: ["12"] returns every live prediction along a whole bus route, so you can see where the vehicles are and how they are spaced.
  • Resolving NaPTAN stop ids for another pipeline — stops mode turns "somewhere near this postcode" or "Trafalgar Square" into ids, fare zones, stop letters and the routes that call there.
  • Station screens for an office or venue — a radius search around your address gives the stops people actually walk to, and the arrivals mode then reads all of them in one run.

Input

Every field is optional; the prefilled stop ids alone are a working run. Examples in the editor are prefills, not defaults — filters only ever narrow the result, they never widen it.

FieldTypeDefaultAllowed values / notes
modestringarrivalsarrivals, lineStatus, stops — see Modes
stopIdsstring[]— (prefilled ["940GZZLUOXC","490000173RC"])NaPTAN ids, see Stop ids. Combine freely with stopQuery and a coordinate; results are merged and de-duplicated
stopQuerystring—Stop or station name, e.g. Oxford Circus, Canary Wharf. The best maxStops matches are used
latitudenumber—Centre of a radius search in decimal degrees, e.g. 51.5074. Needs longitude
longitudenumber—e.g. -0.1278. Only used together with latitude
radiusMetersinteger40050–2000. Radius of the coordinate search
maxStopsinteger31–25. How many stops a name or radius search may expand to before arrivals are fetched. Ids you list yourself are always all used; in stops mode only maxItems limits the output
stopTypesstring[]on-street stops + stationsRadius search only, see Stop types
transportModesstring[]every mode the stop or feed servesSee Transport modes
lineIdsstring[]—Rail line ids and bus route numbers, see Line ids
directionFilterstringanyany, inbound, outbound — TfL labels a vehicle inbound towards central London
destinationContainsstring—Keep arrivals whose destination or towards text contains this, case-insensitive
withinMinutesinteger601–180. Drop predictions further away than this
perLineLimitinteger00–20. Keep the N soonest arrivals per line, direction and platform at each stop. 0 = all
onlyDisruptedbooleanfalselineStatus: drop lines in Good Service
onlyStatusChangesbooleanfalselineStatus: output a line only when its status differs from the previous run
sortOrderstringsoonestsoonest, latest for arrivals; severity for line status (worst first). Stop rows are always ordered by distance, then name
maxItemsinteger501–1000. The run stops writing at this many rows
fieldsstring[]allKeep only these output fields, in this order
appKeystring (secret)—Optional. The feeds answer without any key and the actor ships none; paste your own TfL app key to use your own allowance

Reference

Modes

modeWhat a row isWhat it reads
arrivalsOne predicted arrival at a stop (rowType: arrival), or one noArrivals row per stop or line that has nothing running/StopPoint/{id}/Arrivals per stop, or /Line/{ids}/Arrivals for whole routes
lineStatusThe current service state of one line (rowType: lineStatus)/Line/{ids}/Status or /Line/Mode/{modes}/Status
stopsOne stop with its id, position, routes and fare zone (rowType: stop)/StopPoint/Search/{name}, /StopPoint?lat=&lon= or /StopPoint/{ids}

In arrivals mode there are four ways to say where to look, and you can mix them:

  1. stopIds — exact NaPTAN ids, all of them are read.
  2. stopQuery — a name; the actor searches TfL's stop index and takes the best maxStops matches.
  3. latitude + longitude (+ radiusMeters) — every stop in the circle, nearest first, up to maxStops.
  4. lineIds with no stop at all — the whole route: every stop its vehicles are currently approaching.

A big interchange (Canary Wharf, King's Cross St. Pancras) is one stop in TfL's index but holds separate Tube, DLR and rail parts, and only the parts carry predictions. The actor notices that, counts the interchange as one stop against maxStops and reads up to six of its parts, so a station name gives you every line that calls there.

Stop ids (NaPTAN)

Ids are the stable identifiers of the national stop register. The prefix tells you what you are looking at:

PrefixExampleWhat it is
940G940GZZLUOXCTube, DLR and tram stations
910G910GCANWHRFNational Rail and Elizabeth line stations
490490000173RCOn-street bus stops (the letters at the end are the stop letter)
490G490G000917A group of bus stops at one place
930G930GCAWRiver piers
HUBHUBCAWAn interchange that groups the stops above

You rarely need to know an id by heart: run stops mode once with a name or a coordinate, copy the ids you want, and reuse them. A stop id that TfL does not recognise ends the run with a message naming it — the actor never quietly returns a different stop.

Transport modes

ValueTfL calls it
busBus
tubeTube (London Underground)
dlrDLR
overgroundLondon Overground — the six named lines Lioness, Mildmay, Windrush, Weaver, Suffragette, Liberty
elizabeth-lineElizabeth line
tramTram
river-busRiver bus and the Woolwich Ferry
cable-carLondon Cable Car
national-railNational Rail, as far as TfL's feeds cover it
coachCoach
replacement-busRail replacement bus

Line ids

Rail-type lines have a word for an id; bus and rail-replacement routes use the number printed on the vehicle (12, 88, n91, sl8). The full word list:

ModeLine ids
Tubebakerloo, central, circle, district, hammersmith-city, jubilee, metropolitan, northern, piccadilly, victoria, waterloo-city
Elizabeth lineelizabeth
DLR / Tramdlr, tram
Overgroundlioness, mildmay, windrush, weaver, suffragette, liberty
River / cable carrb1, rb4, rb6, woolwich-ferry, london-cable-car

The printed name works too (Elizabeth line, Hammersmith & City), and an obvious typo in a word id is corrected and reported in the log and the run summary (victora → victoria). Anything that is neither a known line nor a route number ends the run with the list above instead of a silently wider result.

Stop types

Used by the radius search only. Leaving the field empty means on-street stops plus the three station types.

ValueWhat it is
NaptanPublicBusCoachTramOn-street bus, coach or tram stop
NaptanMetroStationTube, DLR, Overground or Elizabeth line station
NaptanRailStationNational Rail station
NaptanBusCoachStationBus or coach station
NaptanFerryPortRiver pier
NaptanLiftCableCarStopCable car stop
TransportInterchangeAn interchange grouping several stops
NaptanOnstreetBusCoachStopPairA pair of on-street stops facing each other

Status severity

statusSeverity is TfL's own code and statusDescription its text. The numbers are not a single scale: 1–9 run from closed to minor delays, 10 means the line is fine, and 11–20 are other notable states. Use isGoodService (true only for Good Service, No Exceptional Delays and No Issues) when you need a yes/no answer, and sortOrder: "severity" to put the worst line on top and the healthy ones last.

CodeText you will see
0Special Service
1Closed
2 / 3Suspended / Part Suspended
4 / 5Planned Closure / Part Closure
6Severe Delays
7 / 8Reduced Service / Bus Service
9Minor Delays
10Good Service
11–20Part Closed, Exit Only, Change of frequency, Diverted, Not Running, Issues Reported, Information, Service Closed

Examples

Next Tube trains at a station, two per platform

{
"mode": "arrivals",
"stopIds": ["940GZZLUOXC"],
"transportModes": ["tube"],
"perLineLimit": 2,
"withinMinutes": 30,
"sortOrder": "soonest",
"maxItems": 20
}

A departure board for one bus stop

{
"mode": "arrivals",
"stopIds": ["490000173RC"],
"transportModes": ["bus"],
"perLineLimit": 2,
"withinMinutes": 30,
"maxItems": 20
}

Is my Tube line delayed right now (worst first)

{
"mode": "lineStatus",
"transportModes": ["tube"],
"sortOrder": "severity",
"maxItems": 20
}

Elizabeth line, Overground, DLR and Tram — only when something is wrong

{
"mode": "lineStatus",
"transportModes": ["elizabeth-line", "overground", "dlr", "tram"],
"onlyDisrupted": true,
"maxItems": 20
}

Find the bus stops within 400 m of a coordinate

{
"mode": "stops",
"latitude": 51.5074,
"longitude": -0.1278,
"radiusMeters": 400,
"stopTypes": ["NaptanPublicBusCoachTram"],
"maxItems": 25
}

Every live bus on one route

{
"mode": "arrivals",
"lineIds": ["12"],
"withinMinutes": 30,
"maxItems": 40
}

Arrivals by station name, for an agent that only has words

{
"mode": "arrivals",
"stopQuery": "Canary Wharf",
"transportModes": ["tube", "dlr", "elizabeth-line"],
"maxStops": 2,
"perLineLimit": 3,
"maxItems": 25
}

Output

One row per predicted arrival, line status or stop. rowType says which shape you are holding, so mixed runs and the overview view stay readable. Every timestamp is ISO 8601 in UTC (…Z) — London is one or two hours ahead of it, depending on the season. A real row from a cloud run:

{
"rowType": "arrival",
"stopId": "940GZZLUOXC",
"stopName": "Oxford Circus Underground Station",
"stopIndicator": null,
"stopLat": 51.515224,
"stopLon": -0.141903,
"transportMode": "tube",
"lineId": "bakerloo",
"lineName": "Bakerloo",
"destinationName": "Queen's Park Underground Station",
"destinationStopId": "940GZZLUQPS",
"towards": "Queen's Park",
"direction": "outbound",
"platformName": "Northbound - Platform 4",
"currentLocation": "At Piccadilly Circus Platform 1",
"vehicleId": "205",
"expectedArrival": "2026-09-25T21:28:11.000Z",
"minutesToArrival": 1,
"secondsToArrival": 67,
"predictionTimestamp": "2026-09-25T21:27:04.014Z",
"expiresAt": "2026-09-25T21:28:11.000Z",
"stopPageUrl": "https://tfl.gov.uk/tube/stop/940GZZLUOXC/oxford-circus-underground-station",
"url": "https://api.tfl.gov.uk/StopPoint/940GZZLUOXC/Arrivals",
"fetchedAt": "2026-09-25T21:27:15.841Z"
}

rowType: "arrival"

FieldTypeAlways filledMeaning
stopIdstringyesNaPTAN id of the stop the vehicle is approaching
stopNamestringyesStop name as TfL prints it
stopIndicatorstringno"Stop RC", "Platform 4" — the label on the pole or sign
stopLat / stopLonnumbernoPosition of the stop; filled whenever the stop was looked up (ids, name or radius search), null for whole-route runs
transportModestringyesbus, tube, dlr, …
lineId / lineNamestringyesvictoria / Victoria; for buses both are the route number
destinationNamestringnoWhere this vehicle terminates
destinationStopIdstringnoNaPTAN id of that terminus (rail only)
towardsstringnoThe direction text shown on the sign ("Marble Arch Or Great Portland Street")
directionstringnoinbound or outbound
platformNamestringno"Northbound - Platform 4"; for buses the stop letter
currentLocationstringnoWhere the vehicle is now ("Between North Greenwich and Canary Wharf") — rail modes only
vehicleIdstringnoTrain set or bus registration
expectedArrivalstringyesPredicted arrival, ISO 8601 UTC
minutesToArrivalintegeryesMinutes to go, from TfL's own countdown (0 = arriving)
secondsToArrivalintegeryesThe same countdown in seconds
predictionTimestampstringyesWhen TfL computed this prediction
expiresAtstringnoWhen the prediction stops being valid
stopPageUrlstringnoThe public TfL page of the stop
urlstringyesThe exact API request that produced the row
fetchedAtstringyesWhen this run read the feed

rowType: "noArrivals"

A stop or line that TfL answered for with an empty list still gets one row, so a closed station at 03:00 is a clear answer instead of an empty dataset: stopId, stopName, transportMode, lineId/lineName (for route runs), message, stopPageUrl, url, fetchedAt. These rows come after the real arrivals. A run that finds predictions but drops them all through your filters says so in the run status message and writes no noArrivals row — the stop is running, your filter was narrow.

rowType: "lineStatus"

FieldTypeAlways filledMeaning
lineId / lineNamestringyesvictoria / Victoria
transportModestringyesMode of the line
statusSeverityintegeryesTfL severity code, see Status severity
statusDescriptionstringyes"Good Service", "Severe Delays", "Part Closure"
isGoodServicebooleanyestrue only when the line is running normally
reasonstringnoTfL's explanation of the disruption
disruptionCategorystringno"RealTime", "PlannedWork" …
disruptionDescriptionstringnoThe longer disruption text
disruptionAdditionalInfostringnoExtra notes, e.g. tickets accepted on other modes
validFrom / validTostringnoValidity window of the status, ISO 8601 UTC
isNowValidbooleannoWhether that window covers this moment
statusChangedbooleannoFilled with onlyStatusChanges
previousStatusDescriptionstringnoWhat the previous run saw, with onlyStatusChanges
url, fetchedAtstringyesSource request and read time

A line can carry two statuses at once (a part closure plus good service on the rest); each one is its own row.

rowType: "stop"

FieldTypeAlways filledMeaning
stopIdstringyesNaPTAN id to feed into arrivals
stopNamestringyesStop name
stopTypestringyesSee Stop types
stopIndicator / stopLetterstringno"Stop X" / "X"
towardsstringnoDirection the stop serves
zonestringnoFare zone — filled for stations, usually empty for on-street stops
modesstring[]yesModes served
linesstring[]noIds of the routes that call there
stopLat / stopLonnumberyesPosition
distanceMetersnumberradius searchDistance from the coordinate you gave
parentStopIdstringnoThe station or group this stop belongs to
stopPageUrlstringnoThe public TfL page of the stop
url, fetchedAtstringyesSource request and read time

Dataset views: Live arrivals (mixed, works for every row type), Departure board, Line status, Stops. The run also writes a SUMMARY record to the key-value store with the stops it resolved, the requests it made, the filters applied and any corrected input.

Use it from code / agents

curl -X POST "https://api.apify.com/v2/acts/yadroo~tfl-live-arrivals/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"arrivals","stopIds":["490000173RC"],"perLineLimit":2,"withinMinutes":20,"maxItems":10}'
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/tfl-live-arrivals').call({
mode: 'arrivals',
stopQuery: 'Canary Wharf',
transportModes: ['tube', 'dlr', 'elizabeth-line'],
perLineLimit: 2,
maxItems: 12,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const i of items) console.log(`${i.minutesToArrival} min ${i.lineName} → ${i.destinationName}`);
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/tfl-live-arrivals").call(run_input={
"mode": "lineStatus",
"transportModes": ["tube"],
"onlyDisrupted": True,
"maxItems": 20,
})
for row in client.dataset(run["defaultDatasetId"]).list_items().items:
print(row["lineName"], row["statusDescription"], row["reason"])

MCP: add https://mcp.apify.com to Claude / Cursor / any MCP client and call the yadroo/tfl-live-arrivals tool with the same JSON input.

As an agent tool. The input is small enough to hand to a model directly: mode plus a stop id or a name, optionally perLineLimit and withinMinutes. Keep maxItems low (10–15) so the answer fits a reply, use fields to trim the row to what the user asked for — ["lineName", "destinationName", "minutesToArrival", "platformName"] is a complete answer to "when is my next train" — and let stopQuery do the name resolution instead of asking the user for a code.

Pricing

Pay per event: $0.001 per run start + $0.002 per dataset row. The start event is charged for every run, including a run that returns no rows, and it is the same at every subscription tier; row prices fall with the tier (BRONZE −10 %, SILVER −20 %, GOLD and above −30 %).

RunRowsCost at FREECost at GOLD
A platform board: one station, two trains per platform12$0.001 + 12 × $0.002 = $0.025$0.001 + 12 × $0.0014 = $0.0178
Tube line status (every line, worst first)11$0.001 + 11 × $0.002 = $0.023$0.001 + 11 × $0.0014 = $0.0164
The prefilled run: a Tube station and the bus stop outside itup to 50 (44 in our checks)$0.001 + 50 × $0.002 = $0.101 at most$0.001 + 50 × $0.0014 = $0.071 at most
An alerting run while the whole network is fine (onlyDisrupted)0$0.001$0.001

Two ways to keep a schedule cheap: set maxItems to what your screen shows, and use perLineLimit instead of a long list. Apify platform usage (compute) is charged separately by Apify and is tiny here — a run takes a few seconds at 256 MB with no browser.

Limits & FAQ

How far ahead do the predictions go? Rarely more than about 30 minutes, and on quiet routes much less. withinMinutes can only cut that window, not extend it; there is no timetable in this actor, only live forecasts.

Why did a stop return a noArrivals row? Because TfL had nothing running there at that moment — most often a rail station between about 00:30 and 05:30 London time, or a stop that is closed. The row names the stop so a display can say "no service" instead of going blank. Bus stops served by night routes answer around the clock.

Some fields are empty. currentLocation and platformName are filled for rail modes and mostly empty for buses; destinationStopId is rail-only; zone is published for stations, rarely for on-street stops; vehicleId is a train set number on rail and a registration plate on buses. The output tables above mark what is always filled.

National Rail coverage is partial. TfL's feeds carry the National Rail services they know about, which is not every train in London. For full rail data use a rail-specific source.

Rate limits and the app key. TfL asks for no more than 500 calls a minute per feed; this actor sends one request a second and a typical run makes 2 to 6 of them, so the anonymous allowance is enough. If you run very large or very frequent jobs, register your own app key with TfL and paste it into appKey — it is sent as the app_key parameter of each request and is never written to a row, a log line or the run summary. The actor ships no key of its own. If TfL does throttle you, lower maxStops, raise perLineLimit instead of reading more stops, or schedule less often.

Are there really no anti-bot blocks? No. These are open data feeds over plain HTTPS: no login, no captcha, no browser needed. Failed requests are retried politely with backoff and a run ends with a message naming what failed.

What happens to a bad input? An unknown stop id, an unknown line or a name nothing matches ends the run with a message that says what to change — never an empty success and never a quietly widened search. An obvious typo in a value from a closed dictionary (a mode, a rail line id, a sort order) is corrected, logged, and reported in SUMMARY.inputCorrections.

Can I watch for changes only? Yes: lineStatus with onlyStatusChanges remembers each line's status in the actor's own key-value store and returns a line only when it differs, with previousStatusDescription filled. The first run returns everything and remembers it. Separate schedules that watch different modes or lines keep separate state.

Time zone. Every date in the output is UTC with a trailing Z, exactly as the source publishes it. London clocks run UTC+1 in summer and UTC+0 in winter, so convert in your own display code.

Attribution (required by the licence). If you publish this data, keep these lines with it:

Powered by TfL Open Data

Contains OS data © Crown copyright and database rights 2016 and Geomni UK Map data © and database rights 2019

The terms are at https://tfl.gov.uk/corporate/terms-and-conditions/transport-data-service; they permit commercial and non-commercial use of the feeds this actor reads.


Made by Yadroo. Sibling actors: open-meteo-weather, public-holidays, osm-geocode, uk-planning-applications, rss-to-json.