I'm building a side project called Parcelle. It's a site for landowners in French-speaking Switzerland: our goal is to give landowners access to important public information that will govern what they can and cannot develop on their land, and the tools to speak eye to eye with real estate developers and architects.
So, we decided to build an easy "plot lookup" system: you type your address, and it tells you what the public registers say about your plot. Zoning, legal restrictions, building data, links to the official documents. You can find an example here: https://ma-parcelle.ch/fr/parcelle/CH314566837512
The data is public and free. There's no API key anywhere in this post. What took me a week was working out how to use it, because the official docs list the endpoints and say nothing about the ways you'll get them wrong.
Four of those cost me most of the week. I've only tested this against one canton out of twenty-six, and I'll come back to why that matters.
The starting point
Switzerland runs a federal geodata API at api3.geo.admin.ch. No registration, no key, no quota I've hit so far. It fronts hundreds of layers: zoning, noise maps, geology, solar potential, public transport, the federal building register.
An address lookup:
GET https://api3.geo.admin.ch/rest/services/api/SearchServer
?searchText=Avenue de la Gare 1 Lausanne
&type=locations
&origins=address
&sr=2056
That one works first time. Then it gets interesting.
Trap 1: x is the northing
The response looks like this:
{
"label": "Avenue de la Gare 1 <b>1003 Lausanne</b>",
"x": 1152019.625,
"y": 2538509.5,
"lat": 46.5166,
"lon": 6.6374
}
If you're like me, you read x, y and you reach for geometry=x,y in the next call.
Don't. In this API x is the northing and y is the easting. Every downstream endpoint wants geometry={easting},{northing}, which means you pass geometry={y},{x}.
The part that ate my afternoon is that swapping them doesn't throw. Swiss LV95 coordinates sit roughly between 2.4 and 2.8 million east, and 1.0 and 1.3 million north. Swap them and you get a point that's numerically plausible and geographically nowhere. The API returns an empty result set, and you spend an hour assuming your layer name is wrong.
I now have a unit test whose only job is to assert that a known Lausanne address comes back with a commune called Lausanne.
Trap 2: one point, a hundred and forty rows
I wanted the commune and canton for a coordinate. There's a layer for that:
GET .../MapServer/identify
?geometry=2538509,1152019
&geometryType=esriGeometryPoint
&layers=all:ch.swisstopo.swissboundaries3d-gemeinde-flaeche.fill
&tolerance=0&sr=2056&returnGeometry=false
I expected one result. I got more than 140, several hundred kilobytes of JSON, for a point that sits inside exactly one commune.
The layer is historical. One feature per year, back to 1850. Lausanne in 1882, Lausanne in 1883, Lausanne in 1884, each with a slightly different area as the municipal boundaries moved.
The fix is one line, once you know the field exists:
const commune = results.find(r => r.attributes.is_current_jahr === true);
The history is there on purpose. If you ever need to know which commune a parcel belonged to before a merger, this is where you look. It's just a surprise when all you wanted was today's name.
Trap 3: the layer that tells you where to go next
This is the good one, and the thing I wish someone had written down.
Switzerland maintains a cadastre of public-law restrictions on land, the ÖREB cadastre, RDPPF in French. It records what legally constrains a given plot: zoning plans, noise sensitivity, forest boundaries, airport safety zones.
It isn't a federal service. All 26 cantons run their own. So how do you know which endpoint to call for an arbitrary coordinate?
There's a layer for that, and you'd never guess it from the name:
layers=all:ch.swisstopo-vd.stand-oerebkataster
Query it with a point and you get this, per commune:
{
"gemeindename": "Lausanne",
"kanton": "Vaud",
"bfs_nr": 5586,
"oereb_status_fr": "Cadastre RDPPF introduit",
"firmenname": "Direction du cadastre et de la géoinformation du Canton de Vaud",
"url_oereb": "http://www.rdppf.vd.ch/",
"oereb_webservice": "https://www.rdppf.vd.ch/ws/RdppfSVC.svc",
"email": "info.dgtl@vd.ch",
"telefon": "021 316 24 60"
}
oereb_webservice is the field that matters. A federal layer handing you the cantonal API endpoint for any point in the country, along with the office that runs it and whether that commune has the cadastre in production yet. Plenty of them don't.
I was about a third of the way through hand-writing a lookup table of cantonal portals when I found it.
Trap 4: the cantonal endpoints don't agree on paths
With the service URL in hand, the ÖREB spec gives you standard operations. Resolving a coordinate to a federal property identifier works exactly as documented:
GET {service}/getegrid/json/?EN={easting},{northing}
{"Item":[{
"egrid": "CH774594628366",
"number": "6177",
"identDN": "VD0132000000",
"type": {"Text":[{"Text":"Bien-fonds"}]}
}]}
Then I went looking for the full extract. The common implementation, pyramid_oereb, which a number of cantons run, exposes it at:
{service}/extract/reduced/json/{egrid}
- So did
oereb/extract/reduced/json/{egrid}. So did every path variant I tried.
I concluded the canton simply didn't publish the extract, wrote that down as a constraint in a technical spec, and moved on. It sat there for two days.
It does publish it. The path uses a query parameter instead of a path segment:
GET {service}/extract/json/?EGRID={egrid}
145 KB of structured JSON. Every restriction on the parcel with its exact area share, and every legal document with a direct PDF link:
{
"Theme": "Plans d'affectation (cantonaux/communaux)",
"AreaShare": 947,
"PartInPercent": 96.258939734339,
"LegalProvisions": [{
"Title": "Plan général d'affectation Règlement",
"TextAtWeb": "https://www.rdppf.vd.ch/ws/utilities/GetDocuments.ashx?..."
}]
}
What I'd do differently: before concluding an endpoint doesn't exist, call {service}/capabilities/json/ and {service}/versions/json/. Both answered fine on this service while the documented extract path was 404ing. That should have told me the service was alive and I was knocking on the wrong door.
The short version
| What | Where |
|---|---|
| Address to coordinates |
api/SearchServer?type=locations, remember x is north |
| Point to commune |
swissboundaries3d-gemeinde-flaeche.fill, filter on is_current_jahr
|
| Point to cantonal cadastre endpoint |
ch.swisstopo-vd.stand-oerebkataster, field oereb_webservice
|
| Point to property ID | {service}/getegrid/json/?EN=e,n |
| Property ID to full extract | path varies by canton, call capabilities/json/ first |
| Building data, 79 fields | ech/MapServer/ch.bfs.gebaeude_wohnungs_register/{featureId} |
| Ground elevation | rest/services/height?easting=&northing= |
One shortcut worth knowing: the building register entry already contains the property identifier and the parcel number. For any address with a building on it, you can skip the cantonal call entirely.
What I still don't know
All of the above is tested against one canton. There are 26, running at least two different implementations of the same specification, and I already know the paths differ between them. Mapping the rest is my next chore.
If you've worked with the ÖREB service in another canton, I'd like to know which path shape yours uses. Leave it in the comments and I'll keep a running list in this post.
And if your country has its own national open-data API with its own undocumented landmines, I'd read that. I'm starting to suspect they all have a coordinate-order problem somewhere.
I used an AI assistant to probe these endpoints with me and to clean up the draft. Every request and response in this post was actually run, and the outputs are copied from what came back.
Top comments (0)