railworks · OpenAPI 3.1

Irish Rail engineering works API

railworks reads Iarnród Éireann's planned engineering works page every 6 hours and turns it into structured, queryable data: which days, which stations, what happens there, and how that changed over time. It's read-only, unauthenticated and cacheable.

railworks is not affiliated with or endorsed by Iarnród Éireann. The data is machine-read from their page and may be wrong: check the source before travelling.

Quick start

Subscribe a calendar to one station. One all-day event per affected day, e.g. "No trains at Woodbrook":

webcal://railworks.paddez.com/v1/feeds/notices.ics?station=WBROK

Ask whether a station is affected, and when:

curl https://railworks.paddez.com/v1/stations/WBROK/days

List what's planned (today to 90 days ahead by default):

curl "https://railworks.paddez.com/v2/notices?line=dart"

Follow every change without missing any: see Polling for changes.

Client libraries

Python: pyrailworks (PyPI) has sync and async clients, Pydantic models, RFC 9457 errors as exceptions, and cursor handling for the change log:

pip install pyrailworks
from pyrailworks import RailworksClient

with RailworksClient() as client:
    for day in client.get_station_days("WBROK").days:
        print(day.date, day.effect.value)      # 2026-10-03 no_service

Every endpoint below has a Python example. Anything else can use the OpenAPI document with a client generator.

Concepts

Term Meaning
Works Notice One dated block of engineering works, as published under one Service Group: a set of days, the published text, and what it does to the service. Identified across page updates by a stable id.
Service Group A heading on the works page ("DART Services", "Rosslare Services") under which notices are published.
Line The stations one Service Group's trains serve, in order along its branches, named after the group (dart, northern-commuters, …).
Build One published version of the works page, identified by when upstream built it (built_at).
Revision A change to a notice's published text between builds, identity unchanged. A trim is a revision that drops days that have passed; an extension is one that adds days and keeps every day still to come.
Works Segment A track section where works take place ("between Dromod and Sligo"), expanded to every station in between.
Effect A station- or section-level consequence: no_service, station_closed, bus_replacement, reduced_service, other.
Service Alteration A change to one train (cancelled, bus-replaced, terminating short, …).
Station Day One date a station is affected, with the strongest effect there. The same closure published under DART and Rosslare is one Station Day.
Expired / Withdrawn A notice leaves the page when its days pass (expired, the normal end of life) or while it still had days to come (withdrawn). Nothing is ever deleted.

How notices are read

Polling for changes

/v1/changes is an append-only log of created, revised (with trim or extended), expired, withdrawn and extracted events behind an opaque cursor. Start with ?since= once, then always pass the previous response's next:

cursor=$(curl -s "https://railworks.paddez.com/v1/changes?since=2026-09-01T00:00:00Z" | jq -r .next)
# later, and forever after:
curl -s "https://railworks.paddez.com/v1/changes?after=$cursor" | jq '.events[], .next'

Each event names a notice_id: fetch /v2/notices/{id} for its current state. Treat the cursor as opaque. /v2/changes is the same log with the same cursors.

Compatibility and versioning

/v1 only changes additively: new endpoints, new optional query parameters, new response fields, and new enum values. Within /v1, railworks never removes, renames or retypes a field, makes an optional parameter required, or changes what a value means. So that it keeps working:

A change that can't be additive becomes /v2, served alongside /v1 for an overlap period. Anything being retired is marked with Deprecation (RFC 9745) and Sunset (RFC 8594) response headers first. Every change to this document is checked for breaking changes, and pyrailworks' test suite runs against every change to the service.

/v2 and the /v1 Sunset. /v2 was added on 8 November 2026, because railworks no longer republishes Iarnród Éireann's text (ADR 0003). The /v1 routes that serve it are deprecated from that date and gone on 1 December 2026 (00:00 in Dublin, which is 2026-12-01T00:00:00Z):

Retired /v1 route Use instead
/v1/notices /v2/notices
/v1/notices/{id} /v2/notices/{id}
/v1/notices/{id}/raw /v2/notices/{id} (revision history, without the HTML)
/v1/health /v2/health

Until the Sunset they answer as before, with Deprecation: @1794096000 (8 November 2026), Sunset: Tue, 01 Dec 2026 00:00:00 GMT, and a Link to the same request under /v2 (rel="successor-version") and to these notes (rel="deprecation"). From the Sunset on they answer 410 Gone with a problem document naming the successor, and the same headers.

Everything else in /v1 stays, unchanged and with no Deprecation header: /v1/changes, /v1/stations, /v1/stations/{code}, /v1/stations/{code}/days, /v1/openapi.yaml and, for good, /v1/feeds/notices.ics, since calendar subscriptions can't be moved (their text has been railworks' own since before /v2). Each also has a /v2 twin that answers the same.

Migrating to /v2

Swap the /v1/ prefix for /v2/ everywhere; ids, cursors, query parameters, caching and errors are the same. Then, for notices and health:

Freshness, caching and errors

Reference

Notices

Works Notices, their history, and the change log.

GET /v1/changes Follow the change log

Events after an opaque cursor, oldest first. Pass after (a previous next), or since for a first call only. Each event is returned exactly once when you follow next; an empty page means you're up to date (keep the same next).

GET /v1/notices List Works Notices

Deprecated: gone on 1 December 2026 (Sunset 2026-12-01T00:00:00Z). It serves Iarnród Éireann's published text, which railworks no longer republishes (ADR 0003). Use /v2/notices instead (see Migrating to /v2). Until then its responses carry Deprecation, Sunset and a successor-version link; from the Sunset it answers 410 Gone.

Works Notices with any day in the window, unmerged across Service Groups (the same closure published under DART and Rosslare is two notices; use Station Days for one answer per date). Lists leave out day_sections, detail, alterations and revisions: fetch a single notice for those.

GET /v1/notices/{id} Get one Works Notice

Deprecated: gone on 1 December 2026 (Sunset 2026-12-01T00:00:00Z). It serves Iarnród Éireann's published text, which railworks no longer republishes (ADR 0003). Use /v2/notices/{id} instead (see Migrating to /v2). Until then its responses carry Deprecation, Sunset and a successor-version link; from the Sunset it answers 410 Gone.

The notice in full, with its Day Sections, text, per-train Service Alterations and revision history. Works for any status.

GET /v1/notices/{id}/raw Get a notice's upstream HTML

Deprecated: gone on 1 December 2026 (Sunset 2026-12-01T00:00:00Z). It serves Iarnród Éireann's published text, which railworks no longer republishes (ADR 0003). Use /v2/notices/{id} instead (see Migrating to /v2). Until then its responses carry Deprecation, Sunset and a successor-version link; from the Sunset it answers 410 Gone. /v2 has no equivalent of the HTML itself: /v2/notices/{id} has the revision history without it.

The verbatim upstream HTML of every revision, oldest first, so you can check railworks' reading against the source.

GET /v2/changes Follow the change log (v2)

The same change log as /v1/changes (its events carry no upstream text), with the same cursors: a cursor from either works on both. Each event names a notice_id: fetch /v2/notices/{id} for its current state.

GET /v2/notices List Works Notices (v2)

Works Notices with any day in the window, as /v1/notices lists them but without Iarnród Éireann's text: each notice has railworks' own title and summary instead of its published heading, and no quotes. Unmerged across Service Groups (use Station Days for one answer per date). Lists leave out day_sections, alterations and revisions: fetch a single notice for those.

GET /v2/notices/{id} Get one Works Notice (v2)

The notice in full: its title and summary, Day Sections (days only), Works Segments, Effects, per-train Service Alterations and revision history, with every place named from railworks' station list. Works for any status. It replaces both /v1/notices/{id} and /v1/notices/{id}/raw: the upstream HTML isn't served.

Stations

Stations, their Lines, and Station Days.

GET /v1/stations List stations

Every Irish Rail station served by timetabled trains, with its Lines and position on each. Refreshed daily from the TFI GTFS feed and Irish Rail's station list.

GET /v1/stations/{code} Get one station

Alias codes (other Irish Rail codes for the same station, e.g. ADAMF) redirect to the canonical code.

GET /v1/stations/{code}/days Is this station affected, and when?

One Station Day per affected date in the window, deduplicated across Service Groups, with the strongest effect at the station (no_service > station_closed > bus_replacement > reduced_service > other), any stated time scope, and the notices behind it. A station reached only by a works segment gets other. Withdrawn notices don't count.

GET /v2/stations List stations (v2)

The same as /v1/stations.

GET /v2/stations/{code} Get one station (v2)

The same as /v1/stations/{code}. Alias codes redirect to /v2/stations/{canonical code}.

GET /v2/stations/{code}/days Is this station affected, and when? (v2)

The same Station Days as /v1/stations/{code}/days; alias codes redirect within /v2.

Feeds

Calendar (ICS) subscriptions.

GET /v1/feeds/notices.ics Calendar feed

An iCalendar (RFC 5545) feed to subscribe to. All events are all-day; a stated time of day goes in the title.

Text. Titles and descriptions are railworks' own wording, generated deterministically from the structured facts: the Service Group, the days, the Works Segments' ends, the Effects, times, and the Service Alterations counted by change type with a few examples ("Cancelled (2): 16:05 Dublin Connolly–Sligo; 17:05 Sligo–Dublin Connolly"). Station names come from railworks' station list. No heading, detail or quote from Iarnród Éireann's text is copied. A notice event's description lists those facts; a station event's lists each notice behind the day by its title and link. Every description ends with "Source: Iarnród Éireann (https://www.irishrail.ie/)", a disclaimer, and the link to the railworks notice or station resource. Event text isn't part of the JSON contract and may be reworded.

UIDs are stable across rewording and trims (days that have passed stay as history), and SEQUENCE rises with each change. Withdrawn events become STATUS:CANCELLED with a "Cancelled: " title and leave the feed a week after their dates. Expired events stay as ordinary past events. The feed covers 90 days back (from overrides) and asks clients to refresh every 6 hours. The calendar is named after the filters ("Rail works · Woodbrook · DART").

GET /v2/feeds/notices.ics Calendar feed (v2)

The same calendar as /v1/feeds/notices.ics, byte for byte: calendar text was already railworks' own wording. Both stay; /v1/feeds/notices.ics isn't retired, because subscriptions can't be moved.

Service

Health and this documentation.

GET / Home page

Subscribe to a station's calendar, see its upcoming affected days, and what's planned in the next four weeks. Built on this API.

GET /docs This documentation, rendered

GET /v1/health Health and things needing a human

Deprecated: gone on 1 December 2026 (Sunset 2026-12-01T00:00:00Z). It serves Iarnród Éireann's published text, which railworks no longer republishes (ADR 0003). Use /v2/health instead (see Migrating to /v2). Until then its responses carry Deprecation, Sunset and a successor-version link; from the Sunset it answers 410 Gone. (Its notices' heading is the published heading; /v2/health names them by title.)

Freshness, the last failure, the extraction backlog, and anything railworks couldn't resolve by itself.

GET /v1/openapi.yaml This document

GET /v2/health Health and things needing a human (v2)

The same as /v1/health, except that each listed notice is named by its generated title, not its published heading. The container healthcheck (railworks healthcheck) uses this route.

GET /v2/openapi.yaml This document (v2)

The same document as /v1/openapi.yaml, which describes both versions.

Site

Files for browsers, crawlers and tools.

GET /favicon.svg Favicon

The site icon.

GET /llms.txt LLM summary

A plain-Markdown summary of the API for AI tools (llmstxt.org).

GET /og.png Social card

The 1200×630 image shown when the site is shared.

GET /robots.txt Robots rules

Lets crawlers index the pages and keeps them off the API.

GET /sitemap.xml Sitemap

The pages, with the home page's last change.