Transportation — entry 058 of 69

Transport for Switzerland

Verified Jul 2026

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.

transitswitzerlandcommunityopen-sourcejourney-planning
AuthenticationNone requiredCall it straight away — no key, no signup.
HTTPSSupportedTraffic is encrypted in transit.
CORSEnabledCallable directly from browser JavaScript.
PricingFreeNo paid tier — free for the documented use case.
FormatsJSONResponses can be requested as JSON.

GreatAPIs Score

Score88out of 100
Authentication25/25No authentication required
Pricing20/20Free to use
Docs14/20Documentation URL provided
Formats9/15Single response format
Freshness20/20Verified within 6 months

Embed this badge

Scored 88 on greatapis.com
<a href="https://greatapis.com/api/transport-for-switzerland-transportation/"><img src="https://greatapis.com/badge/transport-for-switzerland-transportation.svg" alt="Scored 88 on greatapis.com"></a>

Auth quickstart

  1. 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.
Stored keyNo key stored

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

200 application/json

{"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

Base URLhttps://transport.opendata.ch/v1

Gotchas & 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.