Korea Data

Rail — GTFS

A GTFS dataset for South Korean rail, served as an API. Every subway and commuter-rail stop, route and departure in the country — 11 operators normalised into one feed, with English station names. 1 credit per row; the entire timetable, all 655,961 departures, is about $65.

EndpointReturnsCredits
GET /transit/versionDataset version and row counts0
GET /transit/operatorsThe 11 operators and their route counts0
GET /transit/calendarService patterns and dated exceptions0
GET /transit/stopsStations, by name or all of them1 per row
GET /transit/stops/{stop_id}One station1
GET /transit/stops/{stop_id}/departuresDepartures from a station, in time order1 per row
GET /transit/routesRoutes, with their operator1 per row
GET /transit/routes/{route_id}/tripsTrains on a route1 per row

Authentication, paging, limits and error codes are the same everywhere — conventions.

It is GTFS, and that is checked by other people's tools

Entities, field names and conventions are GTFS: stops, routes, trips, stop_times, calendar and calendar_dates, with parent_station for interchanges and times that run past 24:00. The feed was put through two independently written validators and a routing engine when this dataset was built, in August 2026 — results below.

CheckResult
MobilityData gtfs-validator (Java)0 errors
Interline transitland-lib (Go)0 errors, 0 warnings
OpenTripPlannerBuilds a graph and returns real itineraries, including transfers

Those three are run by hand, not by the build. Every build does run our own checks — a round-trip against the source and a coverage comparison — but if you need a current validator report for a specific feed version, ask and we will run one.

Two validators rather than one because they are written by different people and do not check the same things. transitland-lib is what found that interchange stations were not linked, which would have made transfers impossible.

Korea publishes this data, but each operator publishes it differently — different headers, different separators, different ways of writing "weekday". A nationwide Korean GTFS does exist: the Korea Transport Institute released one in 2023, built from March 2021 timetables as a pilot, obtained by application, and described by its own publisher as internal analysis material whose ids are not standardised. What has not existed is a current feed you can fetch without asking, with stable ids and English names. That is what this is.

Three calls are free

Services that use this data pull it once and serve from their own store. That only works if you can tell whether anything changed — otherwise you either re-pull blindly or go stale. So checking costs nothing.

curl -H "Authorization: Bearer $KCID_API_KEY" "https://notonlystock.com/korea/api/v0/transit/version"

{
  "stops":      { "version": "2026.08", "rows": 1120 },
  "routes":     { "version": "2026.08", "rows": 32 },
  "trips":      { "version": "2026.08", "rows": 23384 },
  "stop_times": { "version": "2026.08", "rows": 655961 }
}

GET /transit/operators is also free — it lists the 11 agencies and how many routes each runs, so you can see what is in here before spending anything.

GET /transit/calendar is free too, and you need it: it returns the service patterns and every dated exception. A timetable that cannot tell you which days a train runs is not a timetable, so this is catalogue, not product.

Everything else is 1 credit per row

Same shape as /companies/search: cursor paging and you are charged for rows returned, never for the call. An empty page costs nothing. limit defaults to 100 and goes up to 1,000 — note that is ten times the company-search default, so a call with no limit bills for 100 rows.

curl -H "Authorization: Bearer $KCID_API_KEY" "https://notonlystock.com/korea/api/v0/transit/stops?limit=2"

{
  "rows": [
    { "stop_id": "…", "stop_name": "Yongsan", "stop_name_ko": "용산역",
      "stop_lat": 37.52989, "stop_lon": 126.964775, "route_name": "경부선",
      "location_type": 0, "parent_station": "…" }
  ],
  "next_cursor": "2",
  "credits_charged": 2,
  "credits_remaining": 4998
}
EndpointReturns
/transit/stopsEvery stop, with WGS84 coordinates
/transit/stops/{id}One stop, as a one-row page — same envelope, rows holds one element
/transit/stops/{id}/departuresEvery departure from that stop, in time order
/transit/routesEvery route, with its operator
/transit/routes/{id}/tripsEvery train on that route

Pulling 655,961 rows means 656 calls, and that is the intended use. Paid calls are not rate-limited — your credit balance is the limit.

Errors

StatusCodeMeans
400bad_cursorNot a value we returned as next_cursor
402insufficient_creditsSays how many the page needed. Lower limit
404not_foundNo such stop, route or trip
503loadingA new timetable is mid-load

While a timetable loads, every paid /transit/* call returns 503 loading and the three free calls keep answering — so a service that polls /transit/version sees the switch instead of half-loaded rows. Nothing is charged. Every error code →

Public holidays are real dates, not weekday flags

Korean operators publish separate weekday, Saturday and holiday timetables — and they label them inconsistently. One line writes 공휴일, the line next to it writes 일요일 for the same thing; four operators publish no Saturday timetable at all because their holiday timetable covers the whole weekend. Copied literally, that produces a feed where Seoul's subway does not run on Sundays and Korail does not run on Saturdays.

So the weekday flags are reconciled per operator, and every public holiday is written out as an actual date in calendar_dates.txt — including the ones that fall midweek, when the weekday timetable stops and the holiday timetable starts. Lunar New Year and Chuseok are handled separately again, because at least one operator publishes a distinct timetable for those.

The holiday list is a reviewed table, not a computed one, and it covers 2026. Two published holiday calendars were cross-checked to build it — both of them were wrong: both omitted the 2026 local-election day, which is a statutory holiday, and one included Constitution Day, which has not been one since 2008.

Three name fields, because display and matching are different jobs

The source is not consistent about station names in English. Some carry a parenthesised subtitle and some do not; the space before the bracket appears in 72 names and is missing in 26 others, in the same file. Mokdong and Mok-dong are harmless to a reader and fatal to a join.

FieldIs
stop_nameAs published
stop_name_shortSubtitle removed, spacing normalised — for display
stop_name_normSubtitle, spaces, hyphens and case stripped — for joining

q matches the normalised key too, so you can send a name in whichever form you already have it and still land on the right station.

GTFS conventions you should know about

Times are HH:MM:SS and may exceed 24 hours: a train arriving at 1am the next day reads 25:00:00. That is the GTFS rule, and writing it as 01:00:00 would move that train to the start of the day.

A station served by two lines appears once per line, joined by a shared parent_station — ask a parent station for departures and you get every line's.

What is not in here

Trains whose source timetable is internally contradictory are left out rather than guessed at, and so are stops whose published coordinates are provably wrong. Holiday-only services cannot be expressed without a Korean holiday calendar, which the sources do not publish. The published row counts above are what actually shipped, not what the sources claimed.

Missing an endpoint or a field? Say what you were trying to do — it is read by the person who builds this.