Transportation — entry 058 of 69
Transport for Switzerland
transport.opendata.ch is a free, keyless, community-run JSON API for Swiss public transport built on the same backend as search.ch's timetable search, covering stop lookups, connections, and station boards. CORS is explicitly enabled site-wide so it can be called directly from browser-based apps, though request volume is bounded by the underlying timetable.search.ch service's own rate limit.
transport.opendata.ch is a free, keyless JSON API for Swiss public transport, built as a community layer over the same backend that powers search.ch's timetable search. It covers three resources — `/locations` (resolve a place name or coordinates to stops/addresses/POIs), `/connections` (journey planning between two locations), and `/stationboard` (live departures from one station) — and enables CORS site-wide so it can be called directly from browser code.
GreatAPIs Score
Auth quickstart
- No API key required — requests are public and keyless. The docs enable CORS explicitly ("so you can easily use the API on transport.opendata.ch from any website") but ask callers to avoid setting custom request headers, since that can break the CORS exemption.
Your key is stored only in this browser (localStorage) and sent directly to the API — never to greatapis.
Resolve a place name to stop IDs and coordinates
GEThttps://transport.opendata.ch/v1/locations?query=Basel
{"stations":[{"id":"8500010","name":"Basel SBB","score":null,"coordinate":{"type":"WGS84","x":47.547403,"y":7.589564},"distance":null,"icon":"train"},{"id":"8500090","name":"Basel Bad Bf","score":null,"coordinate":{"type":"WGS84","x":47.567301,"y":7.606922},"distance":null,"icon":"train"}]}Response abridged to 2 of the 10 matches a bare `query=Basel` actually returns live — the real payload includes trams and secondary stops too.
Developer reference
https://transport.opendata.ch/v1Gotchas & limits
- The docs state the rate limit plainly but don't publish a number: "The number of HTTP requests you can send is constraint by the rate limit of timetable.search.ch" — confirmed live in this run (still true as of 2026-07). Since the ceiling belongs to a third-party upstream and isn't published as a fixed figure here, don't hard-code an assumed request budget; build in backoff on non-200s instead.
- `/connections` accepts **either** form for `from`/`to` — the numeric `id` from `/locations` or a plain name string — so you can pass IDs straight through without a name round-trip. All three probed live in this run against the same city pair: `?from=8503000&to=8500010` (IDs), `?from=Zurich&to=Basel` (informal names), and `?from=Zürich HB&to=Basel SBB` (the exact `name` values `/locations` returns) each returned `200` resolving to Zürich HB (`8503000`) → Basel SBB (`8500010`). Note the informal spelling `Zurich` resolves to `Zürich HB` on its own, so the API is doing fuzzy name matching rather than requiring canonical strings.
- An unresolvable `from`/`to` doesn't error — a live `?from=Zzzznotarealplace&to=Basel` returned `200` with `{"connections":[],"from":null,"to":null,"stations":{"from":[],"to":[]}}` rather than a `400`/`404`. Check for a null `from`/`to` (or an empty `connections` array) instead of relying on the status code to tell you a place name failed to resolve.
- `limit` on `/connections` is capped at 1–16 and connections departing at the exact same time are counted as a single result even if several legs exist — per the docs, don't assume `limit=5` always yields 5 distinct departure times.
- Nullable fields come back as explicit `null` rather than being omitted, and which ones are null depends on how you searched. Both probed live in this run: a plain `?query=Basel` returned all 10 stations with `distance: null` and `score: null`, while a coordinate lookup `?x=47.547403&y=7.589564` (the docs define `x` as latitude and `y` as longitude, in that order) populated `distance` on all 10. The docs describe that field as "If search has been with coordinates, distance to original point in meters"; the probe returned `0` for the exact match, `112`/`261`/`315` for nearby tram stops, up to `444` for the furthest of the 10 — and one sub-meter `0.04340541522099573`, so don't assume the value is a whole number. Watch out on the coordinate form: one returned result was a `Haltestelle` entry with `"id":null`, so don't assume every station in a `/locations` response carries a usable ID to feed onward.
- The docs explicitly warn against setting custom request headers on browser calls, since that can take the request outside the simple-CORS exemption they've enabled and trigger a preflight the API doesn't handle specially.