Rail

Rail station and service data covers the whole of Great Britain (~2,650 stations, all national operators), with live running from National Rail's Darwin and every fare from the Rail Delivery Group. Stations are identified by their 3-letter CRS code (e.g. LDS for Leeds, PAD for London Paddington, EDB for Edinburgh).

Find stations

GET /v1/rail/stations.json?lat={lat}&lon={lon}&radius={metres}

Or by bounding box: min_lat/min_lon/max_lat/max_lon.

Departures

GET /v1/rail/station_timetables/{crs_code}.json

Returns departures (or arrivals, with type=arrivals or both) for the requested station within a time window (from_offset/to_offset minutes, default the next 60 minutes): operator, destination, scheduled time, and live running.

Live running comes from National Rail's Darwin, the railway's own real-time system, and is on by default (live=false turns it off). Each scheduled train is matched to Darwin's board for the station and gains an expected time, a platform, and a status: on_time, late, early, delayed (late, with no estimate yet), or cancelled, with delay_reason or cancel_reason where Darwin gives one. source is darwin for a matched train and gtfs_scheduled for one Darwin did not list, which is a few percent at a busy station. messages carries National Rail's station notices as plain text.

Darwin covers two hours either side of now. A board asked for further out is scheduled only, and live is false. live_error says why if Darwin could not be reached, and the board still comes back, scheduled.

{
  "crs_code": "LDS",
  "station": { "crs_code": "LDS", "name": "Leeds",
               "latitude": 53.7954, "longitude": -1.5476 },
  "live_requested": true,
  "live": true,
  "live_error": null,
  "messages": [
    "Trains running in the Exeter St Davids area may be delayed, cancelled or revised. ..."
  ],
  "departures": [
    { "mode": "rail", "line": "TP", "line_name": "TransPennine Express",
      "operator": "TransPennine Express", "trip_id": "T19113",
      "destination": "Newcastle",
      "aimed_departure_time":    "2026-09-28T16:52:00+00:00",
      "expected_departure_time": "2026-09-28T17:00:00+00:00",
      "best_departure_estimate": "2026-09-28T17:00:00+00:00",
      "status": "late", "platform": "15",
      "delay_reason": "This service has been delayed by trespassers on the railway earlier today",
      "cancel_reason": null, "source": "darwin" }
  ]
}

Example:

curl "https://api.transcapi.com/v1/rail/station_timetables/LDS.json?to_offset=120" \
  -H "X-Api-Key: YOUR_API_KEY"

One train

GET /v1/rail/services/{trip_id}.json

Every station one train calls at, with scheduled times, taking the trip_id from a station timetable. For a train running today, each call also carries live running from Darwin: status, expected_time, actual_time once the train has been there, and platform at its next station. The train itself gets a status (partly_cancelled when some calls are cancelled and others aren't), delay_reason and cancel_reason. A call Darwin doesn't list, such as the part of a journey before a train changes working, keeps status: null rather than a guess. live=false returns the timetable only.

{
  "trip_id": "T15865", "destination": "Skipton",
  "live": true, "status": "on_time",
  "calls": [
    { "crs_code": "LDS", "name": "Leeds",
      "aimed_departure_time": "2026-09-28T18:26:00+01:00",
      "status": "on_time", "expected_time": "2026-09-28T18:26:00+01:00",
      "actual_time": null, "platform": "4B" },
    { "crs_code": "KLF", "name": "Kirkstall Forge",
      "aimed_departure_time": "2026-09-28T18:33:00+01:00",
      "status": "on_time", "expected_time": "2026-09-28T18:32:00+01:00",
      "actual_time": null, "platform": null }
  ]
}

Fares

GET /v1/rail/fares.json?from={crs}&to={crs}

Fares for one adult between two stations, from the Rail Delivery Group's fares data, the same data every ticket retailer works from. You get Anytime, Off-Peak, Super Off-Peak and Advance singles and returns in standard and first class, plus operator products such as LNER's 70min Flex and Lumo Fixed. Each fare comes with:

  • route: the route it's valid on. "Any permitted route" is the usual one; others limit the route ("NOT VIA DONCASTR") or the operator ("LNER ONLY").
  • restriction: the time restriction, in National Rail's words, e.g. "VALID AFTER 0929 MON-FRI". It's null when the fare is valid on any train.
  • validity: how long the ticket is valid for.

Advance fares come as a ladder of prices, one per quota tier. Each ladder is returned as one fare, with price from its cheapest tier and price_to its dearest, and ticket_codes listing the tiers. The data has the prices, but not how many seats are left at each, so treat price as "from".

cheapest gives a quick answer: the cheapest standard class Anytime, Off-Peak and Advance fares.

ParameterDefault
from, torequiredStation CRS codes.
railcardA railcard code from /v1/rail/railcards.json, e.g. YNG for the 16-25 Railcard.
classallstandard, first or all.
include_seasonsfalseInclude season tickets.

With a railcard, price is the railcard price and adult_price_pence is the full fare. Tickets the railcard can't be used with are left out. Some railcards have a minimum fare that only applies at certain times, such as the 16-25 Railcard's £12 before 10:00 on weekdays. That minimum is given as railcard_minimum_fare, with the railcard's own note, rather than applied to the price.

Real output, Sheffield to Leeds with a 16-25 Railcard, 7 October 2026 (shortened):

{
  "from": { "crs": "SHF", "name": "Sheffield" },
  "to":   { "crs": "LDS", "name": "Leeds" },
  "railcard": { "code": "YNG", "name": "16-25 Railcard",
                "note": "MINIMUM FARES MAY APPLY BEFORE 1000 MONDAY-FRIDAY" },
  "cheapest": {
    "anytime_single":  { "name": "Anytime Day Single", "price": 9.7,
                         "route": "NOT VIA DONCASTR", "ticket_code": "SDS" },
    "off_peak_return": { "name": "Off-Peak Day Return", "price": 10.1,
                         "route": "NOT VIA DONCASTR", "ticket_code": "CDR" },
    "advance_single":  { "name": "Advance Single", "price": 1.65,
                         "route": "AP NORTHERN ONLY", "ticket_code": "DG0" },
    ...
  },
  "fares": [
    { "ticket_code": "CDR", "name": "Off-Peak Day Return",
      "category": "off_peak", "class": "standard", "type": "return",
      "price": 10.1, "price_pence": 1010, "adult_price_pence": 1520,
      "railcard_minimum_fare": 12.0,
      "route": { "code": "00098", "description": "NOT VIA DONCASTR" },
      "restriction": { "code": "B1", "description": "OFF-PEAK",
                       "outward": "VALID AFTER 0929 MON-FRI",
                       "return": "VALID AFTER 0929 MON-FRI" },
      "validity": "SEE RESTRICTNS", "reservation_required": false },
    ...
  ]
}

Example:

curl "https://api.transcapi.com/v1/rail/fares.json?from=SHF&to=LDS&railcard=YNG" \
  -H "X-Api-Key: YOUR_API_KEY"

GET /v1/rail/railcards.json lists the railcards: 16-25, 16-17 Saver, 26-30, Senior, Two Together, Family & Friends, Network, Disabled Persons, Veterans and HM Forces.

Railcard prices are a third off (half off for the 16-17 Saver), rounded down to the nearest 5p as retailers do. Child fares, group tickets, Rovers and Rangers aren't included. Fares are reloaded every night, so price changes show from the day they start. Journeys planned with /v1/journey.json carry rail fares too.

Check the changelog for updates as coverage expands.