# Baugesuche Schweiz — full agent reference (llms-full.txt) Free, self-service API + MCP server for official Swiss building-permit publications (Baugesuche), aggregated from 14 cantonal Amtsblatt sources via the public amtsblattportal.ch API, harmonized into one model called `bgch.v1` (JSON is canonical; XML mirrors it 1:1), and updated daily. The original source XML is preserved verbatim. - Base URL: https://baugesuche.switchli.ch - API prefix: https://baugesuche.switchli.ch/api/v1 - OpenAPI: https://baugesuche.switchli.ch/api/v1/openapi.json — Swagger UI: https://baugesuche.switchli.ch/api/docs - Human quickstart: https://baugesuche.switchli.ch/api - MCP server (streamable HTTP): https://baugesuche.switchli.ch/mcp ## Guarantees & legal - INFINITE HISTORY: nothing is ever pruned. Every record ever ingested stays queryable and its xml_original/PDF links stay resolvable for as long as the service runs. Safe to build long-lived mirrors, archives, and agents against `/changes`. - LEGALLY BINDING is ONLY the signed official PDF (`source.official_pdf_url` / `links.pdf`). JSON, harmonized XML, titles and descriptions are this aggregator's best-effort representation, not an official publication. Every record carries a `_disclaimer` field saying so. - revDSG: the API serves the same as-published data as the website; there is no person-search endpoint; removal requests purge the record (and its original XML) too. ## Formats - All machine dates are ISO 8601: dates `YYYY-MM-DD`, timestamps with timezone (e.g. `2026-07-01T04:31:02+00:00`). The website UI shows `DD.MM.YYYY`; the API never does. - Responses are JSON (`application/json`) unless the path ends in `.xml`. ## Auth & rate limits - Anonymous: 120 requests / hour / IP. No key needed. - With a free key: 5000 requests / hour. Send it as `Authorization: Bearer bg_`. - Get a key: `POST https://baugesuche.switchli.ch/api/v1/keys` with JSON `{"email":"you@example.com"}` → a double-opt-in email is sent; clicking the link reveals the key EXACTLY ONCE (only its SHA-256 hash is stored). Also requestable via the form at https://baugesuche.switchli.ch/api . - Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (unix seconds). On 429 there is also `Retry-After` (seconds). ## Endpoints ### GET /api/v1/publications — filtered, keyset-paginated list Query params (all optional): - `canton` — CSV of canton codes, e.g. `ZH,BE`. - `category` — CSV of category codes (see taxonomy below), e.g. `energie_heizung`. - `municipality` — CSV of exact municipality names, e.g. `Winterthur`. - `q` — free-text substring across title, description, municipality and Projektverfasser. - `from` — publication_date lower bound (inclusive), ISO date `YYYY-MM-DD`. - `to` — publication_date upper bound (inclusive), ISO date. - `ingested_since` — only records first ingested at/after this ISO 8601 timestamp. - `cursor` — opaque pagination cursor from a previous response's `next_cursor`. - `limit` — page size, 1..200 (default 50). Response: `{ "items": [...], "next_cursor": , "total": }`. `total` is the full match count regardless of paging. Items are newest publication_date first. ### GET /api/v1/publications/{id} — one record (bgch.v1 JSON) ### GET /api/v1/publications/{id}.xml — same record, harmonized bgch.v1 XML ### GET /api/v1/publications/{id}/original.xml — official source XML, byte-for-byte (404 until backfilled for older records; newer records have it). ### GET /api/v1/changes?since= — incremental sync For mirrors/agents polling deltas. Ordered by `ingested_at` (NOT publication_date), so it never splits a same-timestamp batch across pages. Params: `since` (required, ISO 8601), `limit` (1..1000, default 200). Response: `{ "items": [...], "next_since": , "count": }`. ### GET /api/v1/meta — coverage, taxonomy, service metadata (start here) Returns total_publications, oldest/newest publication_date, municipalities_count, per-canton counts, the full category taxonomy with labels, schema/docs/MCP URLs, `infinite_history: true`, and the disclaimer. Use this for LIVE counts. ### POST /api/v1/keys — request a free key (JSON body `{"email": "..."}`) ## Cursor semantics (pagination) The cursor is an opaque base64 of `(publication_date, id)` of the last row on the page. Pagination uses keyset (seek), never OFFSET, so pages stay stable and cheap no matter how large the table grows (infinite history). Loop: call `/publications` with your filters, process `items`, then if `next_cursor` is non-null call again with the SAME filters plus `cursor=`; stop when `next_cursor` is null. Do not change filters mid-loop. ## /changes sync recipe (incremental mirror) 1. First run: `GET /api/v1/changes?since=1970-01-01T00:00:00+00:00` (or any early time). 2. Process `items` (each is a full bgch.v1 record; upsert by `id`). 3. Remember `next_since` from the response. 4. If `count` == your `limit`, immediately call again with `since=` (more waiting). 5. Otherwise sleep (e.g. hourly) and resume from the stored `next_since`. Because ordering is by `ingested_at` and same-timestamp batches are never split, this loop never skips or double-counts a record. ## bgch.v1 field dictionary Every field of a publication record (JSON canonical; XML uses the same snake_case names under root ``): - `id` (string) — stable internal identifier, opaque. Example: `d22d21f6-e361-4655-bd1d-8c2350863cdb`. - `publication_number` (string) — official publication number, may be empty. Example: `2026-012345`. - `source` (object): - `source.tenant` (string) — source-system tenant key on amtsblattportal.ch. Example: `kabzh`. - `source.subrubric` (string) — sub-rubric code the record was polled under. Example: `BP-ZH01`. - `source.official_detail_url` (string) — human detail page on the cantonal portal. - `source.official_pdf_url` (string) — the official signed PDF (legally binding). - `canton` (string) — canton code (ISO 3166-2:CH; `CH` for cross-cantonal municipal tenants). Example: `ZH`. - `municipality` (object): `name` (string, as published) and `slug` (string, URL-safe). Example: `{"name":"Winterthur","slug":"winterthur"}`. - `publication_date` (string|null) — official publication date, ISO date. Example: `2026-07-01`. - `expiration_date` (string|null) — official expiration/decision date, ISO date, if published. - `entry_deadline` (string|null) — Einsprache-/Auflagefrist deadline, ISO date, if published. - `title` (string) — official title as published. - `description` (string) — project description as published. - `categories` (array of string) — static taxonomy codes (multi-label). Example: `["energie_heizung","umbau_sanierung"]`. - `building_contractor` (string) — Bauherrschaft as officially published (display string; persons privacy-reduced). - `project_framer` (string) — Projektverfasser as officially published (display string). - `location` (object): `raw` (string) — address/location as published, unparsed. - `parcel` (string) — parcel / zone reference as published. - `ingested_at` (string) — when this aggregator first stored the record, ISO 8601 UTC. Example: `2026-07-01T04:31:02+00:00`. - `links` (object): `html`, `json`, `xml`, `xml_original`, `pdf` — absolute URLs for this record. - `_disclaimer` (string) — legal note: only the signed official PDF is legally binding. ## Category taxonomy (static, FROZEN, multi-label) A record can carry several category codes; `sonstiges` means none of the specific rules matched. - `energie_heizung` — Energie & Heizung - `solar_pv` — Solar & PV - `neubau_wohnen` — Neubau Wohnen - `umbau_sanierung` — Umbau & Sanierung - `abbruch` — Abbruch - `kleinbauten` — Kleinbauten - `gewerbe_industrie` — Gewerbe & Industrie - `landwirtschaft` — Landwirtschaft - `aussenanlagen_infrastruktur` — Aussenanlagen & Infrastruktur - `sonstiges` — Sonstiges ## Canton codes - `ZH` — Zürich - `BE` — Bern - `SZ` — Schwyz - `OW` — Obwalden - `NW` — Nidwalden - `ZG` — Zug - `SO` — Solothurn - `BS` — Basel-Stadt - `BL` — Basel-Landschaft - `SH` — Schaffhausen - `AR` — Appenzell A.Rh. - `TI` — Tessin - `VS` — Wallis - `CH` — weitere Gemeinden ## Coverage notes - Sources are the official cantonal publication organs via amtsblattportal.ch ONLY. - ZH (Zürich) is complete by law — all municipalities must publish there. - AG (Aargau) is intentionally ABSENT: it does not publish Baugesuche via amtsblattportal.ch, so there is no lawful source to aggregate; do not expect AG data. - `CH` groups cross-cantonal municipal tenants that publish centrally. - For live per-canton counts and the oldest/newest dates, call https://baugesuche.switchli.ch/api/v1/meta . ## Worked example 1 — filtered search (curl) ``` curl -s "https://baugesuche.switchli.ch/api/v1/publications?canton=ZH&category=energie_heizung&q=W%C3%A4rmepumpe&limit=5" ``` Returns a page of energy/heating permits in Zürich matching "Wärmepumpe", with `total`, `next_cursor` and up to 5 `items`. ## Worked example 2 — pagination loop (Python, stdlib only) ``` import json, urllib.parse, urllib.request BASE = "https://baugesuche.switchli.ch/api/v1" def get(path, **p): url = f"{BASE}{path}?" + urllib.parse.urlencode({k: v for k, v in p.items() if v != ""}) with urllib.request.urlopen(urllib.request.Request(url, headers={"Accept": "application/json"}), timeout=15) as r: return json.load(r) cursor = "" while True: page = get("/publications", canton="ZH", category="energie_heizung", limit=50, cursor=cursor) for item in page["items"]: print(item["publication_date"], item["municipality"]["name"], item["title"][:60]) cursor = page["next_cursor"] if not cursor: break ``` ## Worked example 3 — incremental sync (curl loop) ``` SINCE="1970-01-01T00:00:00+00:00" while : ; do RESP=$(curl -s "https://baugesuche.switchli.ch/api/v1/changes?since=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$SINCE")&limit=200") # ... upsert each .items[] by .id ... SINCE=$(printf '%s' "$RESP" | python3 -c "import json,sys;print(json.load(sys.stdin)['next_since'])") COUNT=$(printf '%s' "$RESP" | python3 -c "import json,sys;print(json.load(sys.stdin)['count'])") [ "$COUNT" -lt 200 ] && break # caught up; resume later from $SINCE done ``` ## MCP server (Model Context Protocol) - Transport: streamable HTTP, stateless. Endpoint: https://baugesuche.switchli.ch/mcp - No API key required (anonymous rate limit applies per IP). - Client config (e.g. Claude Desktop): `{"mcpServers": {"baugesuche": {"type": "streamable-http", "url": "https://baugesuche.switchli.ch/mcp"}}}` - Tools: - `search_baugesuche(query?, canton?, category?, municipality?, date_from?, date_to?, limit<=50)` — filtered search, returns bgch.v1 records. - `get_baugesuch(id)` — one full record incl. links to the official PDF/XML. - `get_coverage()` — cantons covered, counts, oldest/newest, guarantee + disclaimer. - Raw JSON-RPC over HTTP (response is SSE; the JSON-RPC payload is on the `data:` line): ``` curl -s -X POST "https://baugesuche.switchli.ch/mcp" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_baugesuche","arguments":{"query":"W\u00e4rmepumpe","canton":"ZH","limit":3}}}' ```