TfL Live Arrivals: London Tube, Bus & Rail Times, Line Status
Pricing
from $1.40 / 1,000 row returneds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
14 hours ago
Last modified
Categories
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
perLineLimitto 2 andwithinMinutesto 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 —
lineStatuswithonlyDisruptedreturns nothing while everything is fine and one row per problem when it is not; withonlyStatusChangesthe 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 —
stopsmode 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.
| Field | Type | Default | Allowed values / notes |
|---|---|---|---|
mode | string | arrivals | arrivals, lineStatus, stops — see Modes |
stopIds | string[] | — (prefilled ["940GZZLUOXC","490000173RC"]) | NaPTAN ids, see Stop ids. Combine freely with stopQuery and a coordinate; results are merged and de-duplicated |
stopQuery | string | — | Stop or station name, e.g. Oxford Circus, Canary Wharf. The best maxStops matches are used |
latitude | number | — | Centre of a radius search in decimal degrees, e.g. 51.5074. Needs longitude |
longitude | number | — | e.g. -0.1278. Only used together with latitude |
radiusMeters | integer | 400 | 50–2000. Radius of the coordinate search |
maxStops | integer | 3 | 1–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 |
stopTypes | string[] | on-street stops + stations | Radius search only, see Stop types |
transportModes | string[] | every mode the stop or feed serves | See Transport modes |
lineIds | string[] | — | Rail line ids and bus route numbers, see Line ids |
directionFilter | string | any | any, inbound, outbound — TfL labels a vehicle inbound towards central London |
destinationContains | string | — | Keep arrivals whose destination or towards text contains this, case-insensitive |
withinMinutes | integer | 60 | 1–180. Drop predictions further away than this |
perLineLimit | integer | 0 | 0–20. Keep the N soonest arrivals per line, direction and platform at each stop. 0 = all |
onlyDisrupted | boolean | false | lineStatus: drop lines in Good Service |
onlyStatusChanges | boolean | false | lineStatus: output a line only when its status differs from the previous run |
sortOrder | string | soonest | soonest, latest for arrivals; severity for line status (worst first). Stop rows are always ordered by distance, then name |
maxItems | integer | 50 | 1–1000. The run stops writing at this many rows |
fields | string[] | all | Keep only these output fields, in this order |
appKey | string (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
mode | What a row is | What it reads |
|---|---|---|
arrivals | One 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 |
lineStatus | The current service state of one line (rowType: lineStatus) | /Line/{ids}/Status or /Line/Mode/{modes}/Status |
stops | One 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:
stopIds— exact NaPTAN ids, all of them are read.stopQuery— a name; the actor searches TfL's stop index and takes the bestmaxStopsmatches.latitude+longitude(+radiusMeters) — every stop in the circle, nearest first, up tomaxStops.lineIdswith 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:
| Prefix | Example | What it is |
|---|---|---|
940G | 940GZZLUOXC | Tube, DLR and tram stations |
910G | 910GCANWHRF | National Rail and Elizabeth line stations |
490 | 490000173RC | On-street bus stops (the letters at the end are the stop letter) |
490G | 490G000917 | A group of bus stops at one place |
930G | 930GCAW | River piers |
HUB | HUBCAW | An 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
| Value | TfL calls it |
|---|---|
bus | Bus |
tube | Tube (London Underground) |
dlr | DLR |
overground | London Overground — the six named lines Lioness, Mildmay, Windrush, Weaver, Suffragette, Liberty |
elizabeth-line | Elizabeth line |
tram | Tram |
river-bus | River bus and the Woolwich Ferry |
cable-car | London Cable Car |
national-rail | National Rail, as far as TfL's feeds cover it |
coach | Coach |
replacement-bus | Rail 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:
| Mode | Line ids |
|---|---|
| Tube | bakerloo, central, circle, district, hammersmith-city, jubilee, metropolitan, northern, piccadilly, victoria, waterloo-city |
| Elizabeth line | elizabeth |
| DLR / Tram | dlr, tram |
| Overground | lioness, mildmay, windrush, weaver, suffragette, liberty |
| River / cable car | rb1, 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.
| Value | What it is |
|---|---|
NaptanPublicBusCoachTram | On-street bus, coach or tram stop |
NaptanMetroStation | Tube, DLR, Overground or Elizabeth line station |
NaptanRailStation | National Rail station |
NaptanBusCoachStation | Bus or coach station |
NaptanFerryPort | River pier |
NaptanLiftCableCarStop | Cable car stop |
TransportInterchange | An interchange grouping several stops |
NaptanOnstreetBusCoachStopPair | A 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.
| Code | Text you will see |
|---|---|
| 0 | Special Service |
| 1 | Closed |
| 2 / 3 | Suspended / Part Suspended |
| 4 / 5 | Planned Closure / Part Closure |
| 6 | Severe Delays |
| 7 / 8 | Reduced Service / Bus Service |
| 9 | Minor Delays |
| 10 | Good Service |
| 11–20 | Part 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"
| Field | Type | Always filled | Meaning |
|---|---|---|---|
stopId | string | yes | NaPTAN id of the stop the vehicle is approaching |
stopName | string | yes | Stop name as TfL prints it |
stopIndicator | string | no | "Stop RC", "Platform 4" — the label on the pole or sign |
stopLat / stopLon | number | no | Position of the stop; filled whenever the stop was looked up (ids, name or radius search), null for whole-route runs |
transportMode | string | yes | bus, tube, dlr, … |
lineId / lineName | string | yes | victoria / Victoria; for buses both are the route number |
destinationName | string | no | Where this vehicle terminates |
destinationStopId | string | no | NaPTAN id of that terminus (rail only) |
towards | string | no | The direction text shown on the sign ("Marble Arch Or Great Portland Street") |
direction | string | no | inbound or outbound |
platformName | string | no | "Northbound - Platform 4"; for buses the stop letter |
currentLocation | string | no | Where the vehicle is now ("Between North Greenwich and Canary Wharf") — rail modes only |
vehicleId | string | no | Train set or bus registration |
expectedArrival | string | yes | Predicted arrival, ISO 8601 UTC |
minutesToArrival | integer | yes | Minutes to go, from TfL's own countdown (0 = arriving) |
secondsToArrival | integer | yes | The same countdown in seconds |
predictionTimestamp | string | yes | When TfL computed this prediction |
expiresAt | string | no | When the prediction stops being valid |
stopPageUrl | string | no | The public TfL page of the stop |
url | string | yes | The exact API request that produced the row |
fetchedAt | string | yes | When 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"
| Field | Type | Always filled | Meaning |
|---|---|---|---|
lineId / lineName | string | yes | victoria / Victoria |
transportMode | string | yes | Mode of the line |
statusSeverity | integer | yes | TfL severity code, see Status severity |
statusDescription | string | yes | "Good Service", "Severe Delays", "Part Closure" |
isGoodService | boolean | yes | true only when the line is running normally |
reason | string | no | TfL's explanation of the disruption |
disruptionCategory | string | no | "RealTime", "PlannedWork" … |
disruptionDescription | string | no | The longer disruption text |
disruptionAdditionalInfo | string | no | Extra notes, e.g. tickets accepted on other modes |
validFrom / validTo | string | no | Validity window of the status, ISO 8601 UTC |
isNowValid | boolean | no | Whether that window covers this moment |
statusChanged | boolean | no | Filled with onlyStatusChanges |
previousStatusDescription | string | no | What the previous run saw, with onlyStatusChanges |
url, fetchedAt | string | yes | Source 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"
| Field | Type | Always filled | Meaning |
|---|---|---|---|
stopId | string | yes | NaPTAN id to feed into arrivals |
stopName | string | yes | Stop name |
stopType | string | yes | See Stop types |
stopIndicator / stopLetter | string | no | "Stop X" / "X" |
towards | string | no | Direction the stop serves |
zone | string | no | Fare zone — filled for stations, usually empty for on-street stops |
modes | string[] | yes | Modes served |
lines | string[] | no | Ids of the routes that call there |
stopLat / stopLon | number | yes | Position |
distanceMeters | number | radius search | Distance from the coordinate you gave |
parentStopId | string | no | The station or group this stop belongs to |
stopPageUrl | string | no | The public TfL page of the stop |
url, fetchedAt | string | yes | Source 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 osfrom apify_client import ApifyClientclient = 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 %).
| Run | Rows | Cost at FREE | Cost at GOLD |
|---|---|---|---|
| A platform board: one station, two trains per platform | 12 | $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 it | up 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.