Skip to content
Alpha — Headcode is currently in alpha. APIs and data may change without notice.

Performance

The performance endpoint exposes aggregate punctuality analytics for a station or a route over a date range. It answers questions like “how late do trains typically run from this station?” and “what proportion arrive on time?” without scanning raw service rows on every request.

All metrics are served from pre-computed daily rollups. A nightly batch job aggregates each finalised day’s lateness into rollup tables; the endpoint merges those daily rows across your requested range. It never scans the raw service_status or service_calling_point tables, so responses stay fast even over long ranges.

GET /v1/performance

Requires an API key with the performance:read scope.

For the selected dimension and date range, the response provides:

  • Lateness percentiles — median (P50) and 90th-percentile (P90) lateness in seconds, separately for arrival and departure. Negative values mean early. These are derived by merging the daily lateness histograms across the range, so multi-day percentiles remain statistically valid (you cannot simply average daily percentiles).
  • On-time percentage — the share of lateness samples that fall within the on-time threshold.
  • Cancellation rate — cancelled calling points as a percentage of all scheduled calling points.
  • Sample count — the number of lateness samples backing the percentiles.
  • On-time threshold — the cutoff (in seconds) the rollups were computed with.

Lateness is defined per calling point as actual − scheduled. Cancelled calling points have no actual time, so they are excluded from the lateness percentiles but counted in the cancellation rate.

You must select exactly one dimension — a station or a route. The two selectors are mutually exclusive:

  • Station — pass tpl (the station TIPLOC).
  • Route — pass both origin and destination TIPLOCs.

Either dimension can be narrowed to a single operator with operator. When operator is omitted, all operators are merged.

ParameterDescription
tplStation TIPLOC. Mutually exclusive with origin/destination.
originRoute origin TIPLOC. Requires destination; mutually exclusive with tpl.
destinationRoute destination TIPLOC. Requires origin; mutually exclusive with tpl.
operatorOptional two-character ATOC operator code to narrow the dimension. Omit to merge all operators.
fromInclusive start of the service-date range (YYYY-MM-DD), required.
toInclusive end of the service-date range (YYYY-MM-DD), required.
baselineTimetable baseline: public (default) or working.

Invalid combinations return a 400 — for example setting both tpl and origin, supplying origin without destination, an unknown baseline, or a to date before from.

Each rollup is computed against both timetable baselines, and you choose which to read with baseline:

  • public (default) — lateness measured against the public timetable (pta/ptd). Calling points with no public time (passing or non-public points) are excluded.
  • working — lateness measured against the working timetable (wta/wtd). This includes passing/non-public points that the public baseline omits.

Because both baselines are stored side by side, switching between them is a parameter change, not a recomputation.

“On time” is defined by a configurable threshold — by default 5 minutes (300 seconds) for the public baseline, an industry-standard cutoff. The threshold in force when a day was rolled up is stored per rollup row and returned as on_time_threshold_secs. Recording it with the data means changing the configured threshold later never silently reinterprets historical rollups: each day reflects the threshold it was computed with.

The cancellation rate is derived from the point-level cancelled flag on calling points (service.deleted is treated as schedule housekeeping and excluded). It is reported as cancelled calling points over all scheduled calling points for the dimension and range.

Rollups are produced by a nightly batch job, not on the live write path. A day’s services are only rolled up once that day is finalised — defined as a configurable lag (finalised_after, default 6 hours) past the end of the service date. This keeps the job off days whose data is still being updated. Re-running the rollup for a given service date replaces that day’s rows (idempotent delete-then-insert), and a backfill mode can compute rollups for retained historical dates.

In practice this means the most recent fully-finalised day is available the morning after it ends; very recent days within the finalisation lag may not yet have rollups.

Median and P90 lateness, on-time percentage and cancellation rate for London Paddington over three days, on the public baseline:

GET /v1/performance?tpl=PADTON&from=2026-05-12&to=2026-05-14
Authorization: Bearer hc_...
{
"dimension": "station",
"tpl": "PADTON",
"operator": null,
"baseline": "public",
"from": "2026-05-12",
"to": "2026-05-14",
"arrival": {
"p50_secs": 60,
"p90_secs": 480
},
"departure": {
"p50_secs": 30,
"p90_secs": 300
},
"on_time_percent": 88.4,
"cancellation_rate": 1.7,
"sample_count": 12840,
"on_time_threshold_secs": 300
}

When the range has no eligible samples, arrival/departure percentile fields and on_time_percent are null, and cancellation_rate is null when there are no scheduled points. A route response replaces tpl with origin_tpl and destination_tpl.