gb‑transit

Feeds

Using this data

The decisions this feed makes, and the behaviour they produce. Not an introduction to GTFS.

The feed is built from the CIF timetable the Rail Delivery Group publishes through its DTD portal. CIF describes the railway in terms GTFS has no field for, so the build has to decide how to express them. This page lists those decisions and the behaviour they produce.

What you get

Three feeds, from the same build:

feedwhat it holds
gtfs.zipwhere a service calls
gtfs-passing-points.zipwhere a service calls, and where it runs through without stopping
gtfs-national-rail-only.zipwhere a service calls, without the services National Rail does not run

A journey planner wants the first. Anything tracing where a train actually goes — what uses which line, where a delay was picked up, why two services conflict — wants the second, which holds the same trips. See the passing points feed. Drawing a train on a map needs neither: both feeds carry the same shapes.

A feed being merged with the tube and bus timetables wants the third, which has fewer trips than the other two. See the National Rail only feed.

Every link resolves to the most recent release, so they can be bookmarked or scripted against.

Covers
11/09/2026 → 11/12/2026
Trips
293,830
Built from
RJTTC955.ZIP
Built
11 Sept 2026, 09:47
Stop times, gtfs.zip
3,012,565
Stop times, gtfs-passing-points.zip
3,656,317
Stop times, gtfs-national-rail-only.zip
2,939,549
Trips, gtfs-national-rail-only.zip
279,779

The release carries four JSON files as separate assets rather than inside the zips, because they describe the feed rather than being part of it and somebody unzipping a GTFS feed should get GTFS:

asset what it is
validation.json the validator’s report, including every accepted error. One per feed, the others suffixed as their zip is
feed-meta.json what was built, from what, and when
enrichment-report.json what each external source matched and missed
provenance.json every field an external source wrote, and every write that lost

A feed is published nightly. A build that fails validation is not released, so a missing night means the build was stopped rather than that a degraded feed was published. Releases are pruned to the last thirty days plus the earliest release of each month.

Reading the feed

The five things most likely to break a consumer written against the specification alone:

  • No identifier in the feed is a three-letter station code. stop_id is a NaPTAN ATCO code and agency_id a National Operator Code. The CRS is in stop_code. See identifiers.
  • stop_times.txt references boarding points, never stations. Interchange and fixed-link rows in transfers.txt reference stations, and couplings reference boarding points. See stops, stations and platforms.
  • Times run past 24:00:00, and a service departing before 02:00 belongs to the previous service day. See calendars and time.
  • stop_headsign is the only quoted field in the feed, and it contains commas on trains that divide. A reader that splits each line on every comma will misparse those rows.
  • service_id is a plain integer, assigned from the calendar it describes. It is stable between two builds of the same data and not between builds on different days. Join on it within one feed; do not store it.

Identifiers

Every identifier below is derived from the data rather than assigned in the order rows happen to arrive, so it does not move between builds. service_id is the exception, as above.

Stops take NaPTAN’s identifiers, so the feed can be merged with the Department for Transport’s bus and metro data without a translation table.

station platform call naming no platform
stop_id 910GCLPHMJC 9100CLPHMJC15 9100CLPHMJC
stop_code CLJ CLJ CLJ
stop_name Clapham Junction Clapham Junction Platform 15 Clapham Junction
location_type 1 0 0
parent_station 910GCLPHMJC 910GCLPHMJC

A boarding point takes the TIPLOC of the timing point rather than the station’s, which is how Clapham Junction’s West London and Main Line platforms come out as 9100CLPHMJW3 and 9100CLPHMJM11 under one station.

agency_id is the National Operator Code form of the ATOC code: =SN, =AW, =GW. The equals sign is how the catalogue distinguishes a rail operator from an airline holding the same two letters.

route_id is the ATOC code, or a brand where an operator runs more than one — LN and WM for West Midlands Trains, WEA and LIB for Overground lines, BAK and MET for the Underground. Which brand a service belongs to is recovered from where it calls. A scheduled bus is suffixed _BUS and a rail replacement bus _RRB, so AW and AW_RRB are one operator in two modes.

An operator the software does not know about keeps a route of its own under its ATOC code rather than being collapsed into a catch-all, so its route id does not move when the operator list catches up.

trip_id is the train UID, the date the schedule starts and the date it ends: C00049_20260517_20261206. The short term plan indicator is deliberately not part of it, so a schedule withdrawn and reissued reads as an amended timetable rather than one trip disappearing and another appearing. Where two schedules would produce the same id, the second gets a _2 suffix.

trip_short_name is the retail service id, which is what a ticketing system will recognise.

Stops, stations and platforms

The hierarchy is exactly one level: stations with location_type=1, boarding points beneath them.

Every call gets a boarding point, whether or not it names a platform, because a stop time may not reference a station. A station where some calls name a platform and some do not therefore has a platform-less child as well, which is what NaPTAN calls the station’s access node.

Four things are worth knowing before joining anything to stops.txt:

  • stop_desc is not a description. It holds the CATE interchange status as a bare number: 0 is not an interchange, 1 to 3 its significance, 9 a subsidiary location.
  • Running lines are not platforms. DF, UM, DPL and UGL are designations for a line rather than somewhere a passenger can stand, so a call naming one gets the station’s boarding point.
  • A station with no coordinate is either absent or at 0,0. One that nothing references is not published. One that something references is published at 0,0 and named in a warning. It is not given a plausible centroid, because a validator will flag 0,0 and a centroid would pass as a real position.
  • Coordinates and station names come from NaPTAN, which supersedes the upper case, sixteen character names the timetable carries. Boarding points are created before enrichment runs, so a platform keeps the pre-enrichment name and coordinate while its station gets NaPTAN’s. Read a station’s geometry from the station.

A station nothing calls at within the window is still published, as a childless location_type=1 row, so the station list does not change with the length of the build window.

Two kinds of location are removed rather than published. Operator placeholders — CH ORIGIN, XC DESTINATION — exist so a schedule has somewhere to start when the real terminus is unknown, and are not places. Calls at locations the timetable never declares are dropped and stop_sequence renumbered, so the sequence is contiguous.

Routes, trips and headsigns

route_type is 714, rail replacement bus, for replacement services. It is an extended route type rather than one of the original seven, and a strict consumer will not recognise it.

route_desc is empty throughout. Train class and whether a seat can be reserved are properties of a train rather than of a line, so neither belongs on a route.

direction_id, wheelchair_accessible and bikes_allowed are 0 on every trip. In GTFS that means no information, not no; the timetable does not carry these. timepoint is 1 on every stop time, because CIF gives scheduled times rather than estimates, and shape_dist_traveled is empty because the feed has no shapes.

trip_headsign is the name of the station the trip terminates at. Where a portion joins another train it is the destination the passenger reaches, not the station where the coupling happens: a Carstairs portion of a Euston service says London Euston.

stop_headsign is empty except on a train that divides, where it names every destination the train is carrying, at the calls before the divide — Caterham and Tattenham Corner as far as Purley, and nothing from Purley on, where the trip headsign is right by itself. A train that divides twice names all three. This is the only field in the feed that contains a comma and is therefore quoted.

Pickup and drop off come from the activity codes on each call:

activity what it produces
T, TB, U pickup_type 0, scheduled
T, TF, D drop_off_type 0, scheduled
R, request stop 3 on both, coordinate with the driver
N, not advertised 1 on both, and it takes precedence over R
none of these 1, an operational stop where the train stops and nobody boards

The two directions are decided independently, so a call can be pick up only or set down only.

Known and unresolved. Only buses take a route id of their own by mode, so an operator running trains and a ferry publishes both on one route, and the route calls itself a train. A handful of operators are affected, each running a small number of ship schedules.

Splits and joins

A CIF association is two trains sharing a vehicle for part of their run. The feed does not fold one into the other. Both keep their own stops and their own trip, and the association is a row in transfers.txt:

from_stop_id,to_stop_id,from_trip_id,to_trip_id,transfer_type,min_transfer_time,...
9100SWANSEA4,9100SWANSEA4,G38297_20261018_20261018,G38968_20261018_20261018,4,,...

transfer_type=4 is an in-seat transfer: stay on board and you are on the other train. On a split the base is the from trip and the portion the to; on a join it is the other way round.

Four consequences:

  • The base is not cut at the coupling. A through journey stays one trip, so a consumer ignoring transfers.txt sees a through service rather than a change of trains. It costs a validator warning about a mid-trip in-seat transfer, which is accepted deliberately.
  • The coupling carries no calendar. The two trips’ own calendars say exactly when it applies: the days both run are the days they are coupled.
  • The two trips may be on different service days. A portion that leaves after midnight is published on the day its own record gives it, at its own times, so the Aberdeen portion of the sleeper is the 04:28 out of Edinburgh rather than a 28:28 the day before. GTFS does not require the two trips a transfer names to run on one service day.
  • A split cannot say which coaches to be in. The feed states that a passenger may stay on board for the portion, and that the base continues to its own destination. Which of the two a passenger reaches depends on which coaches they are in, and GTFS cannot express that, so a planner may offer both.

If your planner cannot follow a transfer across a service day, --duplicate-overnight-associations publishes the portion twice: once on its own day and once on the base’s, at times past 24:00. The published feed does not use it, because that puts the same train in the feed twice and a departure board built from it shows the train leaving twice.

Calendars and time

Every schedule gets a calendar.txt row for the days it runs and calendar_dates.txt rows for the days it does not. exception_type is always 2, a removal. The feed never adds a date to a service.

An overlay reads as absence. Where a short term plan schedule replaces part of a permanent one, the permanent trip simply has those dates excluded and the replacement is its own trip. GTFS has no way to say this trip replaces that one, so a consumer diffing two days will see one trip stop and another start, with nothing linking them.

feed_start_date and feed_end_date are the window the feed is complete for, not the range of its calendars. A schedule that began in 2021 and still runs carries its real start date, and a calendar routinely runs to 2099, so the earliest and latest dates in calendar.txt describe the source data rather than the feed. Read the window from feed_info.txt.

Service days run to 02:00. A service departing its origin before 02:00 is published on the previous service day, with every time shifted past 24:00 — a 00:35 departure is 24:35:00. Times above 24 hours appear on any overnight service, not only these.

Bank holiday running is in the source and is not modelled. A schedule that does not run on bank holidays is published as running on them.

Shapes

Every trip carries a shape_id, and shapes.txt is the line it runs over: a polyline through the stations the train touches, whether it calls there or runs through. Both feeds carry it, and they carry the identical file — the line is drawn from every timing point regardless of which of them the feed publishes as a call, which is what the nightly checks before releasing.

The passing points are the whole reason the line is worth drawing. Without them a King’s Cross to Newcastle service is a single 395 km straight line; with them it is twenty-odd hops that trace the East Coast Main Line. Across the feed they cut the number of straight-line hops longer than 50 km from 4,751 to 404.

It is not the track. 2,170,472 of the CIF’s 3,062,488 passing points are junctions, loops and signal boxes — WATRLWC, LEEDSWJ, HTRWAJN — and the DTD gives no coordinate for any of them. What is left is a station-to-station sketch: enough to tell a Bristol train from a Birmingham one on a map, not enough to draw the railway. The median hop is a 4.8 km straight line over ground the rails curve across.

Two consequences worth knowing:

  • A shape is shared. One line on the ground carries every stopping pattern that runs over it, so the fast and the stopper have the same shape_id — 13,722 shapes for 278,794 trips. The id is a digest of the stations the line runs through, so it is the same id in every build and can be referred to from outside the feed.
  • There is no shape_dist_traveled. GTFS only reads one where stop_times.txt carries it too, and putting a distance on all 2.9 million calls to disambiguate the 2.5% of trips that pass one station twice is not worth the largest file in the feed growing for it.

A trip whose every station the feed cannot place has an empty shape_id rather than a line drawn through Null Island.

The passing points feed

Half of CIF’s intermediate location records are places a service runs through without stopping. The standard feed drops them from stop_times.txt — though not from the shapes, which are drawn through them in both feeds. gtfs-passing-points.zip keeps them as calls, with pickup_type and drop_off_type of 1 and the pass time as both arrival and departure — the only time the record has.

trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_headsign,pickup_type,drop_off_type,...
C82958_20260517_20261206,05:00:00,05:00:00,9100VICTRIC18,1,,0,1,...
C82958_20260517_20261206,05:03:30,05:03:30,9100BATRSPK3,2,,1,1,...
C82958_20260517_20261206,05:06:00,05:06:00,9100CLPHMJC15,3,,1,1,...

A pass names its platform like any other call, and nearly all passes land on a boarding point the feed already publishes because something stops there — so a consumer promoting a passing point to a real stop has nothing to translate. A pass whose record names no platform gets the station’s boarding point.

Everything else is identical. The two feeds hold the same trips, routes, calendars and shapes, and the nightly stops rather than publishing if the two disagree about how many trips there are or draw different lines.

One caveat. A call publishes its public time and a passing point has only its working time, and the two do not agree. A small number of passes therefore read as later than the call after them. Correcting it would mean inventing a time for one of the two, so the feed reports the source as it is. validation.json names every occurrence, with the trip it is on.

The National Rail only feed

The CIF is the National Rail timetable and it carries services National Rail does not hold authority over. gtfs-national-rail-only.zip leaves them out:

  • everything with a mode of metro, bus or ship. Rail replacement buses stay: they are the railway’s own answer to its own timetable failing, and nothing else publishes them;
  • everything run by ES, LT, TW or ZZ — Eurostar, London Underground, the Tyne & Wear Metro and services with no operator — whatever mode it runs as. That is what takes out a London Underground replacement bus, which is a replacement bus but TfL’s to describe;
  • the replacement buses of LO and XR. The Overground and the Elizabeth line are National Rail and their trains are here, but their replacement buses are run by TfL, the way the Underground’s are.

Where those services matter, their own operators describe them better: TfL gives the NaPTAN stop a replacement bus calls at and the letter route code it runs under — R for the Willesden to Stratford service — neither of which the CIF has a field for.

Everything else is identical to gtfs.zip. A station left with no calls is still published — a tube-only station is still somewhere a ticket can be bought to — but its boarding points are not, because those are built from the calls.

The transfer patterns

transfer-patterns.br is not a feed. It is a companion to gtfs.zip: every sequence of stations a journey can be made of, found in advance, so a transfer pattern journey planner can plan a journey by pricing the handful of routes that exist between two places rather than searching the network for them.

Each line is one pattern — the stations it calls at, three characters each, nothing between them:

LSTNRW            Liverpool Street to Norwich, direct
LSTCBGNRW         changing at Cambridge
LSTCBGELYNRW      changing at Cambridge and Ely

The two ends are written in alphabetical order, so a pattern appears once for both directions of travel: a journey from Norwich to Liverpool Street is found under LST and read the other way. Sorting puts patterns beginning the same way together, so a line records only how many leading stations it takes from the line above and what follows it, and the whole thing is gzip compressed — which is what a browser can decompress, where brotli is not. It needs no marker for a pattern that a longer one runs through, because every line is exactly one pattern.

The station codes are the stop_code of gtfs.zip — CRS codes — so the two files have to be read together. stops.txt is what turns NRW back into Norwich and into somewhere with a platform and a position.

Patterns are found for one day at a time, and the published file holds a week of them together — the seven days beginning the day it was built. What changes between days is the shape of the service rather than the geography, so a week covers every shape the timetable has. transfer-patterns-meta.json on the same release names the days it actually holds.

It is built after the feed it describes is published, so a release that does not carry it is a night the scan did not finish rather than a feed with something wrong. transfer-pattern-planner both writes it and reads it back — PatternLoader indexes it against the feed’s stations.

transfers.txt

Three kinds of row share the file, distinguishable by which columns are populated:

from/to stop trip ids transfer_type min_transfer_time
interchange within a station the same station empty 2 seconds
a fixed link between stations two stations empty 2 seconds
a split or a join the same boarding point both set 4 empty

min_transfer_time is in seconds. A self-transfer — from_stop_id equal to to_stop_id — is the documented way to express the time it takes to change platforms, and there is one for every station the timetable gives an interchange time for.

Fixed links come from the feed’s own file of walking, tube, bus and ferry connections, which holds one record per time window and day pattern. GTFS allows one row per stop pair, so where several records describe a pair the row is their envelope: the shortest time, the earliest start to the latest end, the union of the days, and every mode. Most pairs have a single mode and are exact. A row saying METRO|WALK means the connection exists by some means during that window, not that both run throughout it.

Non-standard columns and extension files

The specification requires a consumer to ignore columns it does not recognise. Everything here is safe to drop.

where what why
transfers.txt mode, start_time, end_time, start_date, end_date and seven day flags GTFS has no way to express a conditional transfer, and a fixed link that only runs in the evening is a different thing from one that always does
attributions.txt attribution_licence the file has an organisation and a URL and no field for the terms, which is the one thing an attribution has to state
routes.txt route_type 714 an extended route type for a rail replacement bus
areas.txt, stop_areas.txt whole files, from GTFS Fares v2 see below

The dates in the transfers.txt extension columns are YYYY-MM-DD, not the YYYYMMDD GTFS uses elsewhere.

Areas publish the industry’s group stations: 1072, London Terminals, is Euston, Waterloo, King’s Cross and the rest of the London termini, and a rider holding a ticket to it needs to know which stations that is. GTFS has no station-of-stations — parent_station is one level and forbidden between stations, and a transfer would assert you can walk from Euston to Waterloo — so a flat set of areas is what the source actually says. area_id is the four-digit National Location Code, which is what the rest of the industry joins on. The source table mixes group stations with travelcard zones and bus groups, and all three are published, each under the description the source gives it.

There is also a links.txt, this project’s own format, which the build writes only when asked and which the published feed does not contain. It is the fixed links unsummarised, one row per window.

What the feed does not say

These are out of scope rather than pending:

  • No shapes.txt. Nothing in CIF describes track geometry, and the route between two calls cannot be derived from the timetable.
  • No fares. areas.txt is the group station membership and nothing else; there are no fare products, rules or transfer rules.
  • No real time. This is the planned timetable. Cancellations and alterations published after the refresh are not in it.
  • No pathways.txt and no station entrances. Without pathways an entrance is a node nothing can route through, so both wait on the other.
  • No bank holiday variations, as above.
  • Accessibility is station-level only. wheelchair_boarding comes from a hand-maintained table covering most stations, is not per platform, and is 0 — no information — where the table has no entry.

Validation, and reporting a problem

Every build is validated with a pinned version of MobilityData’s gtfs-validator before anything is released, and a feed with an unlisted error is not published.

A small number of errors are accepted, each named and capped in a baseline file with its reason. They are all the same case: the source states something that cannot be true, and the feed reports it rather than correcting it. A train that reaches its second station before it leaves its first is in the timetable operators and passengers are working from, and changing it here would hide the fault from the people who can correct it at source. validation.json on each release is the full report.

If the feed says something wrong about a train, it is either the source data, which the feed is reporting as it stands, or one of the decisions on this page. Both are worth raising: the second is a bug, and the first is worth a record so that the next person does not investigate it again.

Issues and source

Sources and licences

Rail Delivery Group
Rail Settlement Plan data licence
Department for Transport
Open Government Licence v3.0