National Railways Federation

Branch federation of the railway operating cooperatives

api.nrf.rof, version 1

Six endpoints, JSON, no key, no quota, no registration. Vehicle positions are not here; they belong to Podnik at api.podnik.rof and are documented by the transport federation. A client that wants a departure board joins the two.

Endpoints

api.nrf.rof/v1
Path Answers Cache
/v1/stations The 145 stations: identifier, name, sub-region, platforms, whether a gate stands there, step-free access, the fittings on the access page. 1 day
/v1/routes The 36 routes: number, name, group, the station sequence, length, end-to-end time, traction. 1 day
/v1/timetable Departures for a station and a date, or a whole route. Published times, platforms and the service identifier that joins to the Podnik feed. 1 hour
/v1/platforms Platform allocation for a station and a day, which moves more often than the timetable does. 5 min
/v1/changes What moved: time changes, platform changes, cancellations, with the cooperative that decided each and when. 1 min
/v1/reports Rider reports and the answers to them, published together under Article 61(2). POST files one. 5 min

Shapes

GET /v1/timetable?stop=LIN-CTR&from=2026-08-02T08:00

{
  "stop": "LIN-CTR",
  "generated": "2026-08-02T08:14:02+02:00",
  "departures": [
    {
      "service": "S-35-0812",
      "route": 35,
      "group": "northern-coast",
      "destination": "TOR-001",
      "published": "2026-08-02T08:20:00+02:00",
      "platform": 2,
      "calls": [
        {
          "station": "NUR-001",
          "published": "2026-08-02T08:41:00+02:00"
        },
        {
          "station": "ORT-001",
          "published": "2026-08-02T09:22:00+02:00"
        },
        {
          "station": "TOR-001",
          "published": "2026-08-02T10:40:00+02:00"
        }
      ],
      "vehicle_hint": "SED-R9"
    }
  ]
}
GET /v1/changes?since=2026-07-30T00:00

{
  "changes": [
    {
      "id": "C-2026-07-30-0114",
      "service": "S-13-1440",
      "route": 13,
      "kind": "platform",
      "was": 2,
      "now": 4,
      "at": "VES-001",
      "decided_by": "southern/duty-office",
      "decided_at": "2026-07-30T11:26:00+02:00",
      "reason": "engineering possession on platform 2 until 12 August"
    }
  ]
}
POST /v1/reports
Content-Type: application/json

{
  "kind": "station",
  "station": "SOR-001",
  "vehicle": null,
  "text": "The lift in the north subway is out of use.",
  "reply_to": null
}

201 Created

{
  "id": "R-2026-08-02-0338",
  "group": "south-east-coast",
  "acknowledge_by": "2026-08-05",
  "published": "https://nrf.rof/reports/R-2026-08-02-0338"
}

Rules of the road

  • Conditional requests

    Every response comes with an ETag and Last-Modified. A conditional request that returns 304 costs nothing and is not counted against anything, because nothing is counted.

  • Polling

    The change feed is regenerated once a minute and platforms once every five. Polling faster returns the same document with the same ETag; there is no rate limit and no benefit.

  • Errors

    Problem details in RFC 9457, with type, title and detail in the language of the Accept-Language header where the message has a translation.

  • Bulk

    The whole timetable for a period is one gzip at /v1/timetable/bulk, published on the day the period opens. A board in a shelter runs off that and the change feed alone.

Joining the two

A published time comes from here. Where a vehicle actually is comes from Podnik. The service identifier in the timetable is the join: pass it to the Podnik journeys endpoint and the reply returns the vehicle, its position, its estimate and the margin on that estimate.

Podnik documents its own interface at api.podnik.rof and sets its own terms; the federation neither proxies it nor mirrors it. board.py shows the join in about eighty lines.