REST-API (Pro)
Lebende Referenz. Diese Seite wird bei jeder Schnittstellen-Änderung mitgepflegt. Brechende Änderungen erhalten eine neue Version; alte Versionen laufen mindestens 12 Monate weiter (siehe Versionierung). Die maschinenlesbare Spezifikation liegt als
openapi-v1.yamlim Repo untercontrolplane/public/docs/api/.
Die REST-API ist im Pro-Plan enthalten und erlaubt externen Programmen den Zugriff auf einen Mandanten: Zählerstände von Feldgeräten/Apps melden, Adressen geocodieren, das Leitungsnetz abfragen und Stammdaten per Massen-Import laden.
Basis-URL & Versionierung
Die API wird unter der Tenant-Subdomain ausgeliefert:
https://<slug>.quellstube.at/api/v1/…
- Pfadbasierte Versionierung (
/api/v1, künftig/api/v2, …). - Nicht-brechende Änderungen (neue Endpunkte, neue optionale Felder) gehen in die laufende Version.
- Brechende Änderungen bekommen eine neue Version. Die alte läuft weiter und
liefert solange die HTTP-Header
Deprecation: trueundSunset: <Datum>. - Garantierter Mindest-Support: 12 Monate ab Ankündigung, bevor eine Version abgeschaltet wird.
Authentifizierung
Jede Anfrage trägt einen API-Schlüssel als Bearer-Token:
Authorization: Bearer qs_live_<slug>_<hex>
Schlüssel werden in der quellstube-App unter Verwaltung → API-Schlüssel erstellt (nur Tenant-Admins, nur Pro). Der Klartext wird einmalig bei der Erstellung angezeigt; gespeichert wird nur ein Hash.
- Der im Schlüssel enthaltene Slug muss zur aufgerufenen Subdomain passen
(sonst
403 wrong_tenant). - Nicht-Pro-Mandanten erhalten
402 pro_plan_required. - Pausierte (gekündigte/abgelaufene) Mandanten erhalten
402 tenant_suspended.
Berechtigungen (Scopes)
Ein Schlüssel trägt eine Teilmenge der App-Berechtigungen. Jeder Endpunkt verlangt einen passenden Scope:
| Scope | Deckt ab |
|---|---|
zaehler |
Zähler & Ablesungen |
network |
Leitungsnetz & Geocoding |
stammdaten |
Kontakte/Objekte, Geocoding, Import |
rechnungen_op |
Rechnungs-Kennzahlen (Statistik) |
Fehlt der Scope: 403 insufficient_scope.
Rate-Limit
Standardmäßig 240 Anfragen/Minute je Mandant/IP. Überschreitung: 429.
Fehlerformat
Fehler sind JSON mit error-Code und optionaler message:
{ "error": "meter_not_found", "message": "Zaehler 12345 nicht gefunden." }
| Status | Beispiel-Codes |
|---|---|
| 400 | tenant_required, invalid_value, invalid_payload |
| 401 | missing_bearer_token, invalid_or_revoked_key |
| 402 | pro_plan_required, tenant_suspended |
| 403 | wrong_tenant, insufficient_scope |
| 404 | meter_not_found, no_active_plan |
| 413 | too_many_rows |
| 429 | (Rate-Limit) |
| 503 | geocoding_unavailable |
Endpunkte (v1)
GET /api/v1 — Übersicht
Version, freigeschaltete Scopes, Endpunkt-Liste.
GET /api/v1/stats — Kennzahlen (Scope: zaehler | rechnungen_op | stammdaten)
Aggregiert, PII-frei: customers, properties, meters_active,
open_invoices, active_period, missing_readings_active_period.
Standort & Leitungsnetz
GET /api/v1/geocode (Scope: network | stammdaten)
Adresse → WGS84-Koordinate (BEV-Adressregister, kein Personenbezug).
Parameter: strasse (Pflicht), hausnummer, plz, ort.
GET /api/v1/geocode?strasse=Dorfstraße&hausnummer=12&plz=4030&ort=Linz
→ { "found": true, "lat": 48.2, "lng": 14.3 }
GET /api/v1/network/features (Scope: network)
Anlagen des aktiven (oder plan_id) Plans. Filter: type (z. B. hydrant),
near=lat,lng + radius (Meter, nur Punkte, aufsteigend nach Entfernung),
limit. PII-frei.
GET /api/v1/network/features?type=hydrant&near=48.2,14.3&radius=300
→ { "plan_id": 1, "count": 4, "features": [ { "id": 7, "feature_type": "hydrant", "lat": …, "distance_m": 41.3, … } ] }
GET /api/v1/network/features.geojson (Scope: network)
Wie oben, als GeoJSON-FeatureCollection.
Zähler & Ablesungen
GET /api/v1/meters (Scope: zaehler)
Zähler-Liste (Nummer, Standort, Typ, property_id) — ohne Kundennamen.
Filter: active (true/false), property_id, limit.
GET /api/v1/meters/<meter_number>/readings (Scope: zaehler)
Ablesungen eines Zählers (Wert, Datum, Verbrauch, is_estimated, Periode).
POST /api/v1/meter-readings (Scope: zaehler)
Einen Zählerstand erfassen. Idempotent pro (Zähler, Abrechnungsperiode) —
ein vorhandener Stand wird aktualisiert; Verbrauch & Schätz-/Korrekturabgleich
laufen wie im UI.
POST /api/v1/meter-readings
{ "meter_number": "A-100", "value": 1234.5, "reading_date": "2026-06-30", "is_estimated": false }
→ 201 { "id": 88, "value": 1234.5, "consumption": 42.0, "billing_period": "2025/26", … }
Ohne billing_period/billing_period_id wird die aktive Periode genutzt.
reading_date akzeptiert ISO (JJJJ-MM-TT) oder TT.MM.JJJJ.
POST /api/v1/meter-readings/bulk (Scope: zaehler)
Bis zu 500 Ablesungen pro Anfrage: { "readings": [ {…}, … ] }. Antwort:
saved_count, error_count, saved, errors (pro Zeile mit Index).
Massen-Import (REST-only, PII-haltig)
Import-Endpunkte verarbeiten Klar-Stammdaten (Namen/Adressen) und sind daher bewusst nicht als MCP-Tool verfügbar.
POST /api/v1/import/customers (Scope: stammdaten)
POST /api/v1/import/properties (Scope: stammdaten)
POST /api/v1/import/meters (Scope: zaehler | stammdaten)
Body: { "rows": [ {…}, … ], "update_existing": false }. Upsert je natürlichem
Schlüssel (Kundennummer / Objektnummer / Zählernummer). Maximal 1000 Zeilen
pro Anfrage (413 too_many_rows, kein stilles Kürzen — größere Mengen in
Blöcken senden). Jede Zeile ist transaktional isoliert; Fehler einer Zeile
verwerfen nur diese. Antwort: created, updated, skipped, error_count,
errors.
Beispiel-Felder:
- customers: name (oder first_name/last_name), customer_number,
email, strasse, hausnummer, plz, ort, is_company, externe_kennung.
- properties: object_number, object_type (Default Haus), strasse,
hausnummer, plz, ort.
- meters: meter_number, property_id oder property_object_number
(beim Anlegen Pflicht), location, meter_type (main/sub),
initial_value, eichjahr.
Changelog
v1 — 2026-06-30
- added Erstveröffentlichung:
geocode,network/features(.geojson),meters,meters/<nr>/readings,meter-readings(+/bulk),import/{customers,properties,meters},stats.