Forecast a live trip
A delay forecast for every remaining call of a trip that is in the feed.
/api/punctuality/trips/:tripId Anchors to the trip's current delay — read from its next call, since no trip in this feed reports a trip-level delay — then applies the empirical distribution of how far delay drifts over N stops.
Each returned call carries both feedDelaySeconds (what the operator itself predicts for that stop) and forecast (what the model says). Comparing the two is the point: where they diverge, the operator is claiming something the network-wide pattern does not support.
stopId or stopSequence narrows the result to a single call. Asking for a stop the trip has already passed is a 404 that tells you where the trip actually is.
Parameters
Path
| Parameter | Type | Default | Description |
|---|---|---|---|
tripIdrequiredpath | string | — | A trip id present in the current feed. Same ids as list trip updates. |
Query
| Parameter | Type | Default | Description |
|---|---|---|---|
stopId | string | — | Narrow to the remaining call at this stop. |
stopSequence | number | — | Narrow to the remaining call at this stop sequence. |
delayBasis | enum | departure | Whether to read each call's delay from its departure or its arrival event. In this capture the two are usually close but not identical — a vehicle can arrive on time and leave late.
|
toleranceSeconds | number | 60 | The half-width of the on-time band, 0–3600. A delay within ±this counts as |
thresholdSeconds | string | — | Comma-separated delays, at most 10, to report an exceedance probability for. |
limit | integer | 50 | Page size, 1–1000. Values outside that range are a 400 rather than a silent clamp. |
offset | integer | 0 | Rows to skip. Compare against |
Response
The list envelope over forecasts, plus a trip object describing what is being forecast (including current, the anchor) and a model object carrying provenance. Both are repeated on every response so a stored one stays interpretable.
curl "$API_BASE/api/punctuality/trips/14010000701096422?limit=2" {
"data": [
{
"stopId": "9022050006719002",
"stopSequence": 16,
"stopName": "Kulfångsgatan",
"predictedArrival": "2026-04-27T11:13:41.000Z",
"predictedDeparture": "2026-04-27T11:13:41.000Z",
"feedDelaySeconds": 33,
"forecast": {
"horizonStops": 1,
"horizonBand": "1",
"method": "anchored",
"delayBasis": "departure",
"currentDelaySeconds": 45,
"basis": "mode",
"basisKey": "bus",
"sampleSize": 786,
"sufficientSamples": true,
"toleranceSeconds": 60,
"probability": {
"early": 0.018,
"onTime": 0.607,
"late": 0.375
},
"delaySeconds": {
"p10": 11,
"p25": 34,
"p50": 52,
"p75": 73,
"p90": 100,
"mean": 57
},
"expected": "on_time"
}
},
{
"stopId": "9022050006758002",
"stopSequence": 17,
"stopName": "Söderbytorp",
"predictedArrival": "2026-04-27T11:15:41.000Z",
"predictedDeparture": "2026-04-27T11:15:52.000Z",
"feedDelaySeconds": 28,
"forecast": {
"horizonStops": 2,
"horizonBand": "2-3",
"method": "anchored",
"delayBasis": "departure",
"currentDelaySeconds": 45,
"basis": "mode",
"basisKey": "bus",
"sampleSize": 1556,
"sufficientSamples": true,
"toleranceSeconds": 60,
"probability": {
"early": 0.047,
"onTime": 0.512,
"late": 0.441
},
"delaySeconds": {
"p10": -19,
"p25": 23,
"p50": 52,
"p75": 91,
"p90": 135,
"mean": 58
},
"expected": "on_time"
}
}
],
"meta": {
"count": 2,
"total": 4,
"limit": 2,
"offset": 0,
"feedTimestamp": "2026-04-27T11:22:38.000Z",
"loadedAt": "2026-08-07T12:36:54.462Z"
},
"trip": {
"tripId": "14010000701096422",
"routeId": "9011001083800000",
"route": {
"routeId": "9011001083800000",
"agencyId": "505000000000000001",
"shortName": "838",
"type": 700
},
"mode": "bus",
"startDate": "20260427",
"remainingCalls": 5,
"current": {
"stopId": "9022050001171005",
"stopSequence": 15,
"stopName": "Brandbergens centrum",
"delaySeconds": 45,
"status": "on_time"
}
},
"model": {
"feedTimestamp": "2026-04-27T11:22:38.000Z",
"builtAt": "2026-08-07T12:36:57.044Z",
"delayBasis": "departure",
"tripObservations": 933,
"driftObservations": 15451,
"minSamples": 30
}
} Errors
{ "error": "request_failed", … } `toleranceSeconds` outside 0–3600, more than 10 `thresholdSeconds`, or an unknown `delayBasis`.
{ "error": "request_failed", … } No such trip, or `stopId`/`stopSequence` matches no *remaining* call — the message names the trip's actual next stop.
{ "error": "request_failed", … } The trip exists but reports no delay at any call, so there is nothing to anchor to.