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.