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.
Authentication
Every endpoint needs an API key, created on your API Keys page once you have an account. Each account has one key. You can replace it (the old one stops working at once) or revoke it there whenever you like; a key is personal to you and the limits below apply to it, so please don't share it. Send it on each request either as an X-Api-Key header or as a bearer token — the two are equivalent:
curl -H "X-Api-Key: rf_live_…" https://railfeed.co.uk/api/live_board?crs=LDS curl -H "Authorization: Bearer rf_live_…" https://railfeed.co.uk/api/live_board?crs=LDS
Keys have a scope. A full key works everywhere. A public_tools key works on every endpoint except /api/adjacent_tiplocs and /api/pass_through_stats, which answer 403 with code scope to it — it exists so a key can be embedded in a public page without opening up the operational endpoints if it is ever lifted from the page source. /api/logs also accepts a signed-in session in place of a key, and its writes need one: they are the logbook's own, authenticated by the browser session and a CSRF token.
Browsers may call /api/ directly: responses carry Access-Control-Allow-Origin: * and the preflight allows the X-Api-Key, Authorization and Content-Type headers. Bear in mind that a key in client-side code is visible to anyone who looks.
Errors
Every error is a JSON object with the same two fields, plus param when the problem is one particular query parameter. Check the HTTP status first and code second; the error text is for humans and may change.
{"error": "date must be YYYY-MM-DD", "code": "invalid_param", "param": "date"}
| Status | code | Meaning |
|---|---|---|
| 400 | missing_param | A required parameter was not given. param names it. |
| 400 | invalid_param | A parameter has a value the endpoint cannot use — a malformed or impossible date, a type or mode outside its allowed list, a bad time window. param names it. Numeric limits are never rejected: an out-of-range limit, hours or per_page is clamped to its bounds. |
| 401 | no_credential | No API key was sent. |
| 401 | invalid_key | The key is unknown, revoked, or belongs to a deactivated account. |
| 401 | no_session | /api/logs write without being signed in. |
| 403 | scope | The key's scope does not cover this endpoint (see Authentication). |
| 403 | forbidden | Signed in, but not allowed to change this record (someone else's log). |
| 403 | use_official_api | A request to /data/, the website's own internal path, from anywhere but this site's pages. Use the same endpoint under /api/ with a key; api_endpoint in the response names it. On live_board and passthrough_board the refusal also carries one placeholder services[] row saying so, for displays that never read error. |
| 403 | invalid_csrf | /api/logs write with a missing or stale CSRF token. |
| 404 | not_found | The station, schedule, unit, charter or log does not exist, or the schedule does not run on that date. An unknown crs, tiploc or stanox is a 404, never an empty 200. |
| 405 | method_not_allowed | HTTP method the endpoint does not support. |
| 429 | rate_limited | Over the limit below. Back off and retry; nothing was processed. |
| 503 | unavailable | The database is not reachable (only /api/health reports this; everything else would fail the same way). |
Numbers are JSON numbers (latitude, longitude, counts, minutes), booleans are JSON booleans, and a field that is listed for an endpoint is always present in its response — null when unknown — so you can read it without checking for existence.
Rate limits
Two limits apply to /api/ at once, and a request must pass both: 60 requests per minute per key (bursts of up to 20 above that are absorbed) and 120 requests per minute per client IP (bursts of up to 40). Exceeding either returns 429 with code rate_limited. The per-IP limit is what stops junk and missing keys costing anything; the per-key limit is the one you will meet in practice. Poll a live board no more than every 30 seconds — the feeds behind it do not update faster than that. If you need more, get in touch and say what for.
Times
Two clocks appear in answers, and each field says which it uses. Scheduled, public and projected times (scheduled_*, public_*, projected_*, origin_departure and the like) are UK local time, as printed in the timetable. Actual times (actual_arrival, actual_departure, actual_pass) and the raw movement timestamps (actual_timestamp, planned_timestamp, created_at) are UTC, as YYYY-MM-DD HH:MM:SS with no offset written: an hour behind the clock in Britain during British Summer Time, the same from late October to late March. Every actual_arrival, actual_departure and actual_pass on /api/live_board, /api/passthrough_board, /api/service_live and /api/charters also has a UK-local twin beside it, actual_arrival_uk, actual_departure_uk and actual_pass_uk: the same instant, same format, on the clock the scheduled times use, so the two can be shown side by side without converting. Dates (date parameters, and the day a figure covers) are UK-local days.
Caching
Every successful GET carries Cache-Control: private, max-age=N saying how long that answer stays current: 30 seconds for live boards, a train's running and positions; a minute or two for unit pages and timetabled boards; five minutes for delays and cancellations; fifteen minutes for schedules and specials; a day for stations and operators. Reuse an answer for that long instead of asking again; it costs nothing against your limits. Errors, and anything about your own account, are no-store.
Please do not point automated clients at the website's pages. /data/ is the site's own internal path and answers only requests from its pages; anything else gets 403 with code use_official_api and a pointer back to this page.
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; one of the three is required, and if several are given crs wins, then tiploc. A code that matches no location is a 404. |
| 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, clamped). Default: 2. |
| type opt | string | departures | arrivals | both | passes. Default: departures. Use passes for through-trains that don't stop. Anything else is a 400. |
| 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), live_platform (Darwin's current platform for this call — where a platform alteration appears before the train arrives; today only, passenger only, and null when Darwin has none or says only "TBC"), live_platform_confirmed (boolean — Darwin calls it confirmed rather than provisional), live_platform_suppressed (boolean — the operator has asked for this platform to be kept off public screens, usually until the train is ready to board; RailFeed returns it anyway, so filter on this if your display should honour that), late_reason/late_reason_code (Darwin's reason for the service running late, as on service_live; today only, and returned whenever Darwin has one — including on trains that have since recovered, so check the delay before showing it), 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 (UTC; _uk twins in UK time, see Times), delay_mins, variation_status, delay_monitoring_point (boolean — true if this location is an official delay-attribution point), auto_expected (boolean — true means TRUST expects this location to report automatically from the signalling, which is the case for nearly every report (about 93% of them); false is a manually entered report; null is unknown. It does not mean the time is a forecast. Automatic reports are occasionally stamped ahead of the clock from the train's last known position (about 1%), so an actual time later than now is a prediction rather than a confirmation, and the flag on its own says nothing about whether the train was seen), status (expected|active|cancelled|passed — already accounts for that: an actual time 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; both null otherwise), reached_destination (boolean — the train's latest report is at its own destination, so the working is over), movement_toc_name (the operator named by the real-time TRUST feed, resolved from its numeric sector code — a different code space from toc_id; only set once the train has activated and reported, and the thing to read for a generic ZZ freight schedule), cancel_type (AT ORIGIN|EN ROUTE|ON CALL|OUT OF PLAN, null unless cancelled; ON CALL is a runs-as-required standby path marked not required, which never ran and is not a passenger-facing cancellation), cancel_reason_code/cancel_reason (the Delay Attribution Guide code TRUST sent and its wording — see cancellations below; the wording is null for a code outside the imported guide), 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), vehicle_label (object or null — what to show for the train's vehicles, ready to display: text, title (a tooltip, or null) and is_consist (true when text is the real unit numbers). The units it ran with (consist, in position order, a locomotive by its own number) take the place of the timetable's class; otherwise traction_class_name; otherwise the timing load (with "t" for a loco-hauled tonnage); otherwise power_type. A train booked as a Class 800 that ran as 185109 says 185109. The same rule as the website's)
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, services and station_platforms: every platform number at the station from Network Rail's BPLAN reference data, as strings in number order (e.g. ["1","2",…,"11"]), including platforms nothing uses on the day. Sections such as 11A or 4R are folded into their number, and an empty array means BPLAN lists none.
Projected times
projected_arrival/projected_departure/projected_pass (UK-local HH:MM:SS, the same clock as the scheduled_* fields beside them) and projected_delay_mins (the effective lateness there, after dwell absorption and the early-departure clamp).
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. 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 RailFeed's own projection, permanently. Where RailFeed projects, 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.
Three fields say where a projection came from. projection_basis is darwin where the value is Darwin’s own forecast, single_point where it is ours, and null when nothing was projected. 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. projection_carry_source is darwin when a single_point projection's lateness was taken from Darwin's view further down the route rather than from the last TRUST report (null otherwise). projection_suppressed names the reason when nothing was projected (on_time, stale_anchor, cancelled, off_route, reached_destination, past_date, no_anchor, implausible_carry). The boards do not carry service_live's per-stop projected_source.
Note: estimated_pass_time is 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.
What is standing at each platform of a station right now, and what is due next, worked out from today's live_board. It is what the timetable page's Platform view shows. Today only. A train that ends here stays on its platform until the train it forms leaves, matched by unit where the consist feed gives one, otherwise the next train starting from that platform before anything else arrives. A calling train is there from its actual arrival to its actual departure, and a train starting with nothing arriving before it shows from 10 minutes before it leaves. Empty-stock moves count, as they are timetabled with platforms. There is one row per platform number, including platforms nothing uses today (from BPLAN); a train in a section of it, such as 11A, says so in its own platform.
| Parameter | Type | Description |
|---|---|---|
| crs * | string | 3-letter CRS code, or use tiploc. An unknown code is a 404. |
| tiploc | string | TIPLOC code. |
Top level: station, date, generated_at, platforms[]. Per platform: platform (the number, or e.g. "?" for trains with none known), here[] (trains standing there now, usually zero or one) and next (the next train due, or null). Each train: state (here, nostock for a train whose incoming stock was cancelled, arriving within 5 minutes, due, or cancelled, shown for 10 minutes after its booked time), kind (call, term ending here, start starting here), countdown_to, platform, uid, headcode, toc_name, origin, destination, units[], delay_mins, cancelled, cancel_reason, booked_arrival, booked_departure, arrival and departure (actual once it has happened, otherwise expected), arrived, departed, forms (for a train ending here, the train it forms) and formed_by (on nostock, the cancelled incoming train); these two carry the train fields only, without state, countdown_to, forms or formed_by. Times are ISO 8601 with the UK offset. Count down locally: minutes left = ceil((countdown_to − now) / 60 s), which is the departure for a train standing there (or of the train it forms), the arrival for one due in, the departure for a train due to start, and the booked time for a cancelled one. It is null for a train that has ended here with nothing booked to form. Fetch again about every 30 seconds for new reports.
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). Rows use the same field names as live_board wherever the two overlap — every entry has is_pass=true — so the two can be merged into one board client-side (see timetables.php, which does exactly this). It is a subset, not a superset: a passing train has no train_id, scheduled_departure/scheduled_arrival, actual_arrival or projected_arrival/projected_departure, and the per-row fields are listed in full below.
| Parameter | Type | Description |
|---|---|---|
| crs * | string | 3-letter CRS code. Alternatively use tiploc (no stanox here). An unknown code is a 404. |
| tiploc | string | TIPLOC code. |
| date opt | string | YYYY-MM-DD. Default: today. |
| hours opt | int | Look-ahead window in hours (1-24, clamped). 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_id, toc_name, toc_type, movement_toc_name (as on live_board), train_category, 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), projected_pass/projected_delay_mins/projection_basis/projection_suppressed (see Projected times below), actual_departure (UTC, with actual_departure_uk in UK time; 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), cancel_type/cancel_reason_code/cancel_reason (as on live_board; null unless cancelled), stale (boolean — see live_board's auto_expected/staleness notes, identical mechanism), reached_destination, 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), vehicle_label (object or null — what to show for the train's vehicles, ready to display: text, title (a tooltip, or null) and is_consist (true when text is the real unit numbers). The units it ran with (consist, in position order, a locomotive by its own number) take the place of the timetable's class; otherwise traction_class_name; otherwise the timing load (with "t" for a loco-hauled tonnage); otherwise power_type. A train booked as a Class 800 that ran as 185109 says 185109. The same rule as the website's).
Projected times
projected_pass and projected_delay_mins follow the same rules as live_board's projected times above, including the three-way precedence of a confirmed TRUST report, then Darwin, then RailFeed's own projection. projection_basis (darwin|single_point|null) and projection_suppressed mean the same as there. There is no projection_carry_source on this board.
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. A UID that has no schedule running on that date is a 404. |
| project opt | 1|0 | Projected times (see below). Default: 1, on. Pass 0 to switch the whole projection pass off; the projected_* fields are then null and projection.applied is false. |
| junctions opt | 1|0 | Merge in the junctions the train passes without a booked time (the /api/junction_passes layer) as extra PASS stops flagged junction: true. Default: 1, on. Pass 0 for the CIF schedule and compiled pass-throughs only. |
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).
Top level also: vehicle_label (object or null — what to show for the train's vehicles, ready to display: text, title (a tooltip, or null) and is_consist (true when text is the real unit numbers). The units it ran with (consist, in position order, a locomotive by its own number) take the place of the timetable's class; otherwise traction_class_name; otherwise the timing load (with "t" for a loco-hauled tonnage); otherwise power_type. A train booked as a Class 800 that ran as 185109 says 185109. The same rule as the website's).
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, live_platform/live_platform_confirmed/live_platform_suppressed (Darwin's live platform per call, as on live_board; absent with project=0), 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 (UTC; _uk twins in UK time, see Times), 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 — the location reports automatically; not a forecast flag), status (expected|reported|passed — an actual time still in the future 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), junction (boolean — true for a stop that came from the junction layer, see the junctions parameter; such a stop is also inferred), projected (boolean — true when any of this stop's projected_* values is set), route_from_prev (array of [lat, lon] pairs tracing the real track between this stop and the previous one in the list, or null — precomputed from Network Rail's track centreline data; coverage isn't complete, treat null as "draw a straight line instead"). When inferred stops lie on a hop, its track is shared out among them: each inferred stop carries the piece from the stop before it, and the booked stop the rest, so joining every stop's route_from_prev in order draws the whole line. A map that leaves inferred stops out should join their pieces onto the next booked stop's.
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. Projection is on by default; 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.
Two further categories can be asked for with categories. network_rail is Network Rail's own test, route-learning and monitoring trains, identified by CIF train_category = 'DT' rather than by headcode (they run under 1Q, 2Q, 3Q and other prefixes, so a headcode match misses most of them). departmental is engineering and departmental stock (train_category DD, DH or DI) — there can be 600+ of these on one day, so narrow with region or expect a very large response. Each item's category field says which bucket it landed in.
| 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; a UID not running that day is a 404. |
| categories opt | string | Comma-separated subset of charter, network_rail, departmental. Default: charter only. Unknown names are ignored. |
| region opt | string | Keep only schedules whose origin falls in one broad area: scotland, north_east, north_west, midlands, wales_western, south_east. An approximate latitude/longitude banding, not Network Rail's route boundaries. Anything else is a 400. The list is also returned as regions[] so a UI can build its dropdown from the server's own definition. |
| junctions opt | 1|0 | Include junction passes as extra PASS stops, as on service_live. Default: 1. |
Response fields
Top level: date, mode (list|detail), count, charters[], regions[] (key, label). Per charter: uid, headcode, category (charter|network_rail|departmental), kind (railhead_treatment|track_recording|hst_test, where the timetable makes it clear what the train is; otherwise null), 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 (UTC; _uk twins in UK time, see Times), 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
Each stop carries the same projected_arrival/projected_departure/projected_pass, projected_delay_mins, projected and projected_source fields as service_live, with the same three-way precedence (confirmed TRUST report, then Darwin, then RailFeed's own projection) and the same meanings — see service_live above. Two differences: there is no service-level projection object here, and there is no project switch; projection is always applied, and only for today's date, since TRUST has no reports for any other day.
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. An unknown code is a 404. |
| threshold opt | int | Minimum delay in minutes, at least 1. 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 (toc_id here is TRUST's numeric sector code, e.g. "60", because the figures come from movement reports — not the two-letter CIF code the schedule endpoints use), 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), late_reason/late_reason_code (Darwin's own passenger-facing reason for the late running and its numeric code — today only, since Darwin's reasons don't outlive the day, and never on a cancelled service, which has its cancellation reason instead), 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. |
| overnight opt | boolean | With date: also return messages written up to 10:00 the next morning, so a train that started on the date and was cancelled after midnight (sleepers, the last trains of the evening) is included. The extra hours also carry messages about the next day's own early trains, so filter by the last two characters of train_id (the day of the month the train started) if you want only the date's trains. |
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 (UTC, YYYY-MM-DD HH:MM:SS — the one timestamp here that is not UK-local), 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. baseline shows each location's own 14-day average instead of the day itself, the "what is normal here" picture. anomaly scores each location against that average, so a permanently busy station only stands out when it is having a worse day than usual. routes draws the lines cancelled trains would have run, instead of points — see below. Anything else is a 400. |
| type opt | string | passenger (default), freight or all, the same buckets as /api/delay_league's type. 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. |
| segment opt | string | With mode=routes only: TIPLOC|TIPLOC for one stretch of line. Returns a different, small shape: date, type, a and b (each {tiploc, name}), n (cancelled trains over that stretch) and trains[] — up to 25 of them, each with headcode, origin, destination and service_url. |
Response fields
Envelope: date, mode, type, total (cancellations), locations, max_count, cached, earliest_date (oldest day in the archive), no_coords (locations dropped for want of coordinates), and in baseline/anomaly modes 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.
mode=routes returns a different shape: each cancelled train is resolved to its schedule and counted on every stretch of line it would still have run over — from where it was cancelled to its destination, or its whole route if it never started. Envelope: date, mode, type, total (cancellations), stretches, max_n, earliest_date, and top (the ten most affected stretches, with a, b, n, name). routes is an array of {a, b, n, line} — the two TIPLOCs (alphabetical, since both directions of a line are counted together), the number of cancelled trains, and line as [[lat, lon], …], simplified for display. It's sorted ascending by n, so drawing in order puts the worst lines on top. Most lines follow real track; where no track shape is known, the line is straight. Routes data begins 2026-09-19, when it could first be rebuilt from the raw feed. A day's routes are built on first request and cached — today's copy is refreshed every few minutes, a finished day's is kept — so the first call for a day not yet in the archive can take a couple of seconds, and each routes request also fills in at most one missing archive day in the background.
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, minimum 1. Cap is 500 without date, 2000 with it (a day's worth of events can exceed 500 at a busy station); a larger value is clamped, not rejected. |
| 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 (both UTC, see Times), 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 requested day — today's when no date was given — 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 | 3-letter CRS code. Alternatively tiploc or stanox — separate parameters, each matching only its own kind of code; one of the three is required, and tiploc wins over crs over stanox if several are given. An unknown code is a 404. |
| tiploc | string | TIPLOC code. |
| stanox | string | STANOX code. |
| 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 | 3-letter CRS code. Alternatively tiploc or stanox, exactly as on departures. An unknown code is a 404. |
| tiploc | string | TIPLOC code. |
| stanox | string | STANOX code. |
| 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: 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 — an actual time still in the future (TRUST occasionally stamps an automatic report ahead of the clock; see auto_expected on 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, TIPLOC or CRS. 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 — a substring of the name or description, or of the TIPLOC or CRS code (case-insensitive). STANOX is returned but not searched. At least 2 characters. Up to 50 results, exact TIPLOC and CRS matches first. 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; absent in the two map modes), results[]. Per result: tiploc, stanox, crs (null for locations with no public station code — junctions, yards, sidings), name, description, latitude, longitude (numbers, or null), and for a q search only is_passenger_station (boolean — derived from whether the timetable books public calls there, which is a better test than the presence of a CRS: 450 depots and yards carry one).
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. A type outside the three values is a 400.
| 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 (integer). last_cif_import: filename, import_type (FULL|DELTA), record_count, status, imported_at. counts: tiploc_count, schedule_count, movement_count (the last two are the database's own estimates, within a few per cent). 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"} after one trivial database round-trip, or 503 with code unavailable when the database cannot be reached.
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 (operator, last_ran and the other column names the table shows; an unknown value falls back to the default order), dir (asc|desc, default desc), page (from 1), per_page (default 50, max 200). |
| 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). Unknown is a 404. |
| services opt | string | That unit's recent workings, newest first. Takes limit (default 30, max 1000). An unknown unit returns an empty list. |
| include opt | string | With unit: workings adds the unit page's Workings, by day, with how each train ran. Slower than the rest of the detail, so only ask when you show it. |
Response fields
?facets=1 returns operators[] (toc_id, name, unit_count) and classes[] (unit_class, class_name, unit_count). ?browse=1 returns units[] (one row per unit: resource_group_id, type_of_resource, last_seen_at, operator, last working and the display number), plus total, page and per_page.
?unit= wraps everything in a single unit object: resource_group_id, unit_name (the printed number, e.g. "801216" or "91110"), type_of_resource, 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; number_of_seats is Network Rail's single figure for the vehicle, with no first/standard split, and for a car with first class it can be the first-class seats only, so show it as "seats listed" rather than a full count), operator_history[], day_pattern (counts_by_dow, total_days_seen), days_seen_30, last_seen_at, last_used_at, last_service_date, last_headcode, last_toc_id/last_toc_name, last_origin/last_dest, last_vehicle_count, deep_link, future_sighting, logs[]/log_count (public logbook entries that mention this unit, plus your own if you are signed in), 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 days (how many distinct days the window counted). mileage is null for anything outside the consist feed's scope. ?services= returns services[], each with schedule_uid, trust_train_id, headcode, service_date, train_origin_datetime, toc_name, origin_name/dest_name, is_future, is_in_service, deep_link and leg_miles (null where the feed gave no reading rather than 0).
?unit=…&include=workings adds workings[]: one entry per day over the retained window, newest first, including days the unit wasn't out (empty workings). Each day has date, miles (run that day by the unit's own odometer, or null) and workings[] in time order, each with time, headcode, uid, service_date, origin/destination with their _tiploc and _crs, toc_id/toc_name, miles, empty (an empty-stock or light-engine move), and how it ran: status (on_time, late, cancelled, terminated, running, scheduled, or null when the railway's reporting said nothing), late_minutes (at the final arrival, or at the last report while running), cancel_reason (a plain category such as "train fault", or null) and terminated_at. A unit can hold more than one provisional diagram for a day, so a day's list may include a working another unit ended up running.
?unit=…&include=now,ran_with,neighbours (any of them, comma-separated) adds the unit page's extras. now is where it is: state (now running, today out today between trips, days last ran within 30 days, gone not seen for 30 days or never), date it last ran (UK) and days_ago, late_mins (now only; negative is early), train (that working: headcode, uid, date, origin/destination with _crs, departs in UK time, toc_name, empty), next (today only: its next booked working, same shape), with (the unit it is coupled to on that working) and, for gone with no working left in the history, last_known (its last train's headcode, uid, date, toc_name, and its route where known: origin/destination with _crs, departs, and route_date/route_headcode, which differ from the last train when the route comes from an earlier working). ran_with[] is the units it ran coupled to over the last three weeks (unit, resource_group_id, last_at, trains), newest first; neighbours[] is up to six units either side in its class by number (unit, resource_group_id, last_ran_at, last_headcode, seen = today/days/gone, seen_text such as "Seen 3 days ago"). Every ?unit= answer also has following: null signed out, false when you don't follow it, else {notify_out, notify_logged} (follow and unfollow at /api/unit_follows).
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=. Unknown is a 404; a known unit with no recorded workings is an empty services[]. |
Response fields
services[], one per working in the same order as /api/units?services=: schedule_uid, trust_train_id, headcode, train_origin_datetime, service_date, is_future, is_in_service, toc_name, origin_name/dest_name, geometry_available and stops[].
geometry_available is false when the working's schedule could not be resolved — a VSTP-only working known by TRUST id alone, or a schedule withdrawn (STP-C) on that date — and stops is then null. When true, stops[] is every calling point in order: tiploc, location_name, latitude, longitude and route_from_prev, an array of [lat, lon] pairs tracing the real track from the previous stop, from GTCL track centrelines. About 17% of TIPLOC pairs have no geometry; there route_from_prev is null and the honest rendering is a straight line between the two stops.
The train logbook. Reading needs an API key, like every endpoint (or a signed-in session); writes are session-authenticated with a CSRF token, because they come from the signed-in logbook pages rather than API clients. A list is always one person's logbook: user names it (anyone's, while their logbook is public; all of your own entries if it is you), and without it you get your own when signed in or 400 missing_param when not. There is no list of everyone's logs. Writes (POST/PUT/DELETE) are owner-only and answer 401 no_session without a sign-in, 403 invalid_csrf without the token, and 403 forbidden for someone else's entry.
| Parameter | Type | Description |
|---|---|---|
| id opt | int | A single entry (404 if private to someone else). |
| user opt | string | Whose logbook to list. Defaults to your own when signed in; required otherwise. |
| stats opt | 1 | Profile totals: entries, trains, days out (days), the first entry's time (first_logged_at), entries per type (types), and favourite station, class and operator. |
| headcode / type opt | string | Filters. type is one or a comma-separated list of passenger|freight|charter|heritage|network_rail|other. |
| q opt | string | Search: a unit number or headcode by prefix, or part of a place name or title. |
| from / to opt | date | YYYY-MM-DD bounds on when the train was logged (inclusive) — dates, not stations. |
| sort / page / per_page opt | string/int | sort is desc (default, newest first), asc, or day (newest day first, each day's entries earliest first); page from 1; per_page default 20, max 50. |
Response fields
A list returns logs[], total, page, per_page and total_pages. Each log: id, user_id, username, logged_at, latitude/longitude (numbers), tiploc/location_label, origin_tiploc/origin_label, destination_tiploc/destination_label, train_number, train_numbers[], headcode, toc_id/toc_name, platform, service_type, carriages, carriage_numbers[], photo_path (with photo_focal_x/photo_focal_y), title, notes, is_public, created_at. On someone else's entry, latitude/longitude are the station's position when it was seen at one, otherwise rounded to 2 decimal places (about 1 km); location_precision says which: exact (your own), station or area.
How much of the network and the fleet a logbook covers, as on /completionist. Same visibility as /api/logs: anyone's while their logbook is public (404 not_found otherwise, the same as a name that doesn't exist), your own when signed in. Fleet figures are of the units RailFeed has seen in the consist feed (passenger stock), not of every unit that exists: units_unmatched counts logged units it can't place, which count towards nothing.
| Parameter | Type | Description |
|---|---|---|
| user opt | string | Whose logbook. Required unless signed in. |
Response fields
username; summary with stations (logged, total, pct), classes and tocs (started, total, pct) and fleet (logged, total, pct); then the full lists: stations (logged, total, stations[] of crs/name), classes and tocs (started, total, rows[] each with logged of total units, by unit_class/class_name or toc_id/toc_name), and units_logged, units_matched, units_unmatched, fleet_total.
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 opt | string | Name, CRS or TIPLOC fragment (at least 2 characters). Required unless lat/lon are given. |
| stations opt | 1 | Only passenger stations (one per CRS). Without it, junctions, depots and yards match too. |
| lat, lon opt | float | Instead of q: the three nearest stations to this point, each with metres. Great Britain only — a point outside it is a 400. |
Response fields
results[], up to 25 for a text search, stations before junctions. Per match: tiploc, crs, name, description, latitude, longitude (numbers; only locations with coordinates are returned). A q shorter than two characters returns an empty list rather than an error, so a typeahead can call it on every keystroke.
Operator autocomplete. /api/toc returns the whole list; this one matches a fragment.
| Parameter | Type | Description |
|---|---|---|
| q req | string | Operator name fragment, or an exact TOC code. Empty returns an empty list. |
Response fields
results[], up to 20, passenger operators first: toc_id, name, type (PASSENGER|FREIGHT|CHARTER). Where an operator exists under both a two-letter CIF code and a numeric TRUST code, only the two-letter row is returned.
Headcode lookup over the last 30 days — given a headcode, which schedules ran under it. Powers the logbook form's 🔍 headcode search. Matches schedules whose validity overlaps the 30 days up to date and that run on date's day of the week, so a Saturday-only working is only found from a Saturday date.
| Parameter | Type | Description |
|---|---|---|
| q opt | string | Headcode or prefix, e.g. 1S08 or 1S. Omit for the first 50 headcodes alphabetically, which is rarely useful. |
| date opt | string | YYYY-MM-DD. Default: today. |
| runs opt | 1 | A different question: which trains under the exact headcode q run on date. One row per train after STP precedence, as the boards and /api/service_live resolve it (an overlay beats the permanent schedule; a day cancelled in the timetable drops out). Answers headcode, date, count and runs[], each with uid, headcode, stp_indicator, toc_id/toc_name, train_category, origin_tiploc/origin_name/origin_crs, origin_departure, destination_tiploc/destination_name/destination_crs and destination_arrival, ordered by departure. Headcodes are reused around the country, so expect several operators. A row's uid and the date are what /api/service_live takes. 400 invalid_param unless q is a whole headcode. |
Response fields
Envelope: headcodes[] (up to 50), date, earliest (the start of the 30-day window). Per match: uid, headcode, origin_tiploc/origin_name, destination_tiploc/destination_name, 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; the keyless Try it path answers 403 scope, so there is no panel here.
| Parameter | Type | Description |
|---|---|---|
| crs / tiploc req | string | The location to start from. Unknown is a 404. |
| depth opt | int | 1 or 2. Default: 2. At depth 2 the second hop is taken only from depth-1 neighbours that have no CRS (junctions), which is where parallel and bypass lines branch. |
| limit opt | int | Neighbours returned per depth level, 1-20. Default: 10. |
Response fields
Envelope: station (name), tiplocs[] (the TIPLOCs the start location resolved to), is_terminal (boolean, for the start location — true when more than 80% of the schedules touching it start or end there) and adjacent[], sorted by pass_count then depth. Per neighbour: tiploc, name, crs, is_junction (no CRS), depth (1 or 2), frequency (how many schedules have the two locations consecutive, i.e. how strong the link is), pass_count (how many of those run through the neighbour without stopping), times_before/times_after (in how many of them the neighbour comes before, or after, the start location).
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 by default (pass junctions=0 there to leave it out); exposed separately here for callers that want only this layer.
| Parameter | Type | Description |
|---|---|---|
| uid / train_id req | string | Schedule UID or TRUST train ID. A UID with no schedule running on the date is a 404. |
| date opt | string | YYYY-MM-DD. Default: today. |
Response fields
Envelope: date, uid, headcode, schedule_id, available (false if the compiled junction layer has been removed — the endpoint then answers with an empty list rather than an error), junctions[], count. Per pass: tiploc, location_name, latitude, longitude, estimated_pass_time (HH:MM:SS) and est_pass_seconds (the same instant as seconds from midnight) — an estimate, interpolated between the timed points either side using BPLAN sectional running times, not an observed report. A junction the schedule itself books a time at is never listed here.
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
last_run (the most recent run only, or null if it has never run): ran_at, mode (incremental|full|geometry), status, duration_secs (elapsed so far while status is running), stations_checked, rows_written, error_msg, 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 (null when that run did not compute it). Alongside it: total_pass_throughs and stations_covered, the size of the compiled table right now.
