API Docs
Live departure boards, service tracking, delays, cancellations and historical
lookback, built straight on Network Rail's TRUST, Darwin and CIF feeds. Every
endpoint below has a “Try it” panel that runs a real request against live
data, no key needed. To call these endpoints from your own code you need a key
from your API Keys page —
paste it below to run the panels against /api/ exactly as your code
will, and confirm the key works before you ship it.
Blank runs each panel against /data/, this site's own internal
path, which needs no credential. With a key they run against
/api/ — the supported, rate-limited surface, and the only
one to build against. /data/ is internal and unsupported.
Real-time departure/arrival board for a station. Merges CIF schedules with TRUST movements to show scheduled time, actual time, delay, platform, operator, destination, and status.
| Parameter | Type | Description |
|---|---|---|
| crs * | string | 3-letter CRS code (e.g. LDS, PAD, EUS). Alternatively use tiploc or stanox. |
| tiploc | string | TIPLOC code (e.g. LEEDS, PADTON) |
| stanox | string | STANOX code (e.g. 17132) |
| date opt | string | YYYY-MM-DD. Default: today. |
| hours opt | int | Time window in hours (1-24). Default: 2. |
| type opt | string | departures | arrivals | both | passes. Default: departures. Use passes for through-trains that don't stop. |
| full_day opt | bool | 1 to also include already-elapsed services from earlier today (window normally starts at "now", not midnight). Default: 0. Only affects requests for today — other dates always cover the full day. |
| time_from, time_to opt | string | Explicit HH:MM or HH:MM:SS window, e.g. time_from=14:00&time_to=18:00 — overrides hours/full_day entirely when both are given. Same-day only (time_to after time_from; no overnight wraparound). Supplying only one of the two is ignored and falls back to hours/full_day. |
Response fields
uid, headcode, toc_id, toc_name, toc_type, power_type, traction_class, traction_class_name (human-readable name for EMU/DMU only, e.g. "Class 800 Azuma/AT300", "Sprinter DMU" — null for loco-hauled freight where traction_class is tonnage), train_category, category_description, origin, origin_crs, destination, destination_crs, scheduled_departure, scheduled_arrival, scheduled_pass (WTT pass time — set for through-trains, null otherwise), public_departure, public_arrival (advertised times; null at points with no public call), platform (booked platform), actual_platform (last reported platform, may differ from booked), is_pass, is_q_path (boolean — true if Q-path/runs-as-required service), line, estimated_pass_time (estimated pass time for through-trains — the known gap between the adjacent timing points is split by real sectional running times either side of the point, from Network Rail's BPLAN data, falling back to a plain time midpoint where that data isn't available), activated (boolean — true if TRUST activation received, i.e. train is actually running), train_id, actual_departure, actual_arrival, delay_mins, variation_status, delay_monitoring_point (boolean — true if this location is an official delay-attribution point), auto_expected (boolean — true means TRUST calculated this timestamp from the train's last known position rather than observing it directly, because this location has no real-time detection; it's a forecast, not a confirmation, and can be later than the current time for a train that hasn't actually arrived yet), status (expected|active|cancelled|passed — already accounts for auto_expected: a forecast that hasn't elapsed yet won't show as passed), stale (boolean — true when this service is more than an hour past its own reference time here with no newer movement anywhere to explain it, i.e. the feed has stopped reporting this working rather than it still being en route; when set, delay_mins carries the overdue minutes, which is an inference from elapsed clock time rather than a confirmed position), origin_tiploc, destination_tiploc, origin_departure (this schedule's own origin departure time — exposed so callers can reproduce the ordering below), delay_last_tiploc/delay_last_tiploc_name (where a propagated delay figure was last actually observed, when the delay came from elsewhere on the route rather than from this station), formation (object or null — Darwin Push Port real-time formation data: total_coaches integer coach count, and coaches array of per-coach objects with number (e.g. "A1") and class (e.g. "Standard", "First")), consist (array or null — NWR Passenger Train Allocation and Consist real unit/coach data, a separate and more detailed source than formation above: each item has resource_group_id, resource_group_position, vehicle_count, and loco_vehicle_id (the real locomotive number for a loco-hauled formation, distinct from NWR's own internal diagram code) — no per-vehicle breakdown here, that's only fetched by service_live below)
Ordering
Services are returned in departure/arrival order, and the ordering is day-aware: each service's time is compared after bumping it by 24h whenever it falls numerically before that service's own origin_departure. Without that, a train that leaves at 15:38 and reaches this station at 00:30 would sort to the top of the board rather than the bottom. That's why origin_departure is included in every row — a client that re-sorts or merges these results (e.g. with passthrough_board) needs it to reproduce the same order. Services sharing an identical time are then ordered by uid, so the sequence is stable between identical requests.
The response envelope carries station, station_latitude, station_longitude, date, type, time_from, time_to, count and services.
Projected times
projected_arrival/projected_departure/projected_pass (UK-local HH:MM:SS, the same clock as the scheduled_* fields beside them), projected_delay_mins (the effective lateness there, after dwell absorption and the early-departure clamp) and projected_source.
Three sources, in strict precedence: a confirmed TRUST report, then Darwin’s own forecast, then RailFeed’s projection of the currently-observed lateness onto the booked timetable. projected_source names which produced each value: darwin (Darwin Push Port’s estimate for that exact point, matched to it by nearest booked time), interpolated (pinned between two confirmed reports, so it cannot drift) or projected (extrapolated ahead of the train, so it can). Darwin is passenger-only and forecasts only the points its own schedule names — so freight, charters run by freight operators, Network Rail test and departmental workings, and every compiled pass-through or junction fall through to the other two, permanently. Where RailFeed projects, booked dwell is absorbed at each intermediate call down to a 90-second floor, and a train is never projected to depart before its booked or advertised time. Values are null, never equal to the booked time, wherever nothing is known, so a punctual service carries none at all. A confirmed actual_* always takes precedence over all three.
projection_basis is darwin where the value is Darwin’s own forecast, and single_point where it is ours. A board row is one station rather than a journey, so when we project it ourselves we cannot see the dwell at the stops in between and carry the delay straight across — a single_point value therefore reads very slightly later than /api/service_live does for the same stop, which has the whole journey and can absorb that dwell. A darwin value carries no such bias. projection_suppressed names the reason when nothing was projected (on_time, stale_anchor, cancelled, off_route, reached_destination, past_date, no_anchor, implausible_carry).
Note: estimated_pass_time is now always the pure BPLAN interpolation. It previously had the propagated delay applied in place, which made a shifted value indistinguishable from a raw one; the shift lives in projected_pass instead.
Trains that pass through a station without stopping — i.e. never listed as a calling point in the schedule itself. Uses a pre-compiled table (built nightly, plus a real-track-geometry pass) rather than computing this from CIF schedules on the fly, since "does this train's route physically pass near this station" isn't something schedule_locations rows can answer directly. Live TRUST data is overlaid the same way as live_board (delay, cancellation, staleness). Response shape is live_board-compatible — every entry has is_pass=true — so the two can be merged into one board client-side (see timetables.php, which does exactly this).
| Parameter | Type | Description |
|---|---|---|
| crs * | string | 3-letter CRS code. Alternatively use tiploc. |
| tiploc | string | TIPLOC code. |
| date opt | string | YYYY-MM-DD. Default: today. |
| hours opt | int | Look-ahead window in hours (1-24). Default: 2. |
| full_day opt | bool | 1 to also include already-elapsed services from earlier today. Default: 0. Only affects requests for today — other dates always cover the full day. |
| time_from, time_to opt | string | Explicit HH:MM or HH:MM:SS window — see live_board, which this mirrors exactly (overrides hours/full_day when both given; same-day only). |
Response fields
Top level: station, date, is_terminal (currently always false — reserved), services[], count.
Per service: uid, headcode, toc_name, toc_type, power_type, traction_class, traction_class_name, origin, origin_tiploc, origin_departure (needed to reproduce the day-aware ordering — see live_board's Ordering note, which applies identically here and matters when merging the two boards), destination, destination_crs, destination_tiploc, is_pass (always true), scheduled_pass/estimated_pass_time (identical value — the known gap between this schedule's adjacent timing points, split by real BPLAN sectional running times either side, falling back to a plain midpoint), actual_departure (the confirmed timestamp once the train has actually gone past — despite the name, this is the pass event, not a stop), is_q_path, activated, delay_mins, delay_last_tiploc/delay_last_tiploc_name (where the propagated delay figure was last actually observed), status (expected|active|cancelled|passed), stale (boolean — see live_board's auto_expected/staleness notes, identical mechanism), platform/formation (always null — not meaningful for a non-stopping pass), consist (same shape as live_board's consist above — not null-forced like platform/formation, a passing train's real consist is still meaningful even though it doesn't stop).
Projected times
projected_arrival/projected_departure/projected_pass (UK-local HH:MM:SS, the same clock as the scheduled_* fields beside them), projected_delay_mins (the effective lateness there, after dwell absorption and the early-departure clamp) and projected_source.
Three sources, in strict precedence: a confirmed TRUST report, then Darwin’s own forecast, then RailFeed’s projection of the currently-observed lateness onto the booked timetable. projected_source names which produced each value: darwin (Darwin Push Port’s estimate for that exact point, matched to it by nearest booked time), interpolated (pinned between two confirmed reports, so it cannot drift) or projected (extrapolated ahead of the train, so it can). Darwin is passenger-only and forecasts only the points its own schedule names — so freight, charters run by freight operators, Network Rail test and departmental workings, and every compiled pass-through or junction fall through to the other two, permanently. Where RailFeed projects, booked dwell is absorbed at each intermediate call down to a 90-second floor, and a train is never projected to depart before its booked or advertised time. Values are null, never equal to the booked time, wherever nothing is known, so a punctual service carries none at all. A confirmed actual_* always takes precedence over all three.
projection_basis is darwin where the value is Darwin’s own forecast, and single_point where it is ours. A board row is one station rather than a journey, so when we project it ourselves we cannot see the dwell at the stops in between and carry the delay straight across — a single_point value therefore reads very slightly later than /api/service_live does for the same stop, which has the whole journey and can absorb that dwell. A darwin value carries no such bias. projection_suppressed names the reason when nothing was projected (on_time, stale_anchor, cancelled, off_route, reached_destination, past_date, no_anchor, implausible_carry).
Note: estimated_pass_time is now always the pure BPLAN interpolation. It previously had the propagated delay applied in place, which made a shifted value indistinguishable from a raw one; the shift lives in projected_pass instead.
Full calling points for a service with live TRUST movement data overlaid. Shows every stop (ORIGIN, CALL, PASS, TERMINATES) with scheduled vs actual times and per-stop delay.
| Parameter | Type | Description |
|---|---|---|
| uid * | string | Schedule UID (6 chars). Alternatively use train_id. |
| train_id | string | TRUST train_id (10 chars) — resolved to UID via activations table. |
| date opt | string | YYYY-MM-DD. Default: today. |
Response fields
Top level: train_id, date, status (cancelled|active|expected — whole-service status), stop_count, stp_cancelled (boolean — true when a CIF/VSTP STP-C override has withdrawn this working, which is distinct from a TRUST cancellation), schedule object, stops[] array, formation (object or null — total_coaches, plus coaches array of per-coach objects with number and class), consist (array or null — same NWR consist source as live_board's consist, but each unit here also carries a full vehicles[] array — vehicle_id, coach_letter, resource_position — since this is the one endpoint that fetches full per-vehicle detail), stale (boolean — see live_board's identical field above for the full staleness mechanism; only ever true when status is expected, i.e. never activated), associations array (joins/splits with other services — category, and the other schedule's headcode/origin/destination where resolvable), late_reason/late_reason_code (why the train is running late, from Darwin — see below).
late_reason_code is Darwin's numeric reason for the service running late, and late_reason its own passenger-facing wording for that code, already a complete sentence ("This train has been delayed by a landslip."). Both are null when Darwin has given no reason, which is not the same as the train being on time — roughly half of services running 10+ minutes late carry one, and Darwin covers passenger services only. Where a code arrives that isn't in the imported reason list, late_reason_code is set and late_reason is null: nothing is ever invented to fill the gap. Note this is an entirely separate code space from cancel_reason_code above, which is TRUST's Delay Attribution Guide code for a cancellation — the same number means unrelated things in the two, and they must never be looked up in each other's list. Available for the current service date only, since Darwin forecasts are not retained beyond it.
schedule object: uid, headcode, toc_id, toc_name, power_type, traction_class, traction_class_name, train_category, category_description, stp_indicator, origin, origin_crs, destination, destination_crs, movement_toc_name (the real-time TRUST feed's own operator name, resolved from its numeric Sector Code — a different code space from toc_id's CIF alpha code; only populated once the service has activated and reported at least one movement, most useful for a generic toc_id='ZZ' freight schedule where toc_name alone doesn't identify the actual operator).
stops[] array, one per calling point: tiploc, location_name, crs, latitude, longitude (for route mapping), location_type, activity (CIF activity codes), call_type (ORIGIN|CALL|PASS|TERMINATES), platform, actual_platform, line, scheduled_arrival/scheduled_departure/scheduled_pass (WTT working times) and public_arrival/public_departure (advertised times, typically 30–60s later than the working time; null at points with no public call), actual_arrival, actual_departure, actual_pass, delay_mins, arrival_delay_mins/departure_delay_mins (independent per-event delay figures — a stop's actual_arrival and actual_departure come from two separate TRUST movement reports, so these can differ from each other and from the combined delay_mins, e.g. arrived early but departed on time after a longer dwell; each is null wherever its corresponding actual_* value is null), variation_status, delay_monitoring_point, auto_expected (see live_board — same forecast-vs-confirmed meaning), status (expected|reported|passed — a still-future auto_expected report shows as expected, not reported/passed), inferred (boolean — true means this stop is not in the train's own CIF schedule at all. It comes from the compiled pass-through table: a location the route physically runs through, matched against real track geometry, with its time interpolated between the surrounding booked timing points. Always a PASS, never a call, and its scheduled_pass is an estimate rather than a booked time — see passthrough_board, which is built from the same source), route_from_prev (array of [lat, lon] pairs tracing the real track between this stop and the previous one, or null — precomputed from Network Rail's track centreline data; coverage isn't complete, treat null as "draw a straight line instead").
Projected times
projected_arrival/projected_departure/projected_pass (UK-local HH:MM:SS, the same clock as the scheduled_* fields beside them), projected_delay_mins (the effective lateness there, after dwell absorption and the early-departure clamp) and projected_source.
Three sources, in strict precedence: a confirmed TRUST report, then Darwin’s own forecast, then RailFeed’s projection of the currently-observed lateness onto the booked timetable. projected_source names which produced each value: darwin (Darwin Push Port’s estimate for that exact point, matched to it by nearest booked time), interpolated (pinned between two confirmed reports, so it cannot drift) or projected (extrapolated ahead of the train, so it can). Darwin is passenger-only and forecasts only the points its own schedule names — so freight, charters run by freight operators, Network Rail test and departmental workings, and every compiled pass-through or junction fall through to the other two, permanently. Where RailFeed projects, booked dwell is absorbed at each intermediate call down to a 90-second floor, and a train is never projected to depart before its booked or advertised time. Values are null, never equal to the booked time, wherever nothing is known, so a punctual service carries none at all. A confirmed actual_* always takes precedence over all three.
The service-level projection object reports applied, basis (full_walk here — unlike the boards, this endpoint has the whole journey and so can absorb dwell properly), carry_mins/carry_secs and where they were measured (anchor_tiploc, anchor_name, anchor_time), min_dwell_secs, interpolated_points, darwin_points (how many values came from Darwin rather than from us), and suppressed — which always names a reason when nothing was projected rather than leaving the caller to guess. Pass project=0 to switch the whole thing off.
Charter/special trains running on a given day. There's no explicit charter flag in CIF/VSTP — this endpoint identifies them by headcode LIKE '1Z%', but headcode alone isn't enough: mainstream franchised TOCs also reuse the 1Z block for ordinary late-notice additional/relief services (e.g. an extra Sunday working), which are single-date VSTP schedules just like a real charter. The extra signal is the operating TOC — schedules are only included if their TOC isn't a mainstream type='PASSENGER' franchise (so genuine charter/heritage operators pass, an ordinary train operating company's late addition doesn't). Every charter includes full real-track route geometry and BPLAN-weighted pass-time estimates by default; uid narrows the response to one specific charter instead of the whole day.
Set nr=1 to also include Network Rail's own infrastructure monitoring/test trains (headcode LIKE '1Q%') alongside charters. Unlike 1Z, this prefix doesn't need the TOC check — it's already unambiguous (every 1Q schedule is operated under toc_id='ZZ', never a real passenger franchise), so it's a plain, separate condition rather than reusing the charter heuristic. Each item's category field says which is which.
| Parameter | Type | Description |
|---|---|---|
| date opt | string | YYYY-MM-DD. Default: today. |
| uid opt | string | Schedule UID (6 chars). Switches to detail mode for that one charter. |
| nr opt | 1|0 | 1 to also include Network Rail test/monitoring trains. Default: 0 (charters only). |
Response fields
Top level: date, mode (list|detail), count, charters[]. Per charter: uid, headcode, category (charter|network_rail_test), toc_id, toc_name, toc_type, power_type, traction_class, traction_class_name, train_category, category_description, stp_indicator, origin, origin_crs, origin_departure, destination, destination_crs, destination_arrival, status (expected|active|cancelled — only meaningful when date is today, TRUST has no data for other dates), activated, train_id, stop_count, stops[].
stops[] array, one per calling point: tiploc, location_name, crs, latitude, longitude, location_type, activity (CIF activity codes), call_type (ORIGIN|CALL|PASS|TERMINATES), platform, line, scheduled_arrival/scheduled_departure/scheduled_pass (WTT working times) and public_arrival/public_departure (advertised times; null at points with no public call), actual_arrival, actual_departure, actual_pass, delay_mins, arrival_delay_mins/departure_delay_mins (independent per-event delay figures — see service_live's identical fields above), variation_status, actual_platform, status, route_from_prev (array of [lat, lon] pairs tracing real track from the previous stop, or null wherever coverage is missing — treat null as "draw a straight line instead"), estimated_pass_time (for PASS stops — BPLAN-weighted, more precise than the WTT booked time's own half-minute resolution). Both are included for every stop of every charter, not just in detail mode.
Projected times
projected_arrival/projected_departure/projected_pass (UK-local HH:MM:SS, the same clock as the scheduled_* fields beside them), projected_delay_mins (the effective lateness there, after dwell absorption and the early-departure clamp) and projected_source.
Three sources, in strict precedence: a confirmed TRUST report, then Darwin’s own forecast, then RailFeed’s projection of the currently-observed lateness onto the booked timetable. projected_source names which produced each value: darwin (Darwin Push Port’s estimate for that exact point, matched to it by nearest booked time), interpolated (pinned between two confirmed reports, so it cannot drift) or projected (extrapolated ahead of the train, so it can). Darwin is passenger-only and forecasts only the points its own schedule names — so freight, charters run by freight operators, Network Rail test and departmental workings, and every compiled pass-through or junction fall through to the other two, permanently. Where RailFeed projects, booked dwell is absorbed at each intermediate call down to a 90-second floor, and a train is never projected to depart before its booked or advertised time. Values are null, never equal to the booked time, wherever nothing is known, so a punctual service carries none at all. A confirmed actual_* always takes precedence over all three.
The service-level projection object reports applied, basis (full_walk here — unlike the boards, this endpoint has the whole journey and so can absorb dwell properly), carry_mins/carry_secs and where they were measured (anchor_tiploc, anchor_name, anchor_time), min_dwell_secs, interpolated_points, darwin_points (how many values came from Darwin rather than from us), and suppressed — which always names a reason when nothing was projected rather than leaving the caller to guess. Pass project=0 to switch the whole thing off.
Find all scheduled services between two stations on a given date. Returns departure/arrival times, journey duration, operator, and whether the train passes or calls.
| Parameter | Type | Description |
|---|---|---|
| from * | string | Origin station — CRS, TIPLOC, or STANOX. |
| to * | string | Destination station — CRS, TIPLOC, or STANOX. |
| date opt | string | YYYY-MM-DD. Default: today. |
Response fields
CIF-scheduled only (no live overlay) — direct services where one schedule calls at both stations, in order. Does not find journeys requiring a change of train; compose with live_board/departures at a common interchange for that. Per service: uid, headcode, toc_id, toc_name, toc_type, power_type, traction_class, train_category, category_description, origin, origin_crs, destination, destination_crs, from_departure/from_public_departure (the latter null if not publicly advertised)/from_platform/from_is_pass (at the from station), to_arrival/to_public_arrival/to_platform/to_is_pass (at the to station), journey_mins. Envelope: from, to (resolved station names), date, count, services[].
Services currently delayed at a station beyond a threshold. Deduplicates by train_id, keeping the worst delay per train.
| Parameter | Type | Description |
|---|---|---|
| crs * | string | CRS code. Alternatively use tiploc or stanox. |
| threshold opt | int | Minimum delay in minutes. Default: 5. |
| hours opt | int | Look-back window (1-720, 720 = 30 days). Default: 2. Results are capped at 2000 rows (worst delay first) before dedup, so a wide window at a busy station returns the trains that matter most, not everything. |
Response fields
Envelope: station, threshold, hours, count, delayed[]. Per row: train_id, schedule_uid, headcode, toc_id, toc_name, event_type (ARRIVAL|DEPARTURE|PASS), actual_timestamp, planned_timestamp, delay_mins (actual minus planned, so negative never appears here — only delays past the threshold are returned), variation_status, tiploc, location_name, crs, train_category, category_description, power_type, traction_class, traction_class_name, origin_tiploc/origin_name, destination_tiploc/destination_name.
The worst-delayed services across the whole network for one day, ranked. Distinct from /api/delays above, which answers “what is late at this station right now” — this one is network-wide, whole-day, and returns one row per service rather than one per movement report. Backs the Train Delays page.
Built only from TRUST movement reports, so every figure is an observed delay, never a forecast. Two things worth knowing about how a “day” is defined here, because both are easy to get wrong and neither matches a naive query: the window is a UK-local day (during BST, UTC midnight is 01:00 in Britain, so bounding on UTC would drop the first hour and pull in the previous evening — measured at 624 trains on one sample day), and a service belongs to the day it originated, not the day its reports landed, so a working that leaves at 23:50 and runs past midnight stays on its own day. That matches how Realtime Trains and other sources file services.
| Parameter | Type | Description |
|---|---|---|
| date opt | string | YYYY-MM-DD, UK-local. Default: today. Data only reaches back MOVEMENTS_RETENTION_DAYS (30). |
| type opt | string | passenger (default), freight, or all. Passenger excludes 5x and other non-public headcodes (empty stock, light engine); freight keys off the TOC's type rather than the schedule's own alpha TOC, which is the generic ZZ for most freight. |
| limit opt | int | 1-200. Default: 50. |
Response fields
Envelope: date, type, count, services[], ordered worst-first. Per service: headcode, train_id, schedule_uid, operator/toc_id/operator_type, origin/origin_tiploc and destination/destination_tiploc (the first and last points a report was actually received for, not necessarily the booked endpoints — a working that only starts reporting mid-route shows the observed leg), worst_delay_mins (the largest gap between actual and booked at any point) and final_delay_mins (how late at its last report), reports (how many movement reports fed the figures; services with fewer than 5 are excluded, since one stray report says nothing about how a journey ran), first_seen/last_seen (UTC), still_running (boolean — last report wasn't an arrival, came within the last two hours, and the service wasn't cancelled), cancelled with cancel_type (AT ORIGIN|EN ROUTE|ON CALL|OUT OF PLAN) and cancel_reason_code with cancel_reason (the Delay Attribution Guide wording, null for codes outside the imported revision) and cancel_responsible (cancellations arrive as TRUST 0002 messages, a different message type from the 0003 movement reports the delay figures are built from, so a service can have a clean run of reports and still have been cancelled after the last of them — its delay is then how late it got before it stopped, not a completed journey), and service_url (a ready-made link to that working's own page, or null where the movement rows never carried a schedule UID).
A whole day of movement reports is aggregated on request, so the first call for a given day takes a few seconds. Finished days are then cached indefinitely and return in milliseconds, flagged with cached: true and a generated_at timestamp. A day is only treated as finished once the clock passes about 06:00 the following morning — trains leaving just before midnight are still reporting into the small hours and their reports still belong to the earlier day, so caching at plain local midnight would freeze a half-finished answer. Today, and yesterday before that cutoff, are always computed live.
Cancelled services, optionally filtered by station. Extracts cancellation data from TRUST 0002 messages and cross-references with CIF schedules. Sorted by the cancelled service's own scheduled departure time (like a normal board), not by when the cancellation message was received — several unrelated trains cancelled around the same moment would otherwise cluster together regardless of how spread across the day their actual times are.
| Parameter | Type | Description |
|---|---|---|
| crs opt | string | Filter by station CRS. Omit for network-wide. Also accepts tiploc/stanox. |
| date opt | string | YYYY-MM-DD. Switches the endpoint to day mode: returns that whole UK-local day (midnight to midnight, correct across BST/GMT and the 23- and 25-hour DST transition days) and ignores hours. Day mode is not capped, since the day already bounds it — a busy day runs to about 4,000 nationally. |
| hours opt | int | Rolling look-back from now on when the cancellation was reported (1-720, 720 = 30 days). Default: 6. Applies only when date is omitted. Note: a cancellation can be issued hours ahead of the service's own scheduled time — this window bounds message recency, not how long the cancellation stays valid. A network-wide rolling query (no station filter) is capped at 3000 rows and sets truncated; a station-filtered one isn't capped. |
Response fields
Envelope: station, date, hours (null in day mode, so it can't be mistaken for the window actually applied), mode (day|rolling), truncated (boolean — true when a capped network-wide rolling pull hit its limit, so a short list is never silently mistaken for a complete one) and limit, count, cancellations[]. Per row: train_id, schedule_uid, headcode, toc_id, toc_name, toc_type (PASSENGER|FREIGHT — split these the way delay_league's ?type= does: passenger means toc_type=PASSENGER and a 1x/2x headcode, since 5x empty-stock and light-engine moves run under passenger operators; freight is toc_type=FREIGHT alone, because ~89% of freight schedules carry the generic ZZ CIF TOC), cancel_type (e.g. AT ORIGIN, EN ROUTE, OUT OF PLAN), reason_code / cancel_reason_code (the same 2-character Delay Attribution Guide code under both names, so a caller doesn't need to know which endpoint it's talking to), cancel_reason (the guide's own wording for that code, e.g. PD → “System generated cancellation” — null where the code isn't in the imported guide revision, about 0.7% of cancellations; nothing is ever invented to fill the gap) and cancel_responsible (the broad accountability bucket the code's first letter implies, e.g. “Network Rail infrastructure”), cancel_location (where the cancellation was reported, which is often not this station), cancelled_at, origin/origin_crs, destination/destination_crs, scheduled_departure, power_type, traction_class.
One UK-local day's cancellations aggregated by the location they were reported at — where the train was actually cancelled, not where the service was booked to start. The two differ: for EN ROUTE cancellations the origin is a different place entirely 99.8% of the time. Returns coordinates and a pre-normalised intensity per location, ready to plot. Backs the cancellation map.
Note the underscore in the endpoint name — the page is /cancellation-map but the endpoint is /api/cancellation_map.
| Parameter | Type | Description |
|---|---|---|
| date opt | string | YYYY-MM-DD, UK-local. Defaults to today. Finished days are served from an archive that outlives the ~30-day movement retention, so this reaches further back than the other movement endpoints. |
| mode opt | string | absolute (default) scales intensity by cancellation count, logarithmically — a handful of termini take a quarter of a typical day, so a linear scale would leave everywhere else invisible. anomaly scores each location against its own 14-day average instead, so a permanently busy station only stands out when it is having a worse day than usual. |
| type opt | string | passenger (default), freight or all, meaning exactly what they mean on /api/cancellations. Note passenger is stricter than "not freight": it requires a PASSENGER-type TOC and a 1x/2x headcode, so it excludes empty coaching stock (5x headcodes run by passenger operators), which was 1,252 of 3,875 cancellations on 2026-08-06. In anomaly mode the baseline is built from the same bucket, so a passenger day is never scored against an all-services average. |
Response fields
Envelope: date, mode, total (cancellations), locations, max_count, cached, earliest_date (oldest day in the archive), no_coords (locations dropped for want of coordinates), and in anomaly mode baseline_days/baseline_window.
points is an array of [lat, lon, intensity] triples, intensity already normalised to 0–1. top lists the ten busiest (or, in anomaly mode, the ten most unusual) with tiploc, name, crs, n, n_en_route, and in anomaly mode baseline and score.
In anomaly mode, locations at or below their own average are omitted entirely rather than returned at zero.
Raw TRUST movement events. Without date, this is the live/recent feed (most recent first). With date, it's a historical lookup for that UK-local calendar day (chronological order) — reaches back as far as the retention window (~30 days). Enriched with TOC names, schedule category, traction data, and location names.
| Parameter | Type | Description |
|---|---|---|
| date opt | string | YYYY-MM-DD. Omit for the live/recent feed. When given, returns that whole day's events chronologically instead. |
| limit opt | int | Max rows. Default: 100. Cap is 500 without date, 2000 with it (a day's worth of events can exceed 500 at a busy station). |
| tiploc | string | Filter by TIPLOC. |
| crs | string | Filter by CRS code. |
| stanox | string | Filter by STANOX code. |
| headcode | string | Filter by headcode (e.g. 1A05). |
| headcode_class | string | Filter by first digit of headcode (0-9). |
| freight | 1|0 | Freight only. |
| msg_type | string | 0001=activation, 0002=cancel, 0003=movement. |
Response fields
Envelope: count, date (null unless a date was requested), movements[]. Per movement: id, train_id, train_service_code, schedule_uid, tiploc, stanox, location_name, crs, event_type (ARRIVAL|DEPARTURE|PASS), msg_type (0003 = movement, 0002 = cancellation), toc_id, toc_name, headcode, actual_timestamp, planned_timestamp, variation_status (ON TIME|EARLY|LATE|OFF ROUTE), created_at (when RailFeed received it, as opposed to when the event happened), train_category, category_description, power_type, traction_class, traction_class_name, total_coaches/coach_detail (Darwin formation for the day, null when unknown).
CIF scheduled departures at a station (STP-resolved). No live data overlay — use /api/live_board for that.
| Parameter | Type | Description |
|---|---|---|
| crs * | string | CRS, TIPLOC, or STANOX. |
| date opt | string | YYYY-MM-DD. Default: today. |
Response fields
Envelope: date, count, departures[]. Per row: uid, headcode, toc_id, stp_indicator, power_type, train_category, tiploc/location_name/location_crs (this station), departure_time (WTT working time) and public_departure (advertised time; null where the stop isn't advertised), platform, line, activity (CIF activity codes), sequence_order (position within the schedule's calling list), origin_tiploc/origin_name, destination_tiploc/destination_name.
CIF scheduled arrivals at a station (STP-resolved).
| Parameter | Type | Description |
|---|---|---|
| crs * | string | CRS, TIPLOC, or STANOX. |
| date opt | string | YYYY-MM-DD. Default: today. |
Response fields
Envelope: date, count, arrivals[]. Per row: uid, headcode, toc_id, stp_indicator, power_type, train_category, tiploc/location_name/location_crs (this station), arrival_time (WTT working time) and public_arrival (advertised time; null where the stop isn't advertised), platform, path (booked path, the arrival-side counterpart of line), activity (CIF activity codes), sequence_order, origin_tiploc/origin_name, destination_tiploc/destination_name.
Full CIF calling points for a schedule UID on a date (STP-resolved). No live overlay — use /api/service_live for that.
| Parameter | Type | Description |
|---|---|---|
| uid * | string | Schedule UID (6 chars). |
| date opt | string | YYYY-MM-DD. Default: today. |
Response fields
Envelope: date, schedule object, stops[]. The schedule object: schedule_id, uid, headcode, toc_id, stp_indicator, power_type, train_category, origin_tiploc/origin_name, origin_departure, destination_tiploc/destination_name, destination_arrival.
stops[], one per calling point in sequence_order: id, schedule_id, tiploc, location_name, location_crs, location_type (LO origin, LI intermediate, LT terminus), arrival_time/departure_time/pass_time (WTT working times — a pass-through has only pass_time), public_arrival/public_departure (advertised times, null at points with no public call), platform, line, path, activity (CIF activity codes), sequence_order.
Latest position per active train (last 2 hours). One row per train_id with the most recent confirmed movement — a still-future forecast at a no-detection location (see auto_expected under live_board) is not used as the position until its predicted time has actually elapsed, so seconds_since_seen is never negative.
| Parameter | Type | Description |
|---|---|---|
| headcode_class | string | Filter by headcode's leading digit only (e.g. 1 for all passenger classes), not a full headcode match. |
| toc_id | string | Filter by 2-letter TOC code (e.g. GR). |
| freight | 1|0 | Freight only. |
Response fields
Envelope: count, as_of (ISO 8601 — the server time the snapshot was taken), positions[]. Per train: train_id, headcode, toc_id, toc_name, last_tiploc/tiploc_name/last_stanox (where it was last seen), last_event_type (ARRIVAL|DEPARTURE|PASS), last_seen, seconds_since_seen (never negative — a still-future forecast at a no-detection location is not used as a position; see auto_expected under live_board).
Search for TIPLOC locations by name, CRS, or STANOX. Also supports two map-mode listing modes that skip the text search entirely (checked before q, in this order): stations_only and bbox.
| Parameter | Type | Description |
|---|---|---|
| q * | string | Search term (matches name, tiploc, CRS, or STANOX). At least 2 characters. Ignored if stations_only or bbox is given. |
| stations_only opt | 1 | Return every CRS-coded (passenger) station in one shot, no text query needed — for populating a map on first load. |
| bbox opt | string | west,south,east,north — TIPLOCs (any type, not just passenger stations) within a map viewport. Capped at 500 rows. |
Response fields
Envelope: query (echoes q), results[]. Per result: tiploc, stanox, crs (null for locations with no public station code — junctions, yards, sidings), name, description, latitude, longitude.
Everything the timetable board’s station panel shows for one location, in a single call: its codes and description, whether the timetable actually books public calls there, plus the operators that serve it, its most common direct destinations and its nearest neighbouring stations.
Operators, destinations and neighbours come from a cached weekly-rebuilt summary and only exist for CRS-coded stations that the timetable serves. A junction, depot or yard returns its codes with those three arrays empty — that is the expected answer, not an error.
| Parameter | Type | Description |
|---|---|---|
| crs | string | 3-alpha station code, e.g. LDS. Either this or tiploc is required. |
| tiploc | string | TIPLOC, for locations with no CRS. Either this or crs is required. |
Response fields
crs (null for locations with no public station code), tiploc, name (as stored, uppercase), display_name (title-cased), stanox, description, latitude, longitude, is_passenger_station (derived from the timetable, not from the presence of a CRS), operators[] (toc_id, name, slug, count), destinations[] (crs, name, count), nearby[] (crs, name, km).
List all TOC (Train Operating Company) entries. Includes both ATOC 2-letter codes and TRUST numeric codes.
| Parameter | Type | Description |
|---|---|---|
| type opt | string | PASSENGER | FREIGHT | CHARTER. |
Response fields
Envelope: tocs[]. Per operator: toc_id (the CIF ATOC code, e.g. "VT"), name, type (PASSENGER|FREIGHT|CHARTER).
List all train category codes and their descriptions.
/api/categoriesResponse fields
Envelope: categories[]. Per entry: category (the two-character CIF train-category code, e.g. "XX", "OO", "BR") and description.
Feed connection status, record counts, last CIF import, recent errors.
/api/statusResponse fields
feeds[]: topic, status, last_message_at, messages_last_minute. last_cif_import: filename, import_type (FULL|DELTA), record_count, status, imported_at. counts: tiploc_count, schedule_count, movement_count. Plus last_vstp (most recent VSTP schedule received), errors[] (recent log entries — source, message, created_at) and server_time (ISO 8601).
Simple health check. Returns {"status":"ok"}.
Response fields
status — "ok" when the API and its database connection are up. Intended for uptime probes; it does not assert that the upstream Network Rail feeds are currently connected (use /api/status for that).
The physical unit registry — real unit and coach numbers from the NWR consist feed, which is passenger-only (no freight, London Overground or Tyne & Wear Metro). Four distinct modes, one per parameter.
| Parameter | Type | Description |
|---|---|---|
| facets opt | 1 | Operator and class lists with unit counts, for filter dropdowns. Accepts toc to scope the class list. |
| browse opt | 1 | Paged, filterable unit table. Takes toc, class, unit (number prefix), sort, dir, page, per_page. |
| unit opt | string | Full detail for one unit. Accepts either the registry key or, for a loco-hauled formation, the real locomotive number (91110 as well as NL08). |
| services opt | string | That unit's recent workings, newest first. Takes limit. |
Response fields
?unit= returns resource_group_id, loco_vehicle_id, vehicles[] (each with vehicle_id, coach_letter, type_of_vehicle, plus vehicle_name, max_speed_mph and number_of_seats where the feed carries them), operator_history[], day_pattern, days_seen_30, last_used_at, deep_link, future_sighting, and mileage — an object of total_miles (the feed's own cumulative lifetime odometer for that unit), window_miles (distance actually run across the retained window, summed from each working's own start/end readings), window_from/window_to (the dates that window really covers, which is not the same as the 30-day retention while the feed is younger than that) and workings. mileage is null for anything outside the consist feed's scope. ?services= rows carry leg_miles, null where the feed gave no reading rather than 0.
Calling points plus real track geometry for a unit's recent workings — what the route map on a unit page is drawn from.
| Parameter | Type | Description |
|---|---|---|
| unit req | string | Unit id, same dual-key resolution as /api/units?unit=. |
Response fields
Per working: identity, origin/destination, and geometry — a polyline from GTCL track centrelines. About 17% of TIPLOC pairs have no geometry, where a straight-line fallback applies; those workings expose geometry_available: false rather than a misleading chord.
The train logbook. This is the one endpoint that does not use API keys — it is session-authenticated with a CSRF token, because it is called from the signed-in logbook pages rather than by API clients. Writes (POST/PUT/DELETE) are owner-only; reads return public logs plus your own.
| Parameter | Type | Description |
|---|---|---|
| id opt | int | A single entry. |
| user opt | string | Scope to one username's public logbook. |
| stats opt | 1 | Profile totals: entries, favourite station, class and operator. |
| headcode / type / from / to opt | string | Filters. type is passenger|freight|charter|heritage|network_rail|other. |
| sort / page / per_page opt | string/int | Ordering and pagination. |
Lightweight station autocomplete with coordinates — the typeahead behind the timetable and logbook search boxes. Use /api/tiploc instead for map-bounded or bulk location queries.
| Parameter | Type | Description |
|---|---|---|
| q req | string | Name, CRS or TIPLOC fragment. |
Response fields
Per match: tiploc, crs, name, latitude, longitude.
Operator autocomplete. /api/toc returns the whole list; this one matches a fragment.
| Parameter | Type | Description |
|---|---|---|
| q req | string | Operator name fragment. |
Headcode lookup over the last 30 days — given a headcode, which schedules ran under it. Powers the logbook form's 🔍 headcode search.
| Parameter | Type | Description |
|---|---|---|
| q req | string | Headcode or prefix, e.g. 1S08. |
| date opt | string | YYYY-MM-DD. Default: today. |
Response fields
Per match: uid, headcode, origin and destination names, and toc_id/toc_name.
Which locations sit next to a given one on the network, derived from how schedules actually traverse them rather than from geography — so it follows the railway, not the map. Depth 2 reaches neighbours-of-neighbours, and terminal stations are flagged as such. Requires a full-scope API key, so the Try it explorer's public key will not work here.
| Parameter | Type | Description |
|---|---|---|
| crs / tiploc req | string | The location to start from. |
| depth opt | int | 1 or 2. Default: 1. |
| limit opt | int | Cap on neighbours returned. |
Response fields
Per neighbour: tiploc, crs, name, pass_count (how many schedules run between the two, i.e. how strong the link is) and is_terminal.
Junctions and level crossings a service runs through without a booked time — the places a train demonstrably passes that its timetable never mentions. Merged into /api/service_live's output when that is called with junctions=1; exposed separately here for callers that want only this layer.
| Parameter | Type | Description |
|---|---|---|
| uid / train_id req | string | Schedule UID or TRUST train ID. |
| date opt | string | YYYY-MM-DD. Default: today. |
Response fields
Per pass: tiploc, name, and est_pass_seconds — an estimate, interpolated between the timed points either side using BPLAN sectional running times, not an observed report.
Status of the pass-through compiler — the offline job that works out which locations each schedule passes without stopping. Operational metadata, not train data. Requires a full-scope API key.
/api/pass_through_statsResponse fields
Per run: mode (incremental|full|geometry), status, row and station counts, duration, and unsupported_rows — an integrity check counting rows for locations with fewer than two rail neighbours, which is a dead end and should never appear. Expect 0; anything else means the compiler produced pass-throughs it cannot justify.