{"openapi":"3.1.0","info":{"title":"European Journey Planner API","version":"1.0.0","description":"A thin, stable, versioned facade (`/v1`) over MOTIS v2 for pan-European\npublic-transport journey planning. Times are RFC 3339 with explicit offset\nin the local time of the relevant place; durations are integer seconds; IDs\nare opaque namespaced strings (do not parse them); errors are RFC 7807\n`application/problem+json`. \"No itinerary found\" is a 200 with an empty\n`itineraries` array plus a `diagnostics` hint, not an error.\n","license":{"name":"See /v1/attributions"}},"servers":[{"url":"https://pt-api.internal.example","description":"internal phase (LAN/WireGuard only)"}],"tags":[{"name":"routing"},{"name":"geocode"},{"name":"timetable"},{"name":"map"},{"name":"operational"}],"paths":{"/v1/plan":{"get":{"tags":["routing"],"operationId":"plan","summary":"Door-to-door journey planning (the core).","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string"},"description":"`lat,lon` or `stop:<opaque-id>`"},{"name":"to","in":"query","required":true,"schema":{"type":"string"},"description":"`lat,lon` or `stop:<opaque-id>`"},{"name":"time","in":"query","required":false,"schema":{"type":"string"},"description":"RFC 3339 with offset; default now"},{"name":"arrive_by","in":"query","required":false,"schema":{"type":"boolean","default":false}},{"name":"results","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":10,"default":5}},{"name":"max_transfers","in":"query","required":false,"schema":{"type":"integer","minimum":0}},{"name":"modes","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated list of TRANSIT modes to INCLUDE. `walk` is always implied (access/egress). Absent => ALL transit modes (default, no filter). Vocabulary: bus, rail, tram, metro, trolleybus, ferry, funicular, aerial_lift, coach, air (plus `walk`, and legacy `transit` = all). Unknown tokens -> 400. Example: `modes=rail,ferry`.\n"},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"schema":{"type":"string"}},{"name":"snap_radius_m","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50000},"description":"Explicit opt-in override for the legacy coordinate snap circle. By default there is NO radius: a coordinate endpoint is resolved to the single nearest served station (fast) instead of scanning every stop in a circle. Only set this if you specifically want multi-stop circle snapping.\n"}],"responses":{"200":{"description":"Journey planning result (possibly empty).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanResponse"}}}},"400":{"$ref":"#/components/responses/Problem"}}}},"/v1/geocode":{"get":{"tags":["geocode"],"operationId":"geocode","summary":"Free-text place search / autocomplete.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}},{"name":"near","in":"query","required":false,"schema":{"type":"string"},"description":"`lat,lon` bias"},{"name":"types","in":"query","required":false,"schema":{"type":"string"},"description":"comma list of stop,address,poi"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":8}},{"name":"lang","in":"query","required":false,"schema":{"type":"string"}},{"name":"include_unserved","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Debug/ops flag. By default `stop` results with no service in the live graph are dropped (only boardable stations are offered). Set to 1 to keep them.\n"}],"responses":{"200":{"description":"Ranked matches.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeocodeResponse"}}}},"400":{"$ref":"#/components/responses/Problem"}}}},"/v1/reverse":{"get":{"tags":["geocode"],"operationId":"reverse","summary":"Coordinates to nearest named place and closest stops.","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"lang","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Nearest address and closest stops.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReverseResponse"}}}},"400":{"$ref":"#/components/responses/Problem"}}}},"/v1/departures":{"get":{"tags":["timetable"],"operationId":"departures","summary":"Departure / arrival board for a whole station (canonical cluster).","parameters":[{"name":"stop","in":"query","required":true,"schema":{"type":"string"},"description":"opaque stop id (bare or canonical). The board aggregates the WHOLE canonical cluster (all member platforms/feed nodes)."},{"name":"time","in":"query","required":false,"schema":{"type":"string"},"description":"ISO8601; default now. Events are sorted ascending."},{"name":"direction","in":"query","required":false,"schema":{"type":"string","enum":["departures","arrivals"],"default":"departures"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20}},{"name":"modes","in":"query","required":false,"schema":{"type":"string"},"description":"Filter the board to these transit modes (same vocabulary as /v1/plan: bus, rail, tram, metro, trolleybus, ferry, funicular, aerial_lift, coach, air). Absent => all modes. Example: `modes=tram,metro`.\n"},{"name":"lang","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Board entries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeparturesResponse"}}}},"400":{"$ref":"#/components/responses/Problem"}}}},"/v1/trip":{"get":{"tags":["timetable"],"operationId":"trip","summary":"One vehicle run in full (stop sequence + geometry).","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"},"description":"opaque trip_id from a board/plan event (round-trips verbatim)."},{"name":"time","in":"query","required":false,"schema":{"type":"string"},"description":"ISO date of the service day (optional; the service day is encoded in the id)."}],"responses":{"200":{"description":"Full trip.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripResponse"}}}},"400":{"$ref":"#/components/responses/Problem"},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/line-times":{"get":{"tags":["timetable"],"operationId":"lineTimes","summary":"Times for a selected LINE at a stop (a board narrowed to one line).","description":"Identify the line with ONE of `trip_id` (from a board event), `route_id` (from a network-map line click), or `display_name` (+`feed_id`/`country`, best-effort). `stop` is the focus (cluster-aware); it defaults to the trip's first stop when only `trip_id` is given. Returns the line's runs on `date` (default today), split by direction; each run links into /v1/trip.\n","parameters":[{"name":"stop","in":"query","required":false,"schema":{"type":"string"},"description":"focus stop id (cluster-aware); required unless derivable from trip_id"},{"name":"trip_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"route_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"display_name","in":"query","required":false,"schema":{"type":"string"}},{"name":"feed_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"country","in":"query","required":false,"schema":{"type":"string"}},{"name":"date","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD service date; default today"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":200}}],"responses":{"200":{"description":"The line's runs at the stop, direction-split.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineTimesResponse"}}}},"400":{"$ref":"#/components/responses/Problem"},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/nearest":{"get":{"tags":["map"],"operationId":"nearest","summary":"Nearest stations to a point (canonical-deduped, boardable).","description":"Nearest served stations to a coordinate, one entry per canonical station cluster, ordered by straight-line distance ascending. Pure served-index lookup (no engine call). Each `id` works verbatim in `/v1/departures`, `/v1/line-times` and as a plan `from=`/`to=` endpoint. Empty result is a 200 with an empty list; bad/missing coordinates are 400.\n","parameters":[{"name":"lat","in":"query","required":false,"schema":{"type":"number"}},{"name":"lon","in":"query","required":false,"schema":{"type":"number"}},{"name":"point","in":"query","required":false,"schema":{"type":"string"},"description":"`lat,lon` (alternative to lat/lon)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":25,"default":5}},{"name":"max_km","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":50,"default":5}},{"name":"modes","in":"query","required":false,"schema":{"type":"string"},"description":"comma list of the transit-mode vocabulary (bus,rail,tram,metro,…)"},{"name":"served_only","in":"query","required":false,"schema":{"type":"boolean","default":true}}],"responses":{"200":{"description":"Nearest stations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NearestResponse"}}}},"400":{"$ref":"#/components/responses/Problem"}}}},"/v1/stops":{"get":{"tags":["map"],"operationId":"stops","summary":"Stations by proximity or bbox (canonical-deduped).","parameters":[{"name":"near","in":"query","required":false,"schema":{"type":"string"},"description":"`lat,lon`"},{"name":"radius_m","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50000}},{"name":"bbox","in":"query","required":false,"schema":{"type":"string"},"description":"minLon,minLat,maxLon,maxLat"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"modes","in":"query","required":false,"schema":{"type":"string"}},{"name":"served_only","in":"query","required":false,"schema":{"type":"boolean","default":true}}],"responses":{"200":{"description":"Stops.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StopsResponse"}}}},"400":{"$ref":"#/components/responses/Problem"}}}},"/v1/status":{"get":{"tags":["operational"],"operationId":"status","summary":"Data & engine health (freshness view over the pipeline).","responses":{"200":{"description":"Status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/v1/status/operators":{"get":{"tags":["operational"],"operationId":"statusOperators","summary":"Per-operator timetable freshness and scrape schedule (backs /status).","description":"One row per operator (GTFS agency) in the graph data: stops, routes, trips, trips running in the next 7 days and in the planning window, timetable validity (service_start / service_end, days_left) and a status computed against today — ok | expiring (ends within `expiring_days`) | no_service (valid, but no trip in the next 7 days) | expired | unknown (no calendar). `at_risk` marks a timetable that ends before the next scheduled source check + 7 days. `feeds` carries provenance per feed (source_url, licence, when the file content last changed, quarantine reason); `units` the scrape cadence, last check, its result and the next check; `graph` the last planner rebuild and the earliest next one. On a row that is not ok (or is at_risk), `action` says what the daily freshness watchdog is doing about it — refreshing | update_pending (newer timetable fetched, live after the next rebuild) | superseded (another feed for the same network replaces this copy) | scheduled | no_new_timetable (publisher has released nothing newer) | checked (reviewed, e.g. seasonal) | removing | needs_review — and `watch` summarises the last check.\n","parameters":[{"name":"country","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated ISO codes, e.g. GR,BE"},{"name":"status","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated: ok, expiring, no_service, expired, unknown"}],"responses":{"200":{"description":"Operator freshness.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"The pipeline has not produced operator_status.json yet."}}}},"/v1/attributions":{"get":{"tags":["operational"],"operationId":"attributions","summary":"License compliance surface (structured NOTICE).","responses":{"200":{"description":"Attributions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttributionsResponse"}}}}}}},"/v1/limits":{"get":{"tags":["operational"],"operationId":"limits","summary":"Public rate-limit / quota tier table.","description":"The per-tier throttling numbers (requests/min, burst, max concurrent /v1/plan, daily cap), how to authenticate for a higher tier, and the meaning of the X-RateLimit-* / Retry-After headers. Every /v1 response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` when enforcement is on; 429 and 503 add `Retry-After`.\n","responses":{"200":{"description":"Tier table.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LimitsResponse"}}}},"429":{"description":"Too Many Requests (rate/daily allowance exhausted).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/tiles/{z}/{x}/{y}.mvt":{"get":{"tags":["map"],"operationId":"tile","summary":"Vector tile proxy (transit overlay + basemap from MOTIS).","parameters":[{"name":"z","in":"path","required":true,"schema":{"type":"integer"}},{"name":"x","in":"path","required":true,"schema":{"type":"integer"}},{"name":"y","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Mapbox vector tile.","content":{"application/vnd.mapbox-vector-tile":{"schema":{"type":"string","format":"binary"}}}}}}},"/tiles/network.pmtiles":{"get":{"tags":["map"],"operationId":"networkPmtiles","summary":"Network-map PMTiles archive (whole-European PT network, browsable pre-search).","description":"A single static PMTiles v3 vector-tile archive (~1 GB) with layers `routes` (LineString, per GTFS route) and `stops` (Point, per canonical station), zoom 0–14. Designed for HTTP **Range** reads: PMTiles clients (MapLibre `pmtiles://`, protomaps-leaflet) fetch a few ~100–300 KB byte ranges per viewport — send a `Range: bytes=start-end` header and you get `206 Partial Content` with `Content-Range`. `Accept-Ranges: bytes`, `ETag` (size+mtime) and `Cache-Control: public, max-age=3600` are set; `If-None-Match` yields `304`. Rebuilt nightly and served by path, so a new build is picked up without a gateway restart (the ETag changes with it). A range-less GET streams the whole archive (not needed by clients). Layer/property schema: routes { mode, display_name, color, country, feed_id, trips_per_day }; stops { name, modes (`|`-joined), departures, country }. Per-feature minzoom is baked in (z0–5 rail/ferry trunk + major hubs; z6–9 regional; z10+ full network).\n","parameters":[{"name":"Range","in":"header","required":false,"schema":{"type":"string"},"description":"e.g. `bytes=0-16383`. Omit for the full archive."}],"responses":{"206":{"description":"Partial content (a requested byte range).","headers":{"Content-Range":{"schema":{"type":"string"}},"Accept-Ranges":{"schema":{"type":"string"}},"ETag":{"schema":{"type":"string"}}},"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"200":{"description":"Full archive (range-less GET; streamed).","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"Not modified (matched If-None-Match)."},"404":{"description":"Archive not available (nightly build absent)."},"416":{"description":"Requested range not satisfiable."}}}},"/health":{"get":{"tags":["operational"],"operationId":"health","summary":"Load-balancer health check (unversioned, never authed).","responses":{"200":{"description":"ok","content":{"text/plain":{"schema":{"type":"string"}}}}}}}},"components":{"responses":{"Problem":{"description":"RFC 7807 problem.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"schemas":{"Problem":{"type":"object","required":["type","title","status"],"properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}}},"Coord":{"type":"object","required":["lat","lon"],"properties":{"lat":{"type":"number"},"lon":{"type":"number"}}},"StopEvent":{"type":"object","required":["scheduled","realtime"],"properties":{"scheduled":{"type":["string","null"]},"realtime":{"type":["string","null"],"description":"null until v2 RT overlay"},"platform":{"type":["string","null"]}}},"Endpoint":{"type":"object","required":["name","lat","lon"],"properties":{"name":{"type":["string","null"]},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]},"stop_id":{"type":"string","description":"opaque; do not parse"},"departure":{"$ref":"#/components/schemas/StopEvent"},"arrival":{"$ref":"#/components/schemas/StopEvent"}}},"Route":{"type":"object","properties":{"id":{"type":["string","null"]},"short_name":{"type":["string","null"],"description":"Raw route_short_name from the feed (may be an internal code, e.g. CZ CIS '766420')."},"long_name":{"type":["string","null"]},"display_name":{"type":["string","null"],"description":"Public-facing line designation to show the traveller (what is marked on the vehicle / in national timetables), e.g. \"420\", \"R12\", \"S8\". Derived from short_name with per-feed internal-code cleanup (CZ CIS six-digit -> line number); equals short_name where it is already public. Pair with `mode` (e.g. render \"Bus 420\"). Null only when the feed carries no usable name.\n"},"agency":{"type":["string","null"]},"headsign":{"type":["string","null"]}}},"Leg":{"type":"object","required":["mode","from","to","start","end","duration_s"],"properties":{"mode":{"type":"string","enum":["WALK","BIKE","CAR","RAIL","BUS","COACH","TRAM","SUBWAY","FERRY","AIRPLANE","FUNICULAR","AERIAL_LIFT","TRANSIT","OTHER"]},"from":{"$ref":"#/components/schemas/Endpoint"},"to":{"$ref":"#/components/schemas/Endpoint"},"start":{"type":["string","null"]},"end":{"type":["string","null"]},"duration_s":{"type":["integer","null"]},"distance_m":{"type":"integer"},"geometry":{"type":["string","null"],"description":"encoded polyline (present on WALK legs too)"},"role":{"type":"string","enum":["first_mile","last_mile","transfer","direct"],"description":"WALK legs only. first/last_mile = walk from/to the door; transfer = between transit legs; direct = a walk-only itinerary (no transit). Consecutive WALK legs are always merged into one."},"distance_kind":{"type":"string","enum":["walk","straight"],"description":"WALK legs: street-routed vs straight-line estimate"},"route":{"$ref":"#/components/schemas/Route"},"trip_id":{"type":["string","null"]},"intermediate_stops":{"type":"integer"}}},"Itinerary":{"type":"object","required":["start","end","duration_s","transfers","legs"],"properties":{"start":{"type":["string","null"]},"end":{"type":["string","null"]},"duration_s":{"type":["integer","null"]},"transfers":{"type":"integer"},"legs":{"type":"array","items":{"$ref":"#/components/schemas/Leg"}}}},"Cursors":{"type":"object","properties":{"earlier":{"type":["string","null"]},"later":{"type":["string","null"]}}},"PlanResponse":{"type":"object","required":["itineraries","cursors","attributions_url"],"properties":{"itineraries":{"type":"array","items":{"$ref":"#/components/schemas/Itinerary"}},"cursors":{"$ref":"#/components/schemas/Cursors"},"diagnostics":{"type":["string","null"],"description":"Set (non-null) only when itineraries is empty — a clear human reason, never a silent empty."},"resolved_from":{"$ref":"#/components/schemas/ResolvedFrom"},"attributions_url":{"type":"string"}}},"ResolvedFrom":{"type":"object","description":"Per-endpoint station substitution (fix #3). `from`/`to` are null when the endpoint was used as requested; otherwise they report the served station the planner actually departed from / arrived at, so the UI can show \"departing from <used_name>, <distance_m> m away\". On a street-routed graph the planner sends the RAW coordinates to the engine, which walks door<->stop itself, so a coordinate endpoint is used as-is and this stays null — the first_mile/last_mile WALK legs carry the real access path. It is only populated when the resolve-then-route FALLBACK actually substituted a stop (e.g. an unroutable node) and that itinerary is the best option.\n","properties":{"from":{"oneOf":[{"$ref":"#/components/schemas/ResolvedEndpoint"},{"type":"null"}]},"to":{"oneOf":[{"$ref":"#/components/schemas/ResolvedEndpoint"},{"type":"null"}]}}},"ResolvedEndpoint":{"type":"object","required":["requested","used_stop_id","used_name","distance_m"],"properties":{"requested":{"type":"string","description":"The original stop id or 'lat,lon' the client sent."},"used_stop_id":{"type":"string","description":"The served stop actually routed from/to (opaque)."},"used_name":{"type":["string","null"]},"distance_m":{"type":"integer","description":"Metres from the requested point to the used station."}}},"GeocodeMatch":{"type":"object","required":["type","name","lat","lon"],"properties":{"type":{"type":"string"},"name":{"type":["string","null"]},"id":{"type":"string"},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]},"country":{"type":["string","null"],"description":"ISO 3166-1 alpha-2. Null when the engine has no admin boundary for the point AND the stop comes from a feed whose pipeline unit has no country of its own — a pan-European coach network belongs to no country, so its stop ids imply which FEED shipped the stop, not where the stop is. Until 2026-08-26 such stops were labelled with the operator's domicile (\"DE\" for a FlixBus stop in Maastricht or Barcelona); an absent country replaces a confidently wrong one. Where the graph does know the real country, it is unchanged.\n"},"region":{"type":["string","null"],"description":"Coarsest sub-country admin area (kraj / Land / région / canton)."},"locality":{"type":["string","null"],"description":"Settlement the result sits in (municipality / city)."},"context":{"type":["string","null"],"description":"Ordered, human-readable admin context that makes same-named results differentiable, e.g. \"okres Blansko, Jihomoravský kraj, CZ\". Built from the OSM admin hierarchy (locality, district, region, country), omitting parts already visible in `name`. Within one response, any two results with the same `name` are guaranteed a distinct `context` (expanded with finer admin areas or coordinates if needed). Null only when no admin data is available.\n"},"disambiguator":{"type":["string","null"],"description":"Short (<=30 char), Latin-transliterated label — the most-specific admin unit + country, e.g. \"Zakynthos, GR\" vs \"Central Corfu Diapontia, GR\". Unlike `context` it never truncates off-screen on mobile, so show it as the compact secondary line to tell same-named results apart. Null only when no admin data is available.\n"},"score":{"type":["number","null"]},"modes":{"type":"array","items":{"type":"string"}},"departures":{"type":"integer","description":"Scheduled departures over the graph horizon. For a collapsed station this is the CLUSTER total (sum over all its nodes). Results are ranked by this."},"served":{"type":"boolean","description":"Always true for stop results in the default response (unserved stops are filtered out)."},"cluster_size":{"type":"integer","description":"Present when this stop result is a collapsed canonical station; the number of underlying feed nodes it represents (station identity, §3). name/id/coords point at the best-connected member."}}},"GeocodeResponse":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/GeocodeMatch"}}}},"ReverseResponse":{"type":"object","required":["address","stops"],"properties":{"address":{"oneOf":[{"$ref":"#/components/schemas/GeocodeMatch"},{"type":"null"}]},"stops":{"type":"array","items":{"$ref":"#/components/schemas/GeocodeMatch"}}}},"DepartureEntry":{"type":"object","required":["scheduled","realtime"],"properties":{"route_short_name":{"type":["string","null"],"description":"Raw route_short_name (may be an internal code)."},"display_name":{"type":["string","null"],"description":"Public-facing line designation (see Route.display_name). Pair with `mode`."},"headsign":{"type":["string","null"]},"mode":{"type":"string"},"scheduled":{"type":["string","null"]},"realtime":{"type":["string","null"]},"platform":{"type":["string","null"]},"trip_id":{"type":["string","null"]}}},"DeparturesResponse":{"type":"object","required":["stop","direction","events"],"properties":{"stop":{"type":"object","required":["id","name","lat","lon"],"properties":{"id":{"type":["string","null"]},"name":{"type":["string","null"]},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]}}},"direction":{"type":"string","enum":["departures","arrivals"]},"events":{"type":"array","items":{"$ref":"#/components/schemas/BoardEvent"}}}},"BoardEvent":{"type":"object","required":["time","mode","trip_id"],"properties":{"time":{"type":["string","null"],"description":"ISO8601 local-with-offset of the stop."},"mode":{"type":"string","description":"10-mode public vocabulary (BUS/RAIL/TRAM/SUBWAY/FERRY/COACH/…)."},"route_name":{"type":["string","null"]},"headsign":{"type":["string","null"]},"trip_id":{"type":["string","null"],"description":"Opaque; usable verbatim in /v1/trip."},"operator":{"type":["string","null"]},"platform":{"type":["string","null"]}}},"TripStop":{"type":"object","required":["id","name","lat","lon"],"properties":{"id":{"type":["string","null"]},"name":{"type":["string","null"]},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]},"arrival":{"type":["string","null"]},"departure":{"type":["string","null"]}}},"TripResponse":{"type":"object","required":["trip_id","stops"],"properties":{"trip_id":{"type":["string","null"]},"route_name":{"type":["string","null"]},"mode":{"type":"string"},"headsign":{"type":["string","null"]},"operator":{"type":["string","null"]},"stops":{"type":"array","items":{"$ref":"#/components/schemas/TripStop"}},"polyline":{"type":["string","null"],"description":"Encoded polyline5 or null."}}},"LineTimesResponse":{"type":"object","required":["line","stop","date","directions"],"properties":{"line":{"type":"object","properties":{"route_id":{"type":["string","null"]},"display_name":{"type":["string","null"]},"mode":{"type":["string","null"]},"operator":{"type":["string","null"]},"feed_id":{"type":["string","null"]},"country":{"type":["string","null"]}}},"stop":{"type":"object","properties":{"id":{"type":["string","null"]},"name":{"type":["string","null"]},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]}}},"date":{"type":"string","description":"YYYY-MM-DD service date."},"directions":{"type":"array","items":{"type":"object","properties":{"direction_id":{"type":["integer","null"]},"headsign":{"type":["string","null"]},"runs":{"type":"array","items":{"type":"object","required":["trip_id"],"properties":{"trip_id":{"type":["string","null"],"description":"Opaque; use in /v1/trip for the full stop-by-stop times."},"headsign":{"type":["string","null"]},"stop_time":{"type":["string","null"],"description":"ISO8601 local-with-offset AT the focus stop."},"first_stop":{"type":"object","properties":{"id":{"type":["string","null"]},"name":{"type":["string","null"]}}},"last_stop":{"type":"object","properties":{"id":{"type":["string","null"]},"name":{"type":["string","null"]}}}}}}}}}}},"Stop":{"type":"object","required":["name","lat","lon"],"properties":{"name":{"type":["string","null"]},"stop_id":{"type":["string","null"],"description":"opaque; usable in /v1/departures, /v1/line-times and as a plan endpoint"},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]},"modes":{"type":"array","items":{"type":"string"}},"distance_m":{"type":["integer","null"]},"served":{"type":["boolean","null"]},"canonical_station_id":{"type":["string","null"]},"departures_per_day":{"type":["integer","null"]}}},"StopsResponse":{"type":"object","required":["stops"],"properties":{"stops":{"type":"array","items":{"$ref":"#/components/schemas/Stop"}}}},"NearestStop":{"type":"object","required":["id","lat","lon","distance_m"],"properties":{"id":{"type":"string","description":"opaque bare id; works in /v1/departures, /v1/line-times, plan from=/to="},"name":{"type":["string","null"]},"lat":{"type":"number"},"lon":{"type":"number"},"distance_m":{"type":"integer","description":"straight-line metres"},"modes":{"type":"array","items":{"type":"string"}},"departures_per_day":{"type":["integer","null"]},"canonical_station_id":{"type":["string","null"]},"country":{"type":["string","null"],"description":"Dataset tag of the pipeline unit that ships the stop, not an ISO code (e.g. \"germany\", \"flixbus\"). Null when unknown. Treat it as provenance; use /v1/geocode for an ISO country.\n"},"locality":{"type":["string","null"]},"disambiguator":{"type":["string","null"]},"served":{"type":"boolean"},"walk_distance_m":{"type":["integer","null"],"description":"street-network walk if available, else straight-line estimate"},"walk_duration_s":{"type":["integer","null"]},"distance_kind":{"type":"string","enum":["walk","straight"]}}},"NearestResponse":{"type":"object","required":["query","stops"],"properties":{"query":{"type":"object","properties":{"lat":{"type":"number"},"lon":{"type":"number"}}},"stops":{"type":"array","items":{"$ref":"#/components/schemas/NearestStop"}}}},"StatusCountry":{"type":"object","properties":{"code":{"type":["string","null"]},"name":{"type":["string","null"]},"kind":{"description":"What this row IS. \"country\" for every entry of `countries` (its `code` is an ISO 3166-1 alpha-2, plus XK for Kosovo); \"operator\" for every entry of `operators` (its `code` is non-ISO and longer than two characters, e.g. EU-FLIXBUS). Added 2026-08-26; absent on responses from an older gateway, where it should be read as \"country\". An operator row has NO country and none is invented for it: there is no `country` field on these rows, and no placeholder code such as \"EU\" or \"ZZ\" appears anywhere in this response.\n","type":"string","enum":["country","operator"],"default":"country"},"feeds":{"type":["integer","null"]},"stops":{"type":["integer","null"]},"trips":{"type":["integer","null"]},"operators":{"description":"Transport operators (GTFS agencies) in this unit's canonical feeds. Added 2026-09-27.\n","type":["integer","null"]},"service_start":{"type":["string","null"]},"horizon_end":{"type":["string","null"]},"newest_snapshot":{"type":["string","null"]},"served":{"type":"boolean","description":"loaded in the live engine graph"}}},"StatusResponse":{"type":"object","required":["ok","engine","totals","countries"],"properties":{"ok":{"description":"True when pipeline metrics are loaded AND the served graph is younger than the stale threshold (48 h by default). False is honest, not an outage: routing keeps working on the older graph.\n","type":"boolean"},"engine":{"type":"object","required":["name","version"],"properties":{"name":{"type":"string"},"version":{"type":"string"}}},"graph_built_at":{"description":"When the graph the engine is SERVING was built (its tt.bin), read at request time, so a swap or rollback shows immediately. Before 2026-09-27 this was the pipeline metrics file's mtime, which advances on refreshes whose build then fails.\n","type":["string","null"]},"newest_snapshot":{"type":["string","null"]},"oldest_snapshot":{"type":["string","null"]},"served_countries":{"description":"ISO codes actually loaded in the live graph (null if all pipeline countries are served). Countries only: operator units are never listed here, however much data they contribute.\n","type":["array","null"],"items":{"type":"string"}},"totals":{"type":"object","properties":{"countries":{"description":"Number of countries SERVED, i.e. the length of `served_countries`. Until 2026-09-27 this was the pipeline's country-unit count, which includes countries whose feeds carry no service (now `countries_in_pipeline`).\n","type":["integer","null"]},"countries_in_pipeline":{"description":"Country units known to the pipeline, served or not.","type":["integer","null"]},"operators":{"description":"Transport operators (GTFS agencies) across everything served. Until 2026-09-27 this held the number of non-country operator units (now `operator_networks`).\n","type":["integer","null"]},"operator_networks":{"description":"Number of entries in `operators[]` (pan-European networks).","type":["integer","null"]},"feeds":{"description":"Feeds merged into the graph (canonical GTFS). Until 2026-09-27 this also counted licence-quarantined and conversion-pending feeds (now `feeds_in_pipeline`).\n","type":["integer","null"]},"feeds_in_pipeline":{"type":["integer","null"]},"stops":{"type":["integer","null"]},"trips":{"type":["integer","null"]}}},"countries":{"description":"One row per country, keyed uniquely by ISO `code`. Until 2026-08-26 this array also carried operator units under their domicile ISO, which put two rows with code \"DE\" in the response; they now live in `operators`.\n","type":"array","items":{"$ref":"#/components/schemas/StatusCountry"}},"operators":{"description":"Operator/network units that are not a country — a pan-European coach network covers many countries and belongs to none. Same row shape as `countries`; `code` is non-ISO by construction. Their stops/trips are included in `totals`. These units have no country at all in the pipeline (not even a domicile), so nothing in this response maps one to an ISO code. A unit with no publishable `code` is omitted from both arrays and reported in `notices` rather than emitted with a null code.\n","type":"array","items":{"$ref":"#/components/schemas/StatusCountry"}},"notices":{"type":"array","items":{"type":"object","properties":{"level":{"type":"string"},"code":{"type":["string","null"]},"message":{"type":"string"}}}},"observed":{"description":"Observed operational numbers computed from the gateway's OWN request log over the last 24h. Not an SLA; null fields mean no samples yet.\n","type":"object","properties":{"observed_uptime_24h":{"description":"fraction of requests that did not 5xx (0–1)","type":["number","null"]},"p95_ms_24h":{"type":["number","null"]},"sample_size_24h":{"type":"integer"},"note":{"type":"string"}}},"source":{"type":"string"}}},"LimitsResponse":{"type":"object","required":["enforced","auth_header","tiers"],"properties":{"enforced":{"type":"boolean"},"auth_header":{"type":"string"},"tiers":{"type":"object","additionalProperties":{"type":"object","properties":{"requests_per_min":{"type":"integer"},"burst":{"type":"integer"},"max_concurrent_plan":{"type":"integer"},"daily_cap":{"type":["integer","null"]},"auth":{"type":"string"}}}},"headers":{"type":"object","additionalProperties":{"type":"string"}},"plan_cost_guard":{"type":"object","properties":{"global_concurrency":{"type":"integer"},"queue_timeout_s":{"type":"number"},"over_capacity_status":{"type":"integer"}}},"notes":{"type":"array","items":{"type":"string"}}}},"Attribution":{"type":"object","required":["feed_id","license","attribution"],"properties":{"feed_id":{"type":["string","null"]},"country":{"description":"Published code of the unit shipping this feed — an ISO code when `kind` is \"country\", the non-ISO operator code when it is \"operator\". Filter on `kind` before treating this as a country: an operator unit has no country, so there is no ISO code to fall back to.\n","type":["string","null"]},"kind":{"type":"string","enum":["country","operator"],"default":"country"},"license":{"type":["string","null"]},"attribution":{"type":"string"},"source_url":{"type":["string","null"]}}},"AttributionsResponse":{"type":"object","required":["attributions","count"],"properties":{"attributions":{"type":"array","items":{"$ref":"#/components/schemas/Attribution"}},"count":{"type":"integer"},"notice":{"type":"string"},"source":{"type":"string"}}}},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}}}