# TfL Live Arrivals: London Tube, Bus & Rail Times, Line Status (`yadroo/tfl-live-arrivals`) Actor

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.

- **URL**: https://apify.com/yadroo/tfl-live-arrivals.md
- **Developed by:** [Samat Makatov](https://apify.com/yadroo) (community)
- **Categories:** Travel, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 row returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 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.

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.

| Field | Type | Default | Allowed values / notes |
|---|---|---|---|
| `mode` | string | `arrivals` | `arrivals`, `lineStatus`, `stops` — see [Modes](#modes) |
| `stopIds` | string\[] | — (prefilled `["940GZZLUOXC","490000173RC"]`) | NaPTAN ids, see [Stop ids](#stop-ids-naptan). 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](#stop-types) |
| `transportModes` | string\[] | every mode the stop or feed serves | See [Transport modes](#transport-modes) |
| `lineIds` | string\[] | — | Rail line ids and bus route numbers, see [Line ids](#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:

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:

| 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**

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

**A departure board for one bus stop**

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

**Is my Tube line delayed right now (worst first)**

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

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

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

**Find the bus stops within 400 m of a coordinate**

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

**Every live bus on one route**

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

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

```json
{
  "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:

```json
{
  "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](#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](#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

```bash
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}'
```

```js
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}`);
```

```python
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 %).

| 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](https://apify.com/yadroo/open-meteo-weather), [public-holidays](https://apify.com/yadroo/public-holidays), [osm-geocode](https://apify.com/yadroo/osm-geocode), [uk-planning-applications](https://apify.com/yadroo/uk-planning-applications), [rss-to-json](https://apify.com/yadroo/rss-to-json).

# Actor input Schema

## `mode` (type: `string`):

`arrivals` returns one row per predicted vehicle arrival at the stops (or lines) you name. `lineStatus` returns one row per line with its current service state, severity and disruption text. `stops` resolves stop ids: search by name or list every stop within a radius of a coordinate — run it once to find the ids you then feed into `arrivals`.

## `stopIds` (type: `array`):

NaPTAN stop ids, e.g. `940GZZLUOXC` (Oxford Circus Underground Station) or `490000173RC` (the bus stop outside it). Tube, DLR, Overground and Elizabeth line stations start with `940G`/`910G`, bus stops with `490`. Use the `stops` mode or README → Reference to look ids up. In `arrivals` mode you may combine stop ids, a name search and a coordinate radius; the results are merged and de-duplicated.

## `stopQuery` (type: `string`):

Search stops by name, e.g. "Oxford Circus", "Trafalgar Square", "Canary Wharf". The best `Max stops from a search` matches are used. Combine with Transport modes to search only bus stops or only stations.

## `latitude` (type: `number`):

Centre of a radius search, decimal degrees, e.g. 51.5074 (Trafalgar Square). Set together with Longitude to take every stop within Radius. Handy for a stop display: give the coordinates of your window and get the stops you can actually see.

## `longitude` (type: `number`):

Centre of a radius search, decimal degrees, e.g. -0.1278. Only used together with Latitude.

## `radiusMeters` (type: `integer`):

Radius of the coordinate search. 400 m is about a five-minute walk; a central London radius of 400 m typically holds 10–15 bus stops.

## `maxStops` (type: `integer`):

How many resolved stops a name search or a radius search may expand to before arrivals are fetched. Each stop costs one extra request, so keep it small for scheduled runs. Stop ids you list yourself are always all used.

## `stopTypes` (type: `array`):

NaPTAN stop types the radius search may return. Empty = on-street stops and stations. Ignored when you pass stop ids or a name search.

## `transportModes` (type: `array`):

Keep only these modes. In `arrivals` it filters the predictions, in `lineStatus` it selects which lines are reported, in `stops` it narrows a name search. Empty = every mode the stop or feed serves.

## `lineIds` (type: `array`):

TfL line ids. Rail lines use words: `victoria`, `central`, `bakerloo`, `circle`, `district`, `hammersmith-city`, `jubilee`, `metropolitan`, `northern`, `piccadilly`, `waterloo-city`, `elizabeth`, `dlr`, `tram`, `lioness`, `mildmay`, `windrush`, `weaver`, `suffragette`, `liberty`, `london-cable-car`, `rb1`, `rb4`, `rb6`, `woolwich-ferry`. Bus routes use the route number as printed: `12`, `88`, `n91`. In `arrivals` mode without any stop, the whole line is followed (every stop it is approaching); with stops it filters those stops' predictions. In `lineStatus` mode it picks the lines to report. Full list in README → Reference.

## `directionFilter` (type: `string`):

Keep arrivals heading one way only. TfL labels a bus or train `inbound` towards central London and `outbound` away from it; a stop display usually needs one of the two.

## `destinationContains` (type: `string`):

Keep only arrivals whose destination or `towards` text contains this, case-insensitive, e.g. "Brixton", "Heathrow", "Marble Arch". Use it when one platform serves several branches.

## `withinMinutes` (type: `integer`):

Drop predictions further away than this. TfL rarely predicts more than 30 minutes ahead, so 60 keeps everything; set 10 for a short platform display.

## `perLineLimit` (type: `integer`):

Keep only the N soonest arrivals for each line and direction at each stop — the classic "next two trains per platform" board. 0 = keep every prediction.

## `onlyDisrupted` (type: `boolean`):

`lineStatus` mode: drop lines in Good Service and keep delays, part closures, suspensions and planned closures. A quiet network then returns no rows, which is the correct answer for an alerting task.

## `onlyStatusChanges` (type: `boolean`):

`lineStatus` mode: remember each line's severity in this actor's key-value store and output a line only when it differs from the previous run, with the previous description in `previousStatusDescription`. A scheduled task then emits exactly the changes. The first run outputs everything.

## `sortOrder` (type: `string`):

`soonest`/`latest` order arrivals by expected time; `severity` orders line-status rows from the most severe disruption to Good Service. Stop rows are always ordered by distance, then name.

## `maxItems` (type: `integer`):

Stop after this many rows. A busy Tube station returns 30–40 predictions, a whole bus route up to 100.

## `fields` (type: `array`):

Keep only these fields, in this order, e.g. \["lineName", "destinationName", "minutesToArrival"]. Empty = every field of the row type.

## `appKey` (type: `string`):

Optional. The API answers without any key and this actor uses no key by default. If you have your own TfL app key and want its higher request allowance, paste it here and it is sent as the `app_key` parameter.

## Actor input object example

```json
{
  "mode": "arrivals",
  "stopIds": [
    "940GZZLUOXC",
    "490000173RC"
  ],
  "radiusMeters": 400,
  "maxStops": 3,
  "directionFilter": "any",
  "withinMinutes": 60,
  "perLineLimit": 0,
  "onlyDisrupted": false,
  "onlyStatusChanges": false,
  "sortOrder": "soonest",
  "maxItems": 50
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "stopIds": [
        "940GZZLUOXC",
        "490000173RC"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("yadroo/tfl-live-arrivals").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "stopIds": [
        "940GZZLUOXC",
        "490000173RC",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("yadroo/tfl-live-arrivals").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "stopIds": [
    "940GZZLUOXC",
    "490000173RC"
  ]
}' |
apify call yadroo/tfl-live-arrivals --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yadroo/tfl-live-arrivals"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/RFW13lP7RGoIvfZCG/builds/cuWOmdxmRcIu5zQM7/openapi.json
