Realtime/SL

Forecast a live trip

A delay forecast for every remaining call of a trip that is in the feed.

GET /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

ParameterTypeDefaultDescription
tripIdrequiredpathstring

A trip id present in the current feed. Same ids as list trip updates.

Query

ParameterTypeDefaultDescription
stopIdstring

Narrow to the remaining call at this stop.

stopSequencenumber

Narrow to the remaining call at this stop sequence.

delayBasisenumdeparture

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.

departurearrival

toleranceSecondsnumber60

The half-width of the on-time band, 0–3600. A delay within ±this counts as on_time; outside it is early or late. Widen it to match your own definition of punctual.

thresholdSecondsstring

Comma-separated delays, at most 10, to report an exceedance probability for. 300,600 answers “how likely is this more than 5 minutes late, and more than 10?” Adds an exceedance array to the forecast.

limitinteger50

Page size, 1–1000. Values outside that range are a 400 rather than a silent clamp.

offsetinteger0

Rows to skip. Compare against meta.total to know when you have them all.

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.

GET /api/punctuality/trips/14010000701096422?limit=2
curl "$API_BASE/api/punctuality/trips/14010000701096422?limit=2"
200 · application/json
{
  "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
  }
}
Captured from a local instance — this endpoint is not on the public API.

Errors

400
{ "error": "request_failed", … }

`toleranceSeconds` outside 0–3600, more than 10 `thresholdSeconds`, or an unknown `delayBasis`.

404
{ "error": "request_failed", … }

No such trip, or `stopId`/`stopSequence` matches no *remaining* call — the message names the trip's actual next stop.

422
{ "error": "request_failed", … }

The trip exists but reports no delay at any call, so there is nothing to anchor to.