Blog · 6 November 2026 · TransCAPI
Rail fares from the RDG feed: 7.5 million prices and the rules that pick one
Britain has no public API for train fares. What exists is a file: the rail industry's Fares, Routeing and Timetable data, a 44 MB zip that anyone with a free National Rail Data Portal account can download. Inside it are 7.5 million prices. None of them says "Bedford to St Pancras". Working out what a ticket between two stations costs means applying a set of lookup rules, and this is what I learned writing them for TransCAPI.
What's in the file
Fixed-width text records, in a format that dates back decades. This week's feed (sequence 938) loads into Postgres as:
| Records | |
|---|---|
| Flows (an origin, a destination, a route) | 693,346 |
| Fares (a price for a ticket type on a flow) | 7,459,813 |
| Non-derivable fare overrides | 224,928 |
| Station cluster memberships | 43,732 |
| Ticket types | 3,457 |
| Railcard-specific discount exceptions | 182,951 |
A flow is the unit everything hangs off. It joins two locations and carries a route code (any permitted route, "not via Doncaster", "Lumo only"). Fares are attached to flows, not to station pairs.
A station isn't a place you can price
Locations are identified by NLC codes, and a location can be a station, a group of stations, or a cluster. King's Cross is not priced as King's Cross. Its fare group is London Terminals, and most fares from it are flows from London Terminals. Manchester Piccadilly is priced as Manchester stations. Bedford, Luton and St Albans all sit in station groups that carry the walk-up fares to St Pancras, and the first version of my lookup returned nothing for them because it only looked at the station's own code. The fix was to query from every group a station belongs to, plus its own NLC.
Which flow wins
Several flows can match one journey, and the specification says only the most specific applies, per route:
- station to station
- station to the destination's cluster
- the origin's cluster to the station
- cluster to cluster
and within the same level, usage code A before G. Skip this step and a broad cluster fare shows up beside, or instead of, the specific one. 277,668 of the flows are reversible, so each also has to be tried backwards.
After that, overrides apply. They replace or suppress a flow fare for one origin, destination, route, ticket and railcard. They are a patch layer for cases the flows can't express, and there are 225,000 of them.
Railcard prices are computed, not stored
The feed doesn't hold a 16-25 Railcard price for each fare. It holds a discount, in thousandths (334 means 33.4%), keyed by railcard status and the ticket's discount category. You apply it to the adult fare yourself. The specification leaves rounding to the retailer; the industry convention is down to the nearest 5p, so that's what the API does. Railcard minimum fares, like the £12 minimum before 10:00 on weekdays for the 16-25 Railcard, depend on the time of travel, so they come back alongside the price rather than being applied to it.
Not everything in the file is a ticket you can buy
The feed includes staff travel, complimentary and corporate fares, sleeper berths, penalty fares, parking, tour-operator packages and contactless reference prices. 100,939 fare records are priced under 50p or over £999, which are placeholders. A fares API that shows "SCHOOL" or "STAFF" tickets is worse than one with gaps, so the loader filters these out by ticket description. It's a blunt list and it will occasionally hide something real.
What it returns
For Leeds to York today:
GET /v1/rail/fares.json?from=LDS&to=YRK
anytime day single £17.00
off-peak day single £15.10
advance single from £2.70
Sheffield to Leeds has a quirk worth knowing about. The cheapest Anytime Day Single (£14.60) is less than the cheapest Off-Peak Day Return (£15.20), and there's no Off-Peak single at all. That isn't a bug in the lookup. It's what the file says for that route, and it's why showing "the cheapest fare" without saying which ticket it is can mislead.
Journey results use the same data. Each rail leg is priced as a walk-up single, which is valid on any train, and consecutive rail legs are priced as one through fare.
What it doesn't do
- Advance fares have no availability. The feed lists the price tiers; it doesn't say which are left on a given train. "From £4.20" is a floor, not an offer.
- No off-peak timing. It doesn't yet judge whether a particular departure falls in the off-peak window. That needs the restriction time-of-day records.
- No child fares, multi-passenger tickets or split ticketing.
- I wrote the record layouts from the open-source dtd2mysql, because I couldn't download the specification PDF. If a layout is wrong, prices will look wrong, so spot-check against a retailer before relying on it for anything that matters.
Operating it
The loader builds a staging schema and swaps it in, so queries never see half a feed, and it only keeps records in force today. It runs every night even though fares change a few times a year, for two reasons: fares starting that day get picked up, and the portal closes accounts that go about 30 days without a download.
Try it. TransCAPI is one API for buses, trains and journey planning across Great Britain. A free key gives you 100 requests a day with no card; the journey docs cover the parameters.