Schnittstelle

Die Gridbert-Schnittstelle

Diese Seite dokumentiert dieselbe Schnittstelle, mit der auch app.gridbert.at rechnet. Keine zweite, abgespeckte Variante. Sie ist erzeugt, nicht getippt: Werkzeuge, Endpunkte und Grenzen kommen direkt aus dem Gateway-Code.

Was die Schnittstelle ist

Gridbert bietet zwei Wege zu denselben Werkzeugen: einen MCP-Server (unten) für Chatbots und eigene Agenten, und HTTP-Endpunkte, über die app.gridbert.at und die Kontoseiten selbst laufen. Für wen das relevant ist: alle, die einen eigenen Client, ein Skript oder einen Agenten gegen Gridbert bauen wollen. Für den fertigen Chat-Einstieg gibt es die Anleitungen je Agent.

Verbinden über MCP

Die Adresse für jeden MCP-fähigen Client:

https://mcp.gridbert.at/mcp

Verbindungsaufbau läuft über OAuth 2.1 mit Dynamic Client Registration, die Einrichtung je Agent beschreibt den Klickweg, hier die technischen Eckdaten:

AS-Discoveryhttps://mcp.gridbert.at/.well-known/oauth-authorization-server
Resource-Discoveryhttps://mcp.gridbert.at/.well-known/oauth-protected-resource
JWKShttps://mcp.gridbert.at/jwks.json
Client-Registrierung (DCR)https://mcp.gridbert.at/register
Scopesmcp-tools offline_access
Grant-Typenauthorization_code, refresh_token, client_credentials
PKCES256, Pflicht
Token-Endpunkt-Authnone, client_secret_post (öffentlicher Client)
Zugangs-Token gültig30 Minuten
Refresh-Token gültig30 Tage
Autorisierungs-Code gültig1 Minute
Protokollversion2025-06-18

Unter POST /mcp stehen vier JSON-RPC-Methoden bereit: initialize, notifications/initialized, tools/list, tools/call. Jede Werkzeug-Antwort trägt ihr Ergebnis doppelt: als Text-Block (content) und, wo ein Werkzeug ein Ausgabe-Schema deklariert, zusätzlich strukturiert als structuredContent. Beide sind byte-gleich derselbe Rechenweg, nur einmal als Text und einmal als Objekt.

Werkzeuge

27 Werkzeuge stehen jedem verbundenen Haushaltskonto zur Verfügung. Die Familie zeigt, woher ein Ergebnis kommt:

Gedächtnis liest oder schreibt dein persönliches Profil · Rechenwerk rechnet deterministisch mit energietools · Gridbert-Domäne ist Gateway-eigene Logik (Fakten, Bereitschaft, Rückfragen).

Die Beschreibung je Werkzeug ist wörtlich zitiert, so bekommt sie der Agent.

  • analyze_load_profile Rechenwerk (energietools) nur lesend

    Lastprofil-Metriken (Grundlast, Spitzen, Anomalien, Sparpotenziale) + Ursachen-Signale (E-Heizung, PV-Eigenverbrauch, Dauerläufer) + signal-getriebene Rückfragen-Kandidaten aus einer Lastgang-Serie. IMMER als erster Analyse-Schritt nach list_load_series. lastprofil läuft auch auf Tageswerten weiter (Granularitäts-Caveat statt Verweigerung); signale verweigert bei Tageswerten (interval_minutes >= 60) — dann signale=null + signale_grund (Empfehlung, die Viertelstunden-Auslesung beim Netzbetreiber zu beantragen), lastprofil bleibt trotzdem verfügbar. Bei PV-Haushalten rechnet es automatisch mit dem Netzbezug statt mit dem Gesamtverbrauch (basis_label='Netzbezug'). Gespeicherte Fakten haben Vorrang vor Schätzungen: jeder Signal-Wert trägt eine quelle + profil_abgleich: quelle='profil'/'rechnung'/'messung' ist ein gespeicherter FAKT und IST die Antwort (z.B. aus submit_lastgang_facts) — quelle='heuristik' ist NUR eine Schätzung aus dem Lastprofil. Bei profil_abgleich.status='widerspruch' den Widerspruch dem User BENENNEN, nie stillschweigend zugunsten der Schätzung auflösen. mit_chartdaten=true liefert zusätzlich verdichtete Chart-Reihen (Stunden-Tagesprofil, Nachtlast je Tag) für Anzeige-Oberflächen — im Chat NICHT nötig, dort das Feld weglassen.

    Eingabe

    is_pv boolean true, falls der Haushalt eine PV-Anlage hat (dann wird mit dem Netzbezug gerechnet).
    mit_chartdaten boolean optional, Default false — für Anzeige-Oberflächen (z.B. die Gridbert-App), NICHT für den Chat. true liefert zusätzlich data.chartdaten: ein Stunden-Tagesprofil (gesamt + Werktag/Wochenende, je 24 Werte) und eine Nachtlastreihe je Tag (max. ~120 Tage) — verdichtete Anzeige-Aggregate, keine Rohserie. Ohne dieses Flag bleibt das Ergebnis unverändert.
    price_per_kwh number optional, brutto EUR/kWh — für Kosten-Kontext (lastprofil)
    pv_feedin_kwh number optional — Summe aus dem Einspeise-serie_ref, falls vorhanden. Ohne Angabe versucht der Gateway automatisch eine zweite aktive Einspeise-Freigabe desselben Accounts zu finden.
    serie_ref* string aus list_load_series/request_data_release

    Ausgabe (oberste Ebene)

    data object Zusammengeführtes Ergebnis (lastgang_analysis.run_analyze_load_profile).
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    meta object | null Herkunft der Rechnung/Daten aus energietools (quelle, stand, snapshot_version) — Inhalt variiert je Capability.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • answer_lastgang_question Gridbert-Domäne

    Speichert die Antwort auf eine offene Lastgang-Rückfrage (ereignis_id aus list_open_lastgang_questions) — entweder label (EXAKT eines der optionen[].label dieser Frage) ODER freitext (frei formuliert, höchstens 200 Zeichen), NIE beides gleichzeitig erfinden: nur übernehmen, was der User wirklich gesagt hat. Liefert danach dieselbe Rückschau wie die Web-Antwortseite — was diese Auffälligkeit seit Beginn der Messwerte gemacht hat, längstens über die letzten zwölf Monate (Tage, kWh, wenn auflösbar Euro) —, damit die Antwort für den User einen sofortigen Gegenwert hat, nicht nur einen 'danke, notiert'. Eine leere rueckschau ist kein Fehler (z.B. ohne Datenfreigabe oder zu wenig Messwerte) — die Antwort ist trotzdem gespeichert. Im Berater-Workspace ist das der Weg, die telefonisch aufgenommene Antwort des Kunden einzupflegen; sie wird als Berater-Angabe vermerkt und dem Haushalt später nicht als seine eigene zurückzitiert. Auch dort gilt: nur übernehmen, was der Kunde wirklich gesagt hat.

    Eingabe

    ereignis_id* integer aus list_open_lastgang_questions, NICHT raten oder selbst vergeben.
    freitext string Frei formulierte Antwort, falls keine Option passt.
    label string Exakt eines der optionen[].label der Frage. Weglassen, wenn stattdessen freitext gesetzt wird.

    Ausgabe (oberste Ebene)

    gespeichert boolean immer true — bei Ablehnung wird NICHTS gespeichert.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    rueckschau array Sätze der Rückschau in Alltagssprache (Tage, kWh, wenn auflösbar Euro). Kann LEER sein — kein Fehler, nur zu wenig Daten/keine Freigabe/kein Preis; die Antwort bleibt trotzdem gespeichert.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • capability_gap Gridbert-Domäne

    Melde einen Bedarf, den Gridbert (noch) nicht abdecken kann, wenn für eine Energie-Frage des Nutzers kein passendes Werkzeug existiert. Das Team sieht gesammelt, was Nutzer sich wünschen, und baut danach weiter.

    Eingabe

    kategorie* string Bereich des fehlenden Bedarfs.
    kurzbeschreibung* string Was hat konkret gefehlt? Zwei bis vier Sätze — welche Frage offen blieb, welche Zahl gebraucht wurde, ob sie extern beschafft werden musste. Keine Zählpunkte, Beträge oder Kontaktdaten.

    Ausgabe (oberste Ebene)

    aufgenommen boolean immer true, wenn die Meldung angenommen wurde.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    inhaltstragend boolean immer true — der Freitext wird gespeichert (personenbezogene Angaben werden vorher entfernt).
    kategorie string Eingeordnete Kategorie. Eine unbekannte Kategorie fällt bewusst auf 'sonstiges' zurück, statt die Meldung zu verwerfen.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • check_invoice_reading Gridbert-Domäne nur lesend

    Letzter Schritt des Lesens mit DEINEM Modell: prüft deine Antwort auf den Leseauftrag aus get_invoice_reading_task. Gridbert ruft hier KEIN Modell auf. Geprüft wird deterministisch: Schema, wörtliches Zitat gegen die Textebene, Kreuzproben (Summen, Verbrauch, Arbeitspreis), Zuschuss und Rabatt; danach wird mit dem Regelleser zusammengeführt (sicher gelesene Regel-Felder haben Vorrang, deine Felder stehen höchstens auf 'mittel'). Das Ergebnis hat EXAKT die Form von read_invoice_facts — je Feld Wert, Zitat und Konfidenz (hoch|mittel|niedrig|fehlt) — und ist dem User vorzulegen und bestätigen zu lassen, bevor submit_invoice_facts es rechnet ('quellen_anker' unverändert weitergeben). Rechnet nichts, speichert nichts. Eingabe: rechnung_ref, 'regelwerk_version' UNVERÄNDERT aus get_invoice_reading_task, und 'antwort' (dein JSON-Objekt in Kompaktform, oder derselbe Text; ein Codeblock drumherum wird toleriert; null, wenn lesen_noetig=false war). Fehler: error_class 'antwort_ungueltig' mit einer lesbaren Liste unter 'fehler' — korrigiere genau diese Stellen und rufe noch einmal auf (ein Nachbesserungsversuch genügt in der Regel); 'regelwerk_veraltet': das Regelwerk hat sich geändert, hol die Aufgabe mit get_invoice_reading_task neu. Gabst du bei get_invoice_reading_task seitentexte mit, gib dieselben hier wieder mit.

    Eingabe

    antwort object | string | null Deine Antwort auf den Leseauftrag: das JSON-Objekt in Kompaktform (oder ein Text mit genau diesem JSON). null, wenn get_invoice_reading_task lesen_noetig=false meldete.
    rechnung_ref* string Dieselbe Kennung wie bei get_invoice_reading_task.
    regelwerk_version* string Die regelwerk_version aus get_invoice_reading_task, unverändert.
    seitentexte array Optional, nur wenn schon bei get_invoice_reading_task angegeben: derselbe Text je Seite (Index 0 = Seite 1).

    Ausgabe (oberste Ebene)

    ablehnungsgrund string | null teilbetrag | einspeisung | energiegemeinschaft | grosskunde | ausland | keine_energierechnung — oder null, wenn prüfbar.
    einmalrabatte_brutto_eur number | null Einmaleffekte dieser Rechnung in €, brutto und positiv — oder null: befristete und einmalige Rabatte des Lieferanten (Neukunden-, Wechsel-, Aktionsbonus, Gratis-/Freimonate) × 1,2 plus ein Stromkostenzuschuss, der im Rechnungsbetrag schon abgezogen ist. Dauerhafte Nachlässe (Online, Bankeinzug, Bestandskunde) zählen nie, sie stecken im Arbeitspreis; ein Zuschuss ausserhalb des Betrags nie. Wiederkehrende Kosten der Periode = rechnungsbetrag_brutto_eur + einmalrabatte_brutto_eur. Gerechnet, nicht gelesen; NICHT an submit_invoice_facts weitergeben.
    energieart string strom | gas | kombi | unbekannt.
    energiepositionen array Die EINZELNEN Zeilen des Energieblocks, wenn die Rechnung dort mehr als eine führt (z.B. Spotpreis, Stromqualität, Mehrkosten Ökoenergie/HKN). Jeder Eintrag ist fertig für submit_invoice_facts: {bezeichnung, wert_eur, ist_netto}. **Unverändert weiterreichen, nichts ergänzen** — ein viertes Feld lässt die ganze Einreichung ablehnen. LEERE LISTE heisst 'keine erkannt', NICHT 'der Energieblock ist leer': dann wird der Block wie bisher über Arbeitspreis × Menge nachgerechnet. Die Belege dazu stehen in quellen_anker unter dem Feldnamen 'energiepositionen'.
    felder array Ein Eintrag je gelesenem Feld.
    gelesen_mit string textebene = aus der Textebene des PDFs gelesen (nur dann kann ein Feld 'hoch' sein). ocr = aus einem Scan oder Foto bzw. aus Seitentexten des Aufrufers gelesen (höchstens 'mittel').
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    pruefbar boolean false = keine prüfbare Jahres-, Zwischen- oder Monatsrechnung. Dann steht der Grund in 'ablehnungsgrund' und es gibt keine Prüfung.
    quellen_anker array Fertig für die Übergabe an submit_invoice_facts: Objekte {feld, zitat}. NICHT neu bauen — dort wird geprüft, ob der übergebene Zahlenwert im Zitat wirklich vorkommt.
    rabatt_ausserhalb_bloecke_eur number | null Rabatt-/Bonuszeile AUSSERHALB der drei Blöcke in €, netto und negativ (Rabatt = Minuszahl) — oder null. Nur gesetzt, wenn die Rechnung damit aufgeht. Unverändert an submit_invoice_facts weitergeben: (Summe Blöcke + Rabatt) × 1,2 − Zuschuss = Rechnungsbetrag.
    rabatt_im_energieblock_eur number | null Summe der gelesenen Rabatte des Lieferanten in €, netto und positiv — oder null. Unverändert an submit_invoice_facts weitergeben: die Nachrechnung zieht ihn nur ab, wenn der Energieblock danach aufgeht.
    rabatte array Die Rabatt- und Bonuszeilen des Lieferanten hinter einmalrabatte_brutto_eur. Nur zur Anzeige; NICHT an submit_invoice_facts weitergeben.
    rechnungstyp string jahresrechnung | zwischenrechnung | monatsrechnung | unbekannt.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • compile_done Gedächtnis (engram)

    Meldet ein Inbox-Item als fertig eingearbeitet (nach compile_next + upsert_page). keys = die Seiten, in die der Inhalt eingearbeitet wurde; jede muss einen [quelle: <item_id>]-Anker tragen, dessen Werte wörtlich in der Quelle stehen — der Server prüft das automatisch. keys=[] heißt: nichts Verwertbares, nichts einzuarbeiten. Für jede Seite ohne Typ typing[key] = { type, status, relations, confidence } angeben, ausschließlich mit dem Vokabular aus dem tbox-Feld der letzten compile_next-Antwort. claim_token aus compile_next.item.claim_token UNVERÄNDERT zurückgeben — er beweist, dass das Item noch dir gehört (sonst lease_lost = die Sperre ist abgelaufen).

    Eingabe

    claim_token string Sperr-Schlüssel aus compile_next.item.claim_token — unverändert zurückgeben.
    item_id* string Die Item-ID aus compile_next.
    keys* array Seiten-Keys, in die eingearbeitet wurde; [] = nichts Verwertbares.
    typing object Typing je noch untypisierter Ziel-Seite: { "<key>": { "type": "<T-Box-Typ|null>", "status": "<status|null>", "relations": { "<prädikat>": ["<key>"] }, "confidence": 0..1 } }.

    Ausgabe (oberste Ebene)

    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    proxied boolean immer true — die Nutzlast kommt aus der engram-Instanz, nicht aus dem Gateway.
    result object | null Rohes MCP-Tool-Result der Instanz: {content: [...], isError: bool}. Der eigentliche Inhalt steckt als JSON-Text in content[0].text.
    tool string Name des proxierten Vault-Tools.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • compile_next Gedächtnis (engram)

    Holt das nächste noch nicht eingearbeitete Item aus der Inbox und sperrt es für dich (nur eines pro Aufruf). Du bekommst dazu: eine Übersicht, in welche Seiten es gehört, die betroffenen Zielseiten, die Einarbeitungs-Regeln und das komplette Vokabular für die Typisierung (tbox.types/tbox.relations/tbox.statuses) — ein weiterer Tool-Call dafür ist nicht nötig. Leer = nichts zu tun.

    Ausgabe (oberste Ebene)

    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    proxied boolean immer true — die Nutzlast kommt aus der engram-Instanz, nicht aus dem Gateway.
    result object | null Rohes MCP-Tool-Result der Instanz: {content: [...], isError: bool}. Der eigentliche Inhalt steckt als JSON-Text in content[0].text.
    tool string Name des proxierten Vault-Tools.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • foerderung_und_beratung Rechenwerk (energietools) nur lesend

    Auskunft zu Förderungen, kostenloser Energieberatung und Energiegemeinschaften in Österreich — in einem Aufruf. Nutze es, wenn der User nach Förderungen für PV, Speicher, Heizungstausch, Sanierung, Balkonkraftwerk, Geräte-Reparatur oder Wallbox fragt, eine unabhängige Energieberatung in seinem Bundesland sucht oder wissen will, was eine Energiegemeinschaft (GEA, EEG, BEG) bringt und wie man beitritt. Bietet es am besten NACH einem Tarifvergleich an, nicht davor. Braucht 'bundesland' ODER 'plz' (die PLZ wird intern aufgelöst; ist sie unbekannt oder liegt sie in zwei Bundesländern, kommt eine Rückfrage nach dem Bundesland statt eines geratenen Ergebnisses). Nur für Energiegemeinschaften reicht es auch ganz ohne Ort. 'thema' wählt einen Teil: 'foerderungen' (offene, verifizierte Förderungen von Bund und Land mit Fördersatz, Bedingungen, Antrags-Link und Quelle), 'energieberatung' (Beratungsstelle mit Träger und Kontakt) oder 'energiegemeinschaften' (Vorteile je Rechtsform, die Netzentgelt-Reduktion für BEG greift erst ab 31.12.2026 — NICHT schon ab 1.10.2026, das frühere Datum betrifft nur Begriffs-Definitionen; dazu Beitrittsschritte und bekannte Verzeichnis-Einträge, die NICHT vollständig sind). Ohne 'thema' kommen alle drei Teile. Geschlossene oder unsichere Einträge werden nie ausgespielt. Nenne dem User den Datenstand ('stand') je Teil; es ist KEINE Rechts- oder Förderberatung, vor einem Antrag immer die verlinkte Quelle prüfen. 'kategorien' filtert nur die Förderungen, z.B. ['pv','speicher'].

    Eingabe

    bundesland string Bundesland, für das gesucht wird.
    kategorien array Nur für Förderungen, z.B. ['pv','speicher']. Leer/weggelassen = alle Kategorien.
    plz string PLZ, aus der das Bundesland aufgelöst wird (Alternative zu bundesland).
    thema string Optional: nur ein Teil. Weggelassen = Förderungen, Energieberatung und Energiegemeinschaften zusammen.

    Ausgabe (oberste Ebene)

    bundesland string Aufgelöstes Bundesland, falls eines bekannt ist.
    energieberatung object | null Kostenlose, produktneutrale Energieberatungsstelle(n) des Bundeslands.
    energiegemeinschaften object | null Rechtsformen mit Netzentgelt-Vorteil, ElWG-Änderung, Beitrittsschritte, optional Verzeichnis-Einträge.
    ergebnisse array Immer [] — Platzhalter des Leer-Vertrags.
    foerderungen object | null Offene, verifizierte Förderungen (Bund + Bundesland); geschlossene oder unsicher markierte Einträge sind ausgefiltert.
    grund string Warum leer — z.B. 'keine Lastgang-Serien am Account'.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    leer boolean true = Leer-Vertrag (errors.empty_result): kein Fehler, aber auch keine Ergebnisse. Die Nutzdatenfelder dieses Tools fehlen dann.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    thema string foerderungen | energieberatung | energiegemeinschaften | alles.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • get_data_release_status Gridbert-Domäne nur lesend

    Fragt den Status einer Datenfreigabe ab (pending/active/revoked/…) + den aktuellen Datenstand der Serie. WARTE-HINWEIS ist PFLICHT: nach jeder Bestätigung im Netzbetreiber-Portal meldet der Netzbetreiber die Freigabe zurück — gemessen war die Hälfte nach rund fünf Stunden durch, einzelne brauchten mehrere Tage (Wartezeit beim Netzbetreiber, nicht dein Fehler). Sag das dem User ehrlich, versprich KEINE Minutenangabe und poll nicht endlos: Gridbert prüft stündlich und mailt von selbst, sobald sie da ist — und meldet sich auch, wenn eine Anfrage ungewöhnlich lange steht. Erinnere bei 'pending' an die ZWEITE, spätere Freigabe-Anfrage für Stammdaten (Adresse/Zählerdaten) — kein Fehler, wenn sie erst Stunden nach der ersten erscheint. Ohne zaehlpunkt liefert es den Status ALLER Freigaben des Accounts. GRANULARITÄT: sobald Daten fließen, sagen 'aufloesung_minuten' und 'q15_aktiv', wie fein sie ankommen. Ist 'q15_empfehlung' gesetzt, liefert der Zähler nur grobe Werte (z. B. Tagessummen) — sprich die Empfehlung dann AKTIV aus: die Viertelstundenauslesung ist ein Opt-in, das der User bei seinem Netzbetreiber anfordern kann, und ohne sie sind Lastgang-Tools (analyze_load_profile, spot_backtest) nicht möglich. Bei 'q15_aktiv': null ist die Auflösung UNBEKANNT (noch keine Daten) — dann nichts über die Granularität behaupten.

    Eingabe

    zaehlpunkt string Optional — ohne Angabe: alle Freigaben des Accounts

    Ausgabe (oberste Ebene)

    ergebnisse array Immer [] — Platzhalter des Leer-Vertrags.
    freigaben array Eine Zeile je Freigabe.
    grund string Warum leer — z.B. 'keine Lastgang-Serien am Account'.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    leer boolean true = Leer-Vertrag (errors.empty_result): kein Fehler, aber auch keine Ergebnisse. Die Nutzdatenfelder dieses Tools fehlen dann.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • get_invoice_reading_task Gridbert-Domäne nur lesend

    Erster Schritt, wenn DU (dein Modell) die hochgeladene Rechnung lesen sollst, statt dass Gridbert ein eigenes Modell fragt. Gridbert ruft hier KEIN Modell auf: es lässt den deterministischen Regelleser laufen, bestimmt, welche Felder noch offen sind, und gibt dir den fertigen Leseauftrag — 'system' (Regelwerk + Ausgabeformat in Kompaktform), 'auftrag' (was zu lesen ist), 'dokument' und 'regelwerk_version'. ABLAUF in drei Schritten: (1) get_invoice_reading_task mit der rechnung_ref aus POST /accounts/rechnung; mit mit_dokument=true kommt die Rechnung (auf die nötigen Seiten gekürzt, ohne Metadaten) base64 im Ergebnis mit. (2) Lies das Dokument nach 'system' und 'auftrag' und antworte NUR mit dem JSON-Objekt in Kompaktform — wörtliche Zitate, nichts rechnen, nichts raten, Fehlendes weglassen. (3) check_invoice_reading mit rechnung_ref, der 'regelwerk_version' (unverändert) und deiner Antwort: es prüft deterministisch und liefert dasselbe Ergebnis wie read_invoice_facts, das du dem User zur Bestätigung vorlegst, bevor submit_invoice_facts es rechnet. lesen_noetig=false heisst: der Regelleser war sicher genug, du musst nichts lesen — dann check_invoice_reading mit antwort=null. Hat die Datei keine Textebene (Scan, Foto) und hast du eine eigene Texterkennung, gib ihren Text je Seite als seitentexte mit (Index 0 = Seite 1); Gridbert nutzt ihn für den Regelleser und die Zitatprüfung, vergibt auf fremdem Text aber höchstens Konfidenz 'mittel'. Rechnet nichts, speichert nichts.

    Eingabe

    mit_dokument boolean true = das vorbereitete Dokument (PDF gekürzt bzw. Foto verkleinert, ohne Metadaten) base64 im Ergebnis mitliefern. Vorgabe false.
    rechnung_ref* string Kennung aus POST /accounts/rechnung. Nicht raten und nicht aus dem Gespräch rekonstruieren — sie ist eine Stunde gültig.
    seitentexte array Optional: Text der Rechnung je Seite aus einer eigenen Texterkennung (Index 0 = Seite 1), für Dateien OHNE Textebene. Eine vorhandene Textebene geht vor.

    Ausgabe (oberste Ebene)

    auftrag string Anweisung an das Modell (Nutzerteil), berücksichtigt offene_felder. Leer bei lesen_noetig=false.
    dokument object Das Dokument, wie ein Modell es bekommen sollte: PDF auf die relevanten Seiten gekürzt und ohne Metadaten, Foto neu kodiert und ohne EXIF.
    empfehlung object Empfohlene Einstellung für das lesende Modell. Eine Empfehlung: das Regelwerk ist an diesem Modell gemessen, die Prüfung hängt nicht daran.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    lesen_noetig boolean false = der Regelleser hat alles sicher gelesen, es muss kein Modell lesen: check_invoice_reading mit antwort=null aufrufen.
    offene_felder array | null Felder, die der Regelleser nicht sicher kennt und die das Modell lesen soll. null = alle Felder (kein Regelleser-Ergebnis, z.B. bei Scans).
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    regelleser object | null Vorläufiger Stand des deterministischen Regellesers (Feld, Wert, Konfidenz, Zitat) — null, wenn er nichts lesen konnte (Scan, Foto).
    regelwerk_version string Version des Regelwerks, nach dem zu lesen ist. UNVERÄNDERT an check_invoice_reading zurückgeben.
    system string Regelwerk und Ausgabeformat (Kompaktform) als System-Text für das lesende Modell. Leer bei lesen_noetig=false.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • get_knowledge Rechenwerk (energietools) nur lesend

    Deterministische Auslieferung der Wissensschicht (wie sich AT-Stromkosten zusammensetzen o. ä.). Reine Text-Auslieferung, kein Rechen-Result. 'thema' ist ein fester Wiki-Slug — Kern-Themen: 'stromkosten-zusammensetzung', 'netz-netzentgelte', 'tarife', 'steuern', 'foerderung', 'glossar'. Bei unbekanntem Thema listet die Fehlermeldung die gültigen Slugs auf.

    Eingabe

    thema* string Wiki-Slug, z.B. 'stromkosten-zusammensetzung' (mit Bindestrich, nicht Unterstrich)

    Ausgabe (oberste Ebene)

    data object Wissens-Artikel der Capability 'get_knowledge'.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    meta object | null Herkunft der Rechnung/Daten aus energietools (quelle, stand, snapshot_version) — Inhalt variiert je Capability.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • get_page Gedächtnis (engram) nur lesend

    Liefert eine gespeicherte Seite im Volltext. Den Seiten-Key (Slug) bekommst du aus search_pages (Feld key) oder aus dem Ergebnis von compile_next/upsert_page; rate keinen Key.

    Eingabe

    key* string Seiten-Key (Slug), wie er in search_pages-Treffern steht.

    Ausgabe (oberste Ebene)

    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    proxied boolean immer true — die Nutzlast kommt aus der engram-Instanz, nicht aus dem Gateway.
    result object | null Rohes MCP-Tool-Result der Instanz: {content: [...], isError: bool}. Der eigentliche Inhalt steckt als JSON-Text in content[0].text.
    tool string Name des proxierten Vault-Tools.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • get_switch_info Gridbert-Domäne nur lesend

    Wechsel-Infos (Schritte, Fristen, Kontaktweg) aus Katalogdaten. Reine Information — KEIN Vollmacht-Flow, keine Durchführung.

    Eingabe

    lieferant string Name des Ziel-Lieferanten, optional.

    Ausgabe (oberste Ebene)

    fristen object Rücktrittsrecht, Kündigungsfrist, übliche Wechseldauer.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    hinweis_vollmacht string Klarstellung: Gridbert wechselt NICHT selbst.
    kontakt_hinweis string Nur mit Lieferant: wo der Kontaktweg steht.
    lieferant string Nur, wenn ein Lieferant übergeben wurde.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    schritte array Die generischen AT-Wechselschritte.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • ingest Gedächtnis (engram)

    Legt neue Rohdaten (JSON oder Text, z.B. eine genannte Tatsache) im Eingangsspeicher der Instanz ab. Sie werden nie überschrieben, nur ergänzt. Eingearbeitet werden sie danach über compile_next.

    Eingabe

    content* string Der abzulegende Inhalt (JSON oder Text).
    content_type string MIME-Type des Inhalts (Default 'application/json').
    source string Herkunft der Daten (frei wählbare Kennung).

    Ausgabe (oberste Ebene)

    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    proxied boolean immer true — die Nutzlast kommt aus der engram-Instanz, nicht aus dem Gateway.
    result object | null Rohes MCP-Tool-Result der Instanz: {content: [...], isError: bool}. Der eigentliche Inhalt steckt als JSON-Text in content[0].text.
    tool string Name des proxierten Vault-Tools.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • lastgang_heute Rechenwerk (energietools) nur lesend

    Normalband je Viertelstunde (P25/Median/P75 über die letzten 4 Wochen, DESSELBEN Tagtyps — Werktag/Samstag/Sonn-und-Feiertag) + drei Tagesaggregate mit Vergleichswert: Energie heute bisher, Tagesspitze, Monat bisher — jedes als Ist/Normal/Delta-Prozent. 'referenz' nennt IMMER Tagtyp, Fenster und Anzahl der Referenztage — ohne sie ist ein Vergleichswert eine Behauptung, nie unkommentiert lassen. Reicht die Datenlage für ein Aggregat nicht (z.B. Messlücke, zu wenige Referenztage vom selben Tagtyp), ist der jeweilige Normalwert null und der zugehörige '..._grund' erklärt warum — NIE einen Ersatzwert erfinden oder verschweigen, dass er fehlt. 'heute bisher'/'Tagesspitze' vergleichen bewusst NUR bis zur aktuellen Uhrzeit (nicht den ganzen Tag) — der Rest des Tages ist noch nicht gemessen. Braucht eine 15-min-Serie; bei reinen Tageswerten liefert 'referenz' ausreichend=false mit Grund statt eines Fehlers.

    Eingabe

    serie_ref* string aus list_load_series/request_data_release

    Ausgabe (oberste Ebene)

    data object Normalband-Ergebnis (energietools.capabilities.lastgang.normalband.NormalbandCompute, unverändert durchgereicht).
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • list_load_series Gridbert-Domäne nur lesend

    Listet alle Lastgang-Serien am Account (Zählpunkt MASKIERT, Richtung, Zeitraum, Coverage, Granularität, Datenstand). IMMER als ERSTER Schritt aufrufen: bevor eine neue Datenfreigabe angefordert wird (keine doppelte Freigabe-Anfrage) und bevor ein Analyse-Tool (analyze_load_profile/load_trend/spot_backtest) aufgerufen wird — liefert den serie_ref, den die Analyse-Tools brauchen.

    Ausgabe (oberste Ebene)

    ergebnisse array Immer [] — Platzhalter des Leer-Vertrags.
    grund string Warum leer — z.B. 'keine Lastgang-Serien am Account'.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    leer boolean true = Leer-Vertrag (errors.empty_result): kein Fehler, aber auch keine Ergebnisse. Die Nutzdatenfelder dieses Tools fehlen dann.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    serien array Eine Zeile je Serie.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • list_open_lastgang_questions Gridbert-Domäne nur lesend

    Listet die offenen Rückfragen dieses Kontos zu Lastgang-Auffälligkeiten (z.B. 'was lief da nachts?') — Frage, Optionen und Zeitraum je Ereignis, damit du die Frage im Chat stellen kannst. 'Offen' heißt HIER: noch nicht beantwortet, egal ob schon per Mail verschickt — ein Konto, das im Chat von einem Gerät erzählt, muss dieselbe Frage nicht nochmal per Mail bekommen. Kein Input: das Konto kommt immer aus der Anmeldung, nie als Parameter. Rufe danach answer_lastgang_question mit der ereignis_id auf, sobald der User geantwortet hat.

    Ausgabe (oberste Ebene)

    ergebnisse array Immer [] — Platzhalter des Leer-Vertrags.
    fragen array Eine Zeile je offener Frage.
    grund string Warum leer — z.B. 'keine Lastgang-Serien am Account'.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    leer boolean true = Leer-Vertrag (errors.empty_result): kein Fehler, aber auch keine Ergebnisse. Die Nutzdatenfelder dieses Tools fehlen dann.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • load_trend Rechenwerk (energietools) nur lesend

    Mehrjahres-Trend (YoY) + Treiber-Zerlegung nach Leistungsband x Tageszeit x Werktag/Wochenende aus einer Lastgang-Serie. trend: Vorjahresvergleich je Kalenderjahr nur bei >=2 vollen Kalenderjahren — sonst Vorjahresvergleich über deckungsgleiche Zeitfenster (Monat, Tag, Stunde, Minute); NIE ein Teiljahr gegen ein Volljahr stellen; läuft auch auf Tageswerten weiter (Granularitäts-Caveat). attribution benennt je Treiber eine Geräte-KLASSE als Hypothese — NIE einen Gerätenamen (Viertelstundenwerte reichen dafür nicht aus) — nur mit >=2 Jahren Viertelstunden-Daten verfügbar, sonst attribution=null + attribution_grund (trend bleibt trotzdem verfügbar).

    Eingabe

    jahr_a integer attribution-Basisjahr, leer = zweitjüngstes Jahr der Serie
    jahr_b integer attribution-Vergleichsjahr, leer = jüngstes Jahr der Serie
    serie_ref* string aus list_load_series/request_data_release

    Ausgabe (oberste Ebene)

    data object Zusammengeführtes Ergebnis (lastgang_analysis.run_load_trend).
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    meta object | null Herkunft der Rechnung/Daten aus energietools (quelle, stand, snapshot_version) — Inhalt variiert je Capability.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • pv_potenzial Rechenwerk (energietools) nur lesend

    NUR bei vorhandener Lastgang-Serie (serie_ref aus list_load_series) mit Viertelstundenwerten. Beantwortet 'Was würde mir eine PV-Anlage bringen?' auf dem ECHTEN Verbrauchsverlauf statt auf einem Standardprofil: je Anlagengröße Ertrag, Eigenverbrauch, Eigenverbrauchsquote, Autarkie, Einspeisung, vermiedene Netzkosten und Amortisation. Das Sonnenertragsprofil des Standorts holt der Gateway (PVGIS) — NIE vom User-LLM. Verweigert bei Tageswerten mit Begründung, weil sich daraus die Tageszeit des Verbrauchs nicht ablesen lässt. Liefert zwei getrennte Ergebnisse: 'gemessen' (nichts modelliert) und 'jahr_modelliert' (Tagessummen + gemessene Tagesform, als Modell gekennzeichnet). Euro-Beträge gibt es nur auf Jahresbasis — ein Teiljahr wird NICHT hochgerechnet. Die Einspeisevergütung ist eine Annahme und wird als Szenario-Band gerechnet.

    Eingabe

    arbeitspreis_netto_ct_kwh number Arbeitspreis netto in ct/kWh (Alternative zu energiepreis_brutto_ct_kwh).
    ausrichtung string Süd, Südost, Südwest, Ost, West (Default Süd)
    diskontrate number Diskontrate für die Kapitalwertrechnung (z.B. 0.04 = 4%).
    einspeise_szenarien_ct_kwh array Annahme-Band, Default [3, 5, 8] netto ct/kWh
    energiepreis_brutto_ct_kwh number Arbeitspreis von der Rechnung (brutto)
    gemeinde string Gemeinde des Anschlusses. Nur nötig, wenn die Antwort danach fragt: bei geteilten PLZ (z.B. 1140, 1210) liegen zwei Netzgebiete nebeneinander.
    gemessene_anlage_kwp number Installierte Leistung der BESTEHENDEN Anlage in kWp, zu der gemessener_jahresertrag_kwh gehört. Gridbert rechnet daraus den Ertrag je kWp. Nicht mit kwp_varianten verwechseln — das sind die durchzurechnenden Größen, das hier ist die reale.
    gemessener_jahresertrag_kwh number NUR bei einer BESTEHENDEN Anlage: tatsächlich gemessener Jahresertrag in kWh (vom Wechselrichter/Zähler). Ersetzt den PVGIS-Modellertrag — sinnvoll bei Verschattung, Mischausrichtung oder Balkonkraftwerken, wo das Modell weit danebenliegt. Nur zusammen mit gemessene_anlage_kwp verwenden; beide Zahlen erfragen, nie schätzen.
    investition_eur_pro_kwp object Investition brutto EUR/kWp je Größe, z.B. {"10": 1400}
    kwp_varianten array Anlagengrößen in kWp, Default [5, 10]
    neigung_grad integer Default 35
    nutzungsdauer_jahre integer Nutzungsdauer der Anlage in Jahren für die Amortisationsrechnung.
    plz* string PLZ des Standorts, für das Sonnenertragsprofil (PVGIS).
    serie_ref* string aus list_load_series/request_data_release
    speicher_kwh number Zusatzvariante mit Speicher (0 = ohne)

    Ausgabe (oberste Ebene)

    data object Ergebnis der Capability 'pv_potenzial' (+ gateway-seitiger Standort).
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    meta object | null Herkunft der Rechnung/Daten aus energietools (quelle, stand, snapshot_version) — Inhalt variiert je Capability.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • read_invoice_facts Gridbert-Domäne nur lesend

    Liest aus einer hochgeladenen Rechnung (rechnung_ref aus POST /accounts/rechnung) die Felder, die submit_invoice_facts braucht — je Feld mit wörtlichem Zitat von der Rechnung und einer Konfidenz (hoch|mittel|niedrig|fehlt). Rechnet NICHTS und speichert NICHTS: die gelesenen Werte gehören dem User vorgelegt und erst nach seiner Bestätigung an submit_invoice_facts übergeben. Ein Feld mit konfidenz='niedrig' oder 'mittel' ist zu bestätigen, eines mit 'fehlt' zu erfragen — nie selbst ergänzen und nie aus dem Gesprächsverlauf raten. 'mittel' heisst zusätzlich: der Wert stammt aus einer gescannten Seite. Wer nur drei Stufen kennt, darf den vierten Wert NICHT wegwerfen — er ist da und er ist vorzulegen. Beträge und Mengen kommen SYSTEMATISCH als 'niedrig' zurück; das ist kein Mangel, sondern die gemessene Erkennungsgüte (Verbrauch 86 %, Rechnungsbetrag 90 %) — sie sind ausnahmslos vorzulegen. pruefbar=false heisst, die Rechnung ist keine prüfbare Jahres-, Zwischen- oder Monatsrechnung (Teilbetragsvorschreibung, Einspeisung, Energiegemeinschaft, Grosskunde über 100.000 kWh, ausländische Rechnung, keine Energierechnung): dann 'ablehnungsgrund' dem User im Klartext sagen und KEINE Prüfung anbieten. 'quellen_anker' ist fertig für die Übergabe an submit_invoice_facts aufgebaut, nicht neu bauen. Fotos, Scans und PDFs ohne Textebene werden gelesen (gelesen_mit='ocr', Werte daraus höchstens 'mittel'); not_configured kommt nur, wenn auf diesem Server gar kein Leseweg dafür eingerichtet ist. Dieses Werkzeug ruft Gridberts eigenes Modell auf; wer ein eigenes Modell mitbringt, nimmt stattdessen get_invoice_reading_task und check_invoice_reading.

    Eingabe

    leser string vollstaendig (Vorgabe): der ganze Lesevorgang. schnell: nur die Textebene, in Sekunden — für eine erste Anzeige; bei einem Scan kommt ein leerer Befund mit gelesen_mit='ausstehend'. Danach vollstaendig aufrufen.
    rechnung_ref* string Kennung aus POST /accounts/rechnung. Nicht raten und nicht aus dem Gespräch rekonstruieren — sie ist eine Stunde gültig.

    Ausgabe (oberste Ebene)

    ablehnungsgrund string | null teilbetrag | einspeisung | energiegemeinschaft | grosskunde | ausland | keine_energierechnung — oder null, wenn prüfbar.
    einmalrabatte_brutto_eur number | null Einmaleffekte dieser Rechnung in €, brutto und positiv — oder null: befristete und einmalige Rabatte des Lieferanten (Neukunden-, Wechsel-, Aktionsbonus, Gratis-/Freimonate) × 1,2 plus ein Stromkostenzuschuss, der im Rechnungsbetrag schon abgezogen ist. Dauerhafte Nachlässe (Online, Bankeinzug, Bestandskunde) zählen nie, sie stecken im Arbeitspreis; ein Zuschuss ausserhalb des Betrags nie. Wiederkehrende Kosten der Periode = rechnungsbetrag_brutto_eur + einmalrabatte_brutto_eur. Gerechnet, nicht gelesen; NICHT an submit_invoice_facts weitergeben.
    energieart string strom | gas | kombi | unbekannt.
    energiepositionen array Die EINZELNEN Zeilen des Energieblocks, wenn die Rechnung dort mehr als eine führt (z.B. Spotpreis, Stromqualität, Mehrkosten Ökoenergie/HKN). Jeder Eintrag ist fertig für submit_invoice_facts: {bezeichnung, wert_eur, ist_netto}. **Unverändert weiterreichen, nichts ergänzen** — ein viertes Feld lässt die ganze Einreichung ablehnen. LEERE LISTE heisst 'keine erkannt', NICHT 'der Energieblock ist leer': dann wird der Block wie bisher über Arbeitspreis × Menge nachgerechnet. Die Belege dazu stehen in quellen_anker unter dem Feldnamen 'energiepositionen'.
    felder array Ein Eintrag je gelesenem Feld.
    gelesen_mit string textebene = aus der Textebene des PDFs gelesen (nur dann kann ein Feld 'hoch' sein). ocr = aus einem Scan oder Foto bzw. aus Seitentexten des Aufrufers gelesen (höchstens 'mittel').
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    pruefbar boolean false = keine prüfbare Jahres-, Zwischen- oder Monatsrechnung. Dann steht der Grund in 'ablehnungsgrund' und es gibt keine Prüfung.
    quellen_anker array Fertig für die Übergabe an submit_invoice_facts: Objekte {feld, zitat}. NICHT neu bauen — dort wird geprüft, ob der übergebene Zahlenwert im Zitat wirklich vorkommt.
    rabatt_ausserhalb_bloecke_eur number | null Rabatt-/Bonuszeile AUSSERHALB der drei Blöcke in €, netto und negativ (Rabatt = Minuszahl) — oder null. Nur gesetzt, wenn die Rechnung damit aufgeht. Unverändert an submit_invoice_facts weitergeben: (Summe Blöcke + Rabatt) × 1,2 − Zuschuss = Rechnungsbetrag.
    rabatt_im_energieblock_eur number | null Summe der gelesenen Rabatte des Lieferanten in €, netto und positiv — oder null. Unverändert an submit_invoice_facts weitergeben: die Nachrechnung zieht ihn nur ab, wenn der Energieblock danach aufgeht.
    rabatte array Die Rabatt- und Bonuszeilen des Lieferanten hinter einmalrabatte_brutto_eur. Nur zur Anzeige; NICHT an submit_invoice_facts weitergeben.
    rechnungstyp string jahresrechnung | zwischenrechnung | monatsrechnung | unbekannt.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • request_data_release Gridbert-Domäne

    Fordert die Datenfreigabe für einen Zählpunkt an (der Kunde gibt dem Netzbetreiber seine Zustimmung zur Datenweitergabe an Gridbert). Der Zählpunkt MUSS aus den Invoice-Fakten der Instanz stammen (get_page/search_pages) — nicht vom User abtippen lassen. Nur aufrufen, wenn list_load_series noch keine aktive Freigabe für diesen Zählpunkt zeigt (keine doppelte Freigabe-Anfrage). energy_direction ist PFLICHT (consumption|generation) — bewusst setzen, NICHT raten: eine falsche Richtung blockiert den Zählpunkt beim Netzbetreiber (die Anfrage bleibt lange auf 'pending', jeder weitere Versuch endet mit einem Fehler). Ist die Richtung nicht klar aus den Invoice-Fakten ableitbar, den User zuerst fragen: 'Hast du eine PV-Anlage mit Einspeisung an diesem Zählpunkt?' Kündige dem User EXAKT an: 'Du bekommst zwei Freigabe-Anfragen im Netzbetreiber-Portal unter Freigaben→Offen (Provider EP100505) — zuerst die Energiedaten, wenige Stunden später eine zweite für Stammdaten (Adresse/Zählerdaten).' Beide sind eigene Freigaben mit eigenem Ablauf — die zweite (Stammdaten) schickt der Netzbetreiber automatisch, du musst sie nicht selbst auslösen, aber im Portal ebenfalls bestätigen. Nach jeder Bestätigung meldet der Netzbetreiber die Freigabe zurück; wie lange das dauert, ist sehr verschieden — gemessen war die Hälfte nach rund fünf Stunden durch, einzelne brauchten mehrere Tage (ehrlich sagen, keine Live-Fortschrittsanzeige, keine Minutenangabe versprechen). Gridbert prüft stündlich und mailt von selbst, sobald sie da ist. Bei bestätigter PV-Einspeisung: ZWEITER Aufruf mit energy_direction='generation' und dem Einspeise-Zählpunkt (eigene Nummer, i.d.R. Suffix …9901).

    Eingabe

    energy_direction* string PFLICHT, bewusst setzen: 'consumption' für Bezug, 'generation' NUR für den separaten Einspeise-Zählpunkt bei PV. Nicht ableitbar? Erst den User fragen, nicht raten.
    plz* string PLZ des Zählpunkts (4-stellig).
    zaehlpunkt* string 33-stelliger AT-Zählpunkt aus den Invoice-Fakten (kein Abtippen durch den User)

    Ausgabe (oberste Ebene)

    bereits_freigegeben boolean Nur im Bestandsfall: true = es lief schon eine Freigabe, kein zweite Freigabe-Anfrage.
    energy_direction string Nur bei frischer Anforderung: 'consumption' oder 'generation'.
    esp_provider string Nur bei frischer Anforderung: Provider-Kennung im Portal.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    portal_pfad string Nur bei frischer Anforderung: 'Freigaben → Offen'.
    serie_ref string Referenz auf die Serie — der Schlüssel für alle Lastgang-Tools.
    status string Freigabe-Status: 'pending' nach frischer Anforderung, sonst der Stand der bestehenden Freigabe ('pending'|'active').
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
    zaehlpunkt_masked string Zählpunkt MASKIERT. Der Klartext verlässt den Gateway nie.
  • search_pages Gedächtnis (engram) nur lesend

    Volltextsuche über die gespeicherten Seiten der Instanz (reine Textsuche, ohne KI-Auswertung) — das GEDÄCHTNIS des Haushalts. Hier liegen aus Rechnungen gemerkte Fakten (Jahresverbrauch, Arbeits-/Grundpreis, Lieferant, Zeitraum) UND selbst genannte Haushalts-Fakten (Heizart, Geräte, PV, …). ZUERST aufrufen bei JEDER Frage zu dem, was der User schon eingebracht hat — Verbrauch, Kosten, Tarif, Lieferant, WIE er heizt —, BEVOR du aus einem Lastprofil-Signal oder einer Schätzung etwas ableitest, nach manuellen Daten fragst oder 'weiß ich nicht' sagst. Ein gespeicherter Fakt gilt immer mehr als eine Schätzung. Suchtext als 'query'; 'limit' optional (Default 10). Die Treffer enthalten den key, den get_page braucht.

    Eingabe

    limit integer Maximale Trefferzahl (Default 10).
    query* string Suchtext (Stichworte oder eine Frage).

    Ausgabe (oberste Ebene)

    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    proxied boolean immer true — die Nutzlast kommt aus der engram-Instanz, nicht aus dem Gateway.
    result object | null Rohes MCP-Tool-Result der Instanz: {content: [...], isError: bool}. Der eigentliche Inhalt steckt als JSON-Text in content[0].text.
    tool string Name des proxierten Vault-Tools.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • speicher_dimensionierung Rechenwerk (energietools) nur lesend

    NUR bei vorhandener Lastgang-Serie in Viertelstunden-Auflösung. Beantwortet 'Wie groß soll der Batteriespeicher sein?': je Größe Eigenverbrauchsquote, Autarkie, Vollzyklen, Amortisation, Kapitalwert — und den Grenznutzen, also ab welcher Größe die nächste Kilowattstunde fast nichts mehr bringt. Sagt auch ehrlich Nein, wenn keine Größe einen positiven Kapitalwert erreicht. Speicher-Alterung ist NICHT eingepreist; die Vollzyklen je Größe stehen im Ergebnis. Braucht die Anlagengröße (kwp).

    Eingabe

    arbeitspreis_netto_ct_kwh number Arbeitspreis netto in ct/kWh (Alternative zu energiepreis_brutto_ct_kwh).
    ausrichtung string Süd, Südost, Südwest, Ost, West (Default Süd).
    diskontrate number Default 0.04
    einspeise_netto_ct_kwh number Default 5
    energiepreis_brutto_ct_kwh number Arbeitspreis brutto in ct/kWh.
    gemeinde string Gemeinde des Anschlusses. Nur nötig, wenn die Antwort danach fragt: bei geteilten PLZ (z.B. 1140, 1210) liegen zwei Netzgebiete nebeneinander.
    gemessene_anlage_kwp number Installierte Leistung in kWp, zu der der gemessene Jahresertrag gehört. Meist dieselbe Zahl wie kwp.
    gemessener_jahresertrag_kwh number NUR bei einer BESTEHENDEN Anlage: tatsächlich gemessener Jahresertrag in kWh. Ersetzt den PVGIS-Modellertrag — bei Verschattung oder Mischausrichtung liegt das Modell weit daneben, und die Speichergröße hängt daran. Nur zusammen mit gemessene_anlage_kwp; beide erfragen, nie schätzen.
    groessen_kwh array Speichergrößen, Default [2.5, 5, 7.5, 10, 15]
    kwp* number Anlagengröße in kWp
    neigung_grad integer Dachneigung in Grad (Default 35).
    nutzungsdauer_jahre integer Default 15
    plz* string PLZ des Lieferorts (4-stellig).
    serie_ref* string aus list_load_series/request_data_release
    speicher_kosten_eur_pro_kwh number Default 600

    Ausgabe (oberste Ebene)

    data object Ergebnis der Capability 'speicher_dimensionierung' (+ Standort).
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    meta object | null Herkunft der Rechnung/Daten aus energietools (quelle, stand, snapshot_version) — Inhalt variiert je Capability.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • spot_backtest Rechenwerk (energietools) nur lesend

    NUR bei vorhandener Lastgang-Serie (serie_ref aus list_load_series). Für den normalen Tarifvergleich aus einer Rechnung ist dieses Tool NICHT nötig — dafür tariff_compare, das ganz ohne Lastgang rechnet. Profilgewichteter Spot-Backtest (echter Verbrauchs-Shape x EPEX-Stundenpreise, aus Gridberts eigener Datenbank — NIE vom User-LLM übergeben) vs. aktueller Fixpreis, plus Tarifwechsel-Ersparnis. tarif_ersparnis ist nur verfügbar, wenn plz/energiepreis_brutto_ct_kwh/aktuelle_grundgebuehr_brutto_eur_monat/aktueller_lieferant/jahresverbrauch_kwh vollständig sind — sonst verfuegbar=false + grund (nie eine stille 0). Hebel-Reihenfolge: Tarif zuerst prüfen. Verweigert bei Tageswerten mit Begründung.

    Eingabe

    aktuelle_grundgebuehr_brutto_eur_monat number Grundgebühr brutto in €/Monat.
    aktueller_lieferant string Name des aktuellen Stromlieferanten.
    aufschlag_ct number Annahme (Lieferanten-Aufschlag), Default aus energietools
    energiepreis_brutto_ct_kwh number Arbeitspreis brutto in ct/kWh.
    jahresverbrauch_kwh number Jahresverbrauch in kWh.
    plz string PLZ des Lieferorts (4-stellig).
    serie_ref* string aus list_load_series/request_data_release

    Ausgabe (oberste Ebene)

    data object Ergebnis der Capability 'spot_backtest'.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    meta object | null Herkunft der Rechnung/Daten aus energietools (quelle, stand, snapshot_version) — Inhalt variiert je Capability.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • submit_invoice_facts Gridbert-Domäne

    Übergibt EXAKT abgelesene Rechnungswerte + wörtliche Zitate. Validiert deterministisch (energieart muss strom|gas|kombi sein — Nicht-Energie-Rechnungen z.B. Restaurant werden mit error_class=validation_rejected und benanntem Grund abgelehnt, nichts wird gespeichert), rechnet finalize, schreibt die Fakten-Submission in den Raw-Store. PFLICHT: energieart, lieferant, plz, verbrauch_kwh, zeitraum_von, zeitraum_bis (ISO YYYY-MM-DD oder DE TT.MM.JJJJ) und quellen_anker. Zusätzlich muss arbeitspreis (ct/kWh) ODER summe_energieentgelte vorliegen. quellen_anker sind Objekte {feld, zitat} — je ein wörtliches Zitat für den Verbrauch und einen Betrag, z.B. quellen_anker=[{"feld":"verbrauch_kwh","zitat":"Jahresverbrauch 2562,3 kWh"},{"feld":"arbeitspreis","zitat":"Arbeitspreis 16,02 ct/kWh"}]. Lehnt bei Unplausibilität feld-genau mit Rückfrage ab (nichts wird gespeichert). Dieses Tool speichert nur — die Frage 'zahle ich zu viel?' beantwortet erst tariff_compare, das danach im selben Zug zu rechnen ist.

    Eingabe

    arbeitspreis object Arbeitspreis: {wert_ct_kwh, ist_netto}. Erforderlich, falls keine summe_energieentgelte.
    energieart* string Energieart der Rechnung: 'strom', 'gas' oder 'kombi'.
    energiepositionen array | null Die EINZELNEN Positionen des Energieblocks, wenn die Rechnung dort mehr als eine Zeile führt (z.B. Spotpreis, Stromqualität, Mehrkosten Ökoenergie/HKN). Optional — ohne sie wird der Energieblock als Arbeitspreis × Menge nachgerechnet, was bei einem mehrteiligen Block einen Fehlbefund ergibt. read_invoice_facts liefert sie fertig; unverändert weiterreichen, nichts ergänzen. **Die Grundpreis-/Grundgebühr-Zeile gehört NICHT hier hinein**, wenn sie zusätzlich unter 'grundgebuehr' übergeben wird: sie würde zweimal gerechnet, und der Energieblock sähe um die Gebühr zu günstig aus. Im Zweifel die Positionen ganz weglassen.
    grundgebuehr object Grundgebühr: {wert_eur, zeitraum(monat|jahr), ist_netto} plus optional block und aliquotierung.
    jahresverbrauch_prognose_kwh number Der von der Rechnung PROGNOSTIZIERTE Jahresverbrauch in kWh, falls ausgewiesen — nicht der Verbrauch des Abrechnungszeitraums (der steht in verbrauch_kwh).
    lieferant* string Name des Lieferanten auf der Rechnung.
    plz* string PLZ des Lieferorts (4-stellig).
    quellen_anker* array Wörtliche Belegstellen je Feld — mindestens ein Zitat für Verbrauch und einen Betrag.
    rabatt_ausserhalb_bloecke_eur number | null Rabatt-/Bonuszeile außerhalb der drei Blöcke, netto in € und negativ (Rabatt = Minuszahl) — so wie read_invoice_facts sie liefert, unverändert weitergeben. Die Nachrechnung rechnet (Summe Blöcke + Rabatt) × 1,2 − Zuschuss.
    rabatt_im_energieblock_eur number | null Rabatt des Lieferanten, netto und positiv in € — so wie read_invoice_facts ihn liefert, unverändert weitergeben. Die Nachrechnung zieht ihn nur ab, wenn der Energieblock danach aufgeht (z. B. Online-Bonus als Abzugszeile).
    rechnungsbetrag_brutto_eur number Der ausgewiesene Rechnungsbetrag brutto in €. Ohne ihn gibt es keine Summenprobe (Energie + Netz + Abgaben gegen den Betrag).
    summe_energieentgelte object Summe des Energieentgelt-Blocks in € (Zeile 'Energiekosten'/'Summe Energieentgelte'): {wert_eur, ist_netto}. Erforderlich, falls kein arbeitspreis — z.B. wenn nur eine Blocksumme auf der Rechnung steht, kein ct/kWh-Wert.
    summe_netzentgelte object Summe des Netzentgelt-Blocks in € (Zeile 'Summe Netzentgelte'/'Summe Netz'): {wert_eur, ist_netto}.
    summe_steuern_abgaben object Summe des Abgabenblocks in €: {wert_eur, ist_netto}. Die Zeile heißt je nach Versorger 'Summe Steuern und Abgaben', 'Summe Abgaben/Zuschläge' oder 'Summe Steuern, Abgaben und Zuschläge' — gemeint ist immer die Blocksumme, nicht eine einzelne Abgabe. Führt die Rechnung MEHRERE Blöcke neben Energie und Netz (z.B. zusätzlich 'Summe Sonstige'), gehört deren Summe hier zusammengefasst herein, sonst geht die Gesamtsumme nicht auf. Ohne sie ist die Rechnung nicht nachrechenbar.
    tarif_name string Produktname des Tarifs, wie er auf der Rechnung steht.
    verbrauch_kwh* number Verbrauch im Abrechnungszeitraum in kWh.
    zaehlpunkt string Zählpunkt von der Rechnung (33 Zeichen, AT + 31 Ziffern; Punkte und Leerzeichen egal). Optional — aber wenn er auf der Rechnung steht, mitgeben: er benennt das Netzgebiet, das sonst aus der PLZ erschlossen wird, und die liegt falsch, wo ein Netzbetreiber über die Bundeslandgrenze versorgt. Liegt eine Datenfreigabe vor, ergänzt der Gateway ihn selbst.
    zeitraum_bis* string Abrechnungsende, ISO YYYY-MM-DD oder DE TT.MM.JJJJ
    zeitraum_von* string Abrechnungsbeginn, ISO YYYY-MM-DD oder DE TT.MM.JJJJ
    zuschuss_im_betrag_eur number Umsatzsteuerfreier Zuschuss, der im Rechnungsbetrag bereits abgezogen ist (z. B. Stromkostenzuschuss 2022–2024), positiv in €. Nur setzen, wenn er als Abzugszeile auf der Rechnung steht — die Summenprobe rechnet ihn zurück.

    Ausgabe (oberste Ebene)

    gespeichert boolean immer true — bei Ablehnung wird NICHTS gespeichert.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    rechnung object | null Ergebnis der Capability 'finalize_invoice' — der Rechenweg, aus dem die Eingaben für tariff_compare stammen (nicht erneut beim User erfragen).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • submit_lastgang_facts Gridbert-Domäne

    Speichert Profil-Fakten zum Haushalt (Heizung, PV-Eckdaten, Dauerläufer, Lastverschiebung, Heizstromtarif, Viertelstunden-Auslesung beantragt). ZWEI Anlässe, beide richtig: (1) die beantworteten Rückfragen aus der Lastgang-Analyse; (2) alles, was der User von sich aus über seinen Verbrauch erzählt — typischerweise direkt nach analyze_load_profile, wenn er die Auffälligkeit auf dem Schirm hat und sie ungefragt erklärt ('der Verbrauch in der Früh ist der Boiler', 'das Auto lade ich unregelmäßig'). Speichere das, statt es im Gesprächsverlauf zu lassen: aus Viertelstundenwerten ist es nie wieder ableitbar. Liegt dagegen eine KONKRETE offene Rückfrage zu einem Ereignis vor, gehört sie über answer_lastgang_question beantwortet — nur so wird sie als erledigt vermerkt. Validiert deterministisch gegen die bekannten Felder — unbekannte Felder oder unplausible Werte werden mit error_class=validation_rejected abgelehnt (nichts wird gespeichert). Gleiche Ablehnungs-Regeln wie submit_invoice_facts.

    Eingabe

    fakten* array Liste der Fakten als Feld/Wert/Beleg-Objekte — beantwortete Rückfragen ebenso wie ungefragt Erzähltes.

    Ausgabe (oberste Ebene)

    anzahl_fakten integer Wie viele Fakten übernommen wurden.
    gespeichert boolean immer true — bei Ablehnung wird NICHTS gespeichert.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • tariff_compare Rechenwerk (energietools) nur lesend

    Vergleicht den aktuellen STROMtarif mit tagesaktuellen Alternativen (Rechenweg je Alternative). Braucht KEINEN Lastgang und KEINE serie_ref — die fünf Rechnungswerte unten reichen (serie_ref gehört zu spot_backtest/load_trend, nicht hierher). Bei Tarif-/Preisfragen dieses Tool nutzen statt Websuche: der Katalog ist tagesaktuell und jede Zahl kommt mit Rechenweg. Aktuell nur Strom: energieart ist PFLICHT (strom|gas) und 'gas' wird sauber als not_supported abgelehnt — setze es bewusst, nicht raten. Erfrage beim User zuerst: PLZ, Jahresverbrauch (kWh), aktueller Lieferant, Arbeitspreis (ct/kWh brutto), Grundgebühr (€/Monat brutto). Beträge in €/kWh bzw. €/Jahr vorher in ct/kWh bzw. €/Monat umrechnen. Je Tarif im Ergebnis: rabatt_befristet=true bedeutet ein NUR im 1. Jahr wirkender Neukundenbonus — dann jahreskosten_jahr2_eur (Preis ab Jahr 2) mit ausweisen, den Bonus NICHT als Dauer-Ersparnis verkaufen. Beachte netzkosten_vollstaendig: ist es false, ist kein Netzbetreiber hinterlegt und der €-Betrag heißt energiepreis_anteil_eur (nur Energiepreis-Anteil, NICHT jahreskosten_eur) — dem User NICHT als Gesamtrechnung präsentieren. Die VNB-Auflösung macht der Gateway (nb_key vorgelöst): gib den zaehlpunkt mit, wenn du ihn hast. netzbetreiber_herkunft im Ergebnis sagt, worauf sie beruht — bei 'plz' ist der Netzbetreiber ANGENOMMEN (und die Netzkosten mit ihm), bei 'zaehlpunkt' belegt. Vor einer Gesamtkosten-Aussage auf 'plz'-Basis den Zählpunkt erfragen, statt die Zahl als exakt zu verkaufen. Bei netzbetreiber_herkunft='plz_mehrdeutig' (PLZ mit zwei Netzbetreibern, kein Zählpunkt) die Spanne (netzkosten_spanne_eur_jahr, je Tarif gesamtkosten_spanne_eur) mit beiden Netzbetreibern nennen und nach dem Zählpunkt von der Rechnung fragen.

    Eingabe

    aktuelle_grundgebuehr_brutto_eur_monat* number Grundgebühr brutto in €/Monat (nicht €/Jahr)
    aktueller_energiepreis_brutto_ct_kwh* number Arbeitspreis brutto in ct/kWh (nicht €/kWh)
    aktueller_lieferant* string Name des aktuellen Stromlieferanten.
    energieart* string Pflicht: 'strom' (aktuell einziger Support) oder 'gas' (→ not_supported)
    jahresverbrauch_kwh* number Jahresverbrauch in kWh.
    plz* string PLZ des Lieferorts (4-stellig).
    zaehlpunkt string Optional, aber für die Netzkosten der bessere Schlüssel: 33-stelliger AT-Zählpunkt aus den Invoice-Fakten oder list_load_series (kein Abtippen durch den User). Er benennt den Netzbetreiber eindeutig; ohne ihn wird er aus der PLZ angenommen und liegt in Gemeinden mit fremdem Netzbetreiber falsch.

    Ausgabe (oberste Ebene)

    data object Vergleichsergebnis der energietools-Capability 'tariff_compare'.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    meta object | null Herkunft der Rechnung/Daten aus energietools (quelle, stand, snapshot_version) — Inhalt variiert je Capability.
    netzbetreiber_herkunft string | null Worauf die Netzbetreiber-Auflösung beruht: 'zaehlpunkt' = belegt, 'plz' = ANGENOMMEN (und mit ihm die Netzkosten), 'plz_mehrdeutig' = die PLZ gehört zu mehreren Netzbetreibern, kein Zählpunkt: Netzkosten als Spanne (netzkosten_spanne_eur_jahr), nach dem Zählpunkt fragen. Gateway-seitig gesetzt, nicht von energietools.
    netzbetreiber_kandidaten array NUR bei 'plz_mehrdeutig': die infrage kommenden Netzbetreiber mit ihren Netzkosten für den angegebenen Verbrauch.
    netzkosten_spanne_eur_jahr object NUR bei 'plz_mehrdeutig': niedrigste und höchste Netzkosten im Jahr.
    netzkosten_spanne_rechenweg any NUR bei 'plz_mehrdeutig': Rechenweg der Netzkosten-Spanne (No-LLM-Math).
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • upsert_page Gedächtnis (engram)

    Speichert eine Seite genau so, wie du sie übergibst (1:1, keine KI-Bearbeitung). Der Server prüft hier nur, dass die Seite nicht verwaist (nirgends verlinkt) ist; die Quellen-Anker und die Typisierung werden erst bei compile_done geprüft. Überschreibschutz: eine bestehende Seite wird nur mit overwrite=true ersetzt — lies sie vorher mit get_page. Bei einer bestehenden Seite werden Text UND Titel ersetzt: den aktuellen Titel mitgeben, sonst wird er überschrieben.

    Eingabe

    body* string Voller Seitentext (Markdown), wird 1:1 gespeichert.
    key* string Seiten-Key (Slug); bei bestehenden Seiten den Key aus search_pages nehmen.
    overwrite boolean true = bestehende Seite ersetzen; Default false = Ablehnung, wenn sie schon existiert.
    title* string Seitentitel. Wird bei jedem Schreiben gesetzt, auch bei einem Update (overwrite=true) — bei bestehenden Seiten den aktuellen Titel (aus get_page) mitgeben, sonst wird er überschrieben.

    Ausgabe (oberste Ebene)

    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    proxied boolean immer true — die Nutzlast kommt aus der engram-Instanz, nicht aus dem Gateway.
    result object | null Rohes MCP-Tool-Result der Instanz: {content: [...], isError: bool}. Der eigentliche Inhalt steckt als JSON-Text in content[0].text.
    tool string Name des proxierten Vault-Tools.
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.

Berater-Werkzeuge

Ein Beraterkonto sieht zusätzlich 6 Portfolio-Werkzeuge. Für Zugriff auf einen Kundenbereich gibst du deinem Berater die Freigabe im eigenen Konto, nicht umgekehrt.

  • invite_client Berater-Portfolio

    Lädt einen Kunden per E-Mail-Adresse ein, den eigenen Bereich für diesen Berater freizugeben. Funktioniert auch, wenn der Kunde noch kein Gridbert-Konto hat. Der Kunde entscheidet selbst: die Einladung verschafft KEINEN Zugriff, sie führt nur zu einer Anfrage, der er in seinem Konto zustimmen muss. Das Result enthält einen Einladungslink, den der Berater auch selbst weitergeben kann.

    Eingabe

    bezeichnung* string Anzeigename dieses Mandats im Berater-Portfolio, z.B. 'Haushalt Familie K, Wien 1070' oder 'Betrieb Mustermann GmbH'
    einwilligung string Vermerk, wie die Zusage zustande kam, z.B. 'Am Telefon besprochen, 05.08.2026'. Der Kunde sieht ihn in seinem Konto — er ersetzt die Zustimmung NICHT.
    email* string E-Mail-Adresse des Kunden
    kundentyp string 'betrieb' für Unternehmen mit Lastprofilzähler; dann Sie-Form und Betriebs-Wortwahl in allen Mails. Weggelassen = 'haushalt' (Du-Form, Default).

    Ausgabe (oberste Ebene)

    bezeichnung string Anzeigename des Mandats im Portfolio.
    eingeladen string Die eingeladene E-Mail-Adresse.
    einladungslink string Link, den der Berater auch selbst weitergeben kann.
    gueltig_bis string Ablauf der Einladung als ISO-Zeitstempel.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
    zugriff string Immer der Wartezustand — nie ein 'ja'. Der Kunde entscheidet.
  • list_workspaces Berater-Portfolio nur lesend

    Listet die Arbeitsbereiche des Beraters: eigener Bereich + alle Kunden mit aktiver Freigabe (Grant), inkl. Datenstand.

    Ausgabe (oberste Ebene)

    aktiver_workspace string | null Slug des aktiven Bereichs, null = eigener Bereich.
    eigener_bereich object Der eigene Bereich des Beraters.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
    workspaces array Ein Eintrag je aktivem Grant.
  • portfolio_foerder_check Berater-Portfolio nur lesend

    Prüft für JEDEN Mandanten, ob eine offene Förderung (PV, Speicher, Heizungstausch) zu seinem Profil passt, und gibt je Mandant eine Ampel zurück: 'passt', 'passt_vielleicht', 'passt_nicht', 'daten_fehlen' (fehlende_fakten nennt, was beim Mandanten nachzutragen ist), 'nicht_verfuegbar'. Dazu die Förder-IDs und das Antragsfenster. Beantwortet 'bei welchen Kunden gibt es Förder-Potenzial?'. Mit foerderung_id nur diese eine Förderung ('bei wem passt genau diese?'). Das Result enthält KEINE Förderbeträge — nenne dem Berater keine. Für die Förderhöhe eines Mandanten mit select_workspace dorthin wechseln und foerderung_und_beratung rufen.

    Eingabe

    foerderung_id string Optional: ID einer offenen Förderung (z.B. aus foerderung_und_beratung). Weggelassen = alle offenen Förderungen der Kategorien pv, speicher, heizungstausch.

    Ausgabe (oberste Ebene)

    ampel_bedeutung object | null Legende der fünf Ampelwerte.
    anzahl integer Anzahl Mandanten.
    datenstand string | null Recherche-Stand der Förderdaten.
    foerderung_id string Nur wenn eine einzelne Förderung geprüft wurde.
    foerderungen object Legende der genannten Förderungen je ID (Name, Stelle, Link) — ohne Förderhöhe.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    mandanten array Eine Ampel-Zeile je Mandant.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • portfolio_overview Berater-Portfolio nur lesend

    Status-Übersicht über ALLE Mandanten des Beraters auf einen Blick: je Mandant, ob eine Rechnung und ob Lastgang-Daten hinterlegt sind, wann zuletzt gerechnet wurde, und ob der Zugriff gerade trägt. Beantwortet 'bei wem fehlt was?'. Enthält BEWUSST keine Zahlen — keinen Verbrauch, keinen Preis, keinen Betrag. Für die Werte eines Mandanten mit select_workspace dorthin wechseln.

    Ausgabe (oberste Ebene)

    anzahl integer Anzahl Mandanten.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    mandanten array Eine Status-Zeile je Mandant.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • portfolio_tariff_check Berater-Portfolio nur lesend

    Rechnet für JEDEN Mandanten mit hinterlegter Rechnung einen Tarifvergleich und gibt je Mandant eine Ampel zurück: 'ok' (unter 5 % Ersparnis), 'pruefen' (5-15 %), 'handeln' (über 15 %), 'keine_daten' (keine vollständige Rechnung), 'nicht_verfuegbar'. Beantwortet 'bei wem lohnt ein genauerer Blick?'. Das Result enthält KEINE Beträge — nenne dem Berater keine, sie stehen nicht drin. Für den vollen Rechenweg eines Mandanten mit select_workspace dorthin wechseln und tariff_compare rufen. Beachte netzkosten_vollstaendig je Zeile: bei false beruht die Ampel nur auf dem Energiepreis-Anteil.

    Ausgabe (oberste Ebene)

    ampel_bedeutung object | null Legende der fünf Ampelwerte.
    anzahl integer Anzahl Mandanten.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    mandanten array Eine Ampel-Zeile je Mandant.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.
  • select_workspace Berater-Portfolio

    Wechselt den aktiven Arbeitsbereich des Beraters (Kunde per Slug oder eindeutigem Label; 'eigen' = eigener Bereich). Die Auswahl gilt account-weit bis zum nächsten Wechsel.

    Eingabe

    workspace* string Kunden-Slug oder eindeutiges Label aus list_workspaces; 'eigen' wechselt zurück in den eigenen Bereich

    Ausgabe (oberste Ebene)

    aktiver_workspace string | null Slug des NEU gewählten Bereichs. null = eigener Bereich.
    hinweis string | null Optionaler Hinweis ans Modell (F14) — Datenstand-Warnung, nächster Schritt, Granularitäts-Caveat. Darf null sein.
    label string Anzeigename des neuen Bereichs.
    ok boolean immer true. Fehler kommen als isError ohne structuredContent zurück (errors.error_result: {ok:false, error_class, fehler}).
    workspace object Nur bei Berater-Accounts: der Arbeitsbereich, in dem dieser Call gelandet ist (dispatch.py stempelt ihn zentral an JEDES Result). Haushalts-Results tragen das Feld nicht.

Ein Beraterkonto sieht zusätzlich sechs Portfolio-Werkzeuge (oben gelistet) und bekommt dieselben Werkzeug-Texte in der Sie-Form, wenn ein Kunde als 'betrieb' markiert ist (kundentyp='betrieb'). Der Katalog selbst ändert sich dadurch nicht.

HTTP-Endpunkte

46 Endpunkte, nach Aufgabe gruppiert. Die Auth-Spalte zeigt, was ein Aufruf braucht: ein Sitzungs-Cookie (Browser, angemeldet), ein Bearer-Token (MCP-Client), einen signierten Link aus einer E-Mail, oder gar nichts.

Konto

  • POST /accounts/delete Session-Cookie

    Löscht das eigene Konto endgültig, inklusive Instanz, Zugängen und Messwerten.

    Request

    bestaetigung* string Die eigene E-Mail-Adresse, zur Bestätigung wörtlich eingetippt.

    Antwort

    JSON

    • fertig: true, wenn die Löschung vollständig durch ist.
    • konto_geloescht: true/false.
    • offen: Schritte, die beim nächsten Aufruf weiterlaufen, falls einer offen blieb.

    Fehler

    • 400 bestaetigung_stimmt_nicht: Die eingetippte Adresse stimmt nicht mit der Konto-Adresse überein.
    • 401 no_session: Keine Sitzung.
    • 429 rate_limited: Zu viele Versuche von dieser IP-Adresse.
    • 503 loeschung_unvollstaendig: Ein Teilschritt war gerade nicht erreichbar. Derselbe Aufruf macht dort weiter.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • POST /accounts/lastgang Session-Cookie

    Lädt eine Verbrauchsdatei aus dem Netzbetreiber-Portal hoch, die zweite Quelle für Lastgang-Daten neben der Datenfreigabe.

    Request

    datei* multipart/form-data Die Portal-Exportdatei.

    Antwort

    JSON, Status 201

    • status: 'gespeichert'.
    • quellformat: Erkanntes Dateiformat.
    • serien: Eine Zeile je erkannter Zählserie, mit Zeitraum und Anzahl neuer/aktualisierter Messwerte.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 413 file_too_large: Datei über dem Limit.
    • 422 unreadable: Datei nicht lesbar oder falsches Format.
    • 429 rate_limited: Zu viele Uploads von dieser IP-Adresse.
    • 503 storage_unavailable: Lastgang-Speicher gerade nicht angebunden.

    Limits

    • 3 Uploads pro Minute je IP-Adresse
    • Datei bis 20 MB
  • POST /accounts/login öffentlich

    Meldet ein bestätigtes Konto an und setzt das Sitzungs-Cookie für den Kontobereich.

    Request

    email* string E-Mail-Adresse.
    password* string Passwort.

    Antwort

    JSON + Set-Cookie

    • ok: true bei Erfolg.

    Fehler

    • 401 invalid_credentials: E-Mail-Adresse oder Passwort falsch, bewusst eine Meldung für beide Fälle.
    • 429 rate_limited: Zu viele Anmeldeversuche von dieser IP-Adresse.

    Limits

    • 10 Anmeldeversuche pro Minute je IP-Adresse
  • POST /accounts/logout Session-Cookie

    Beendet die Sitzung und löscht das Sitzungs-Cookie.

    Antwort

    Status 204, kein Inhalt

  • GET /accounts/me Session-Cookie

    Zeigt den Stand des eigenen Kontos: Bestätigung, Einrichtung, Kundentyp, Alarm-Einstellung.

    Antwort

    JSON

    • email: Hinterlegte Adresse.
    • email_verified: Bestätigt oder nicht.
    • provisioning_status: Stand der Einrichtung.
    • instance_slug: Kennung der eigenen Instanz.
    • created_at: Angelegt am.
    • alarm_mails: true = bekommt Nachrichten, wenn dem Zähler etwas auffällt.
    • kundentyp: haushalt oder betrieb, steuert Du/Sie in den Texten.

    Fehler

    • 401 no_session: Keine oder abgelaufene Sitzung.
  • POST /accounts/password/reset-confirm öffentlich

    Setzt mit dem Link-Token aus der Reset-Mail ein neues Passwort.

    Request

    token* string Reset-Token aus der Mail.
    password* string Neues Passwort.

    Antwort

    JSON

    • ok: true bei Erfolg.

    Fehler

    • 401 kein fester Code: 'error' trägt Klartext, keinen festen Code: z. B. 'invalid or already used reset token', 'reset token expired' oder 'unknown account'.
    • 429 rate_limited: Zu viele Versuche von dieser IP-Adresse.

    Limits

    • 5 Anfragen pro Minute je IP-Adresse
    • Link 1 Stunde gültig
  • POST /accounts/password/reset-request öffentlich

    Stößt den Passwort-Reset an und verschickt bei einer bekannten Adresse einen Link.

    Request

    email* string E-Mail-Adresse.

    Antwort

    JSON, Status 202

    • status: 'accepted': immer dieselbe Antwort, unabhängig davon, ob die Adresse existiert.

    Fehler

    • 429 rate_limited: Zu viele Anfragen von dieser IP-Adresse.

    Limits

    • 5 Anfragen pro Minute je IP-Adresse
  • POST /accounts/rechnung Bearer-Token

    Nimmt eine Rechnungsdatei entgegen und gibt eine Kennung zurück, mit der read_invoice_facts sie liest (oder get_invoice_reading_task, wenn ein eigenes Modell lesen soll). Die Datei passt nicht durch die Werkzeug-Schnittstelle und geht deshalb diesen Weg.

    Request

    datei* multipart/form-data Die Rechnung als PDF, JPG oder PNG.

    Antwort

    JSON, Status 201

    • rechnung_ref: Kennung für read_invoice_facts, get_invoice_reading_task und check_invoice_reading. Eine Stunde gültig.
    • seiten: Seitenzahl der Datei.
    • quellformat: pdf, jpeg oder png, aus der Datei erkannt und nicht aus dem Namen.
    • textebene: true heisst, die Rechnung wird ohne Modell exakt gelesen. false heisst Foto oder Scan; dafür ist der Modell-Pfad noch nicht gebaut.

    Fehler

    • 401 no_session: Keine Sitzung und kein gültiges Token.
    • 413 file_too_large: Datei über dem Limit.
    • 415 unsupported_format: Kein PDF, JPG oder PNG.
    • 422 unreadable: Datei nicht zu öffnen oder zu viele Seiten.
    • 429 rate_limited: Zu viele Uploads von dieser IP-Adresse.

    Limits

    • 3 Rechnungs-Uploads pro Minute je IP-Adresse
    • höchstens 20 MB pro Rechnungsdatei
  • DELETE /accounts/rechnung/{rechnung_ref} Bearer-Token

    Löscht eine hochgeladene Rechnung sofort, sobald die Werte bestätigt sind. Ohne diesen Aufruf verschwindet sie spätestens nach einer Stunde von selbst.

    Request

    rechnung_ref* Pfad-Segment Die Kennung aus dem Upload.

    Antwort

    Status 204, kein Inhalt

    Fehler

    • 401 no_session: Keine Sitzung und kein gültiges Token.
  • POST /accounts/register öffentlich

    Legt ein neues Konto an und verschickt eine Bestätigungsmail.

    Request

    email* string E-Mail-Adresse, gleichzeitig der Login-Name.
    password* string Passwort, mindestens 8 Zeichen.

    Antwort

    JSON, Status 201

    • account_id: Kennung des neuen Kontos.

    Fehler

    • 422 kein fester Code: 'error' trägt Klartext, keinen festen Code: z. B. 'gültige E-Mail erforderlich' oder 'E-Mail bereits registriert', je nachdem was nicht passt.
    • 429 rate_limited: Zu viele Registrierungen von dieser IP-Adresse.

    Limits

    • 5 Registrierungen pro Minute je IP-Adresse
    • 20 neue Konten pro Tag je IP-Adresse
  • POST /accounts/resend-verify öffentlich

    Schickt die Bestätigungsmail erneut, wenn sie nicht angekommen ist.

    Request

    email* string E-Mail-Adresse.

    Antwort

    JSON, Status 202

    • status: 'accepted': immer dieselbe Antwort, unabhängig davon, ob die Adresse existiert.

    Fehler

    • 429 rate_limited: Zu viele Anfragen von dieser IP-Adresse.

    Limits

    • 5 Anfragen pro Minute je IP-Adresse
  • POST /accounts/verify öffentlich

    Bestätigt die E-Mail-Adresse mit dem Token aus der Bestätigungsmail und stößt danach die Einrichtung des Kontos im Hintergrund an.

    Request

    account_id* string Kontokennung aus der Registrierung.
    token* string Bestätigungs-Token aus der Mail.

    Antwort

    JSON

    • verified: true, wenn bestätigt.
    • provisioning_scheduled: true: die Einrichtung läuft im Hintergrund weiter.

    Fehler

    • 401 kein fester Code: 'error' trägt Klartext, keinen festen Code: z. B. 'unknown account or no pending verification' oder 'invalid verification token'.
    • 429 rate_limited: Zu viele Bestätigungsversuche von dieser IP-Adresse.

    Limits

    • 10 Bestätigungen pro Minute je IP-Adresse

Zugriff & Freigaben

  • GET /accounts/einladung öffentlich

    Zeigt, wer einen Zugriff angeboten hat, bevor man sich dafür anmeldet oder registriert.

    Request

    token* query Einladungs-Token aus dem Einladungslink.

    Antwort

    JSON

    • berater: E-Mail-Adresse der einladenden Person.
    • email: Eingeladene Adresse.
    • bezeichnung: Name des Mandats.
    • gueltig_bis: Ablauf der Einladung.
    • kundentyp: haushalt oder betrieb.

    Fehler

    • 404 kein fester Code: 'error' trägt Klartext, keinen festen Code: 'kein Token', wenn der Query-Parameter fehlt, oder 'Einladung nicht gefunden', wenn der Token unbekannt, zurückgezogen oder abgelaufen ist. Bewusst dieselbe Antwort in allen Fällen, damit der Statuscode keine Auskunft über ein fremdes Konto verrät.
  • POST /accounts/me/alarme Session-Cookie

    Schaltet die Nachrichten ein oder aus, die kommen, wenn dem eigenen Zähler etwas auffällt.

    Request

    aus* boolean true = Nachrichten abstellen, false = wieder einschalten.

    Antwort

    JSON

    • alarm_mails: Neuer Stand: true = eingeschaltet.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 422 kein fester Code: 'error' trägt Klartext, keinen festen Code: 'invalid body' bei kaputtem JSON, 'aus muss true oder false sein' wenn das Feld fehlt oder kein Wahrheitswert ist.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • GET /accounts/me/freigaben Session-Cookie

    Zeigt, wer auf die eigenen Daten schauen darf, inklusive beendeter Freigaben.

    Antwort

    JSON

    • freigaben: Eine Zeile je Freigabe: Status, Gegenseite, Zeitpunkte.

    Fehler

    • 401 no_session: Keine Sitzung.
  • POST /accounts/me/freigaben/{grant_id}/widerrufen Session-Cookie

    Beendet eine Freigabe, wirkt ab dem nächsten Aufruf der Gegenseite.

    Request

    grant_id* Pfad Kennung der Freigabe.

    Antwort

    JSON

    • status: Neuer Zustand der Freigabe.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 404 kein fester Code: 'error' trägt Klartext ('Freigabe nicht gefunden.'), keinen festen Code: Freigabe existiert nicht oder gehört nicht zu diesem Konto.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • POST /accounts/me/freigaben/{grant_id}/zustimmen Session-Cookie

    Stimmt einer angebotenen Freigabe zu. Erst danach kann die Gegenseite die Daten sehen.

    Request

    grant_id* Pfad Kennung der Freigabe.

    Antwort

    JSON

    • status: Neuer Zustand der Freigabe.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 404 kein fester Code: 'error' trägt Klartext ('Freigabe nicht gefunden.'), keinen festen Code: Freigabe existiert nicht oder gehört nicht zu diesem Konto.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • GET /accounts/me/verbindungen Session-Cookie

    Zeigt, welche Chatbots gerade mit dem eigenen Konto verbunden sind.

    Antwort

    JSON

    • verbindungen: Eine Zeile je verbundener App: Name, ob Dauerzugriff, gültig bis, zuletzt verbunden.

    Fehler

    • 401 no_session: Keine Sitzung.
  • POST /accounts/me/verbindungen/entziehen Session-Cookie

    Trennt eine verbundene App sofort, nicht erst zum Ablauf des Zugangs.

    Request

    client_id* string Kennung der zu trennenden App, aus der Verbindungsliste.

    Antwort

    JSON

    • entzogen: Anzahl entwerteter Zugänge.
    • verbindungen: Die aktualisierte Liste.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 422 kein fester Code: 'error' trägt Klartext, keinen festen Code: 'invalid body' bei kaputtem JSON, 'client_id fehlt' wenn das Feld fehlt oder leer ist.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • GET /alarm/abmelden signierter Link aus einer E-Mail

    Ein-Klick-Abmeldung von den Alarm-Mails direkt aus der E-Mail, ohne Anmeldung.

    Request

    token* query Signierter Abmelde-Token aus der Mail.

    Antwort

    HTML-Seite

    Fehler

    • 400 kein fester Code: Token nicht lesbar. Die Seite zeigt einen Hinweis, keine Auskunft über die Adresse.

App-Session

  • GET /app/aktivitaet Session-Cookie

    Mitschrift, was der Chat zuletzt an Werkzeugen aufgerufen hat: Werkzeug, Zeit, Erfolg, nie Argumente oder Ergebnisse.

    Request

    tage query, 1–30 Zeitraum zurück, Default 1 Tag.

    Antwort

    JSON

    • eintraege: Eine Zeile je Aufruf: Werkzeug, Familie, Zeitpunkt, Erfolg, Fehlerklasse.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 422 invalid_tage: 'tage' außerhalb 1–30.
  • POST /app/eda-abfrage Session-Cookie

    Stößt eine Datenfreigabe-Anfrage beim Netzbetreiber aus der App heraus an, dasselbe Werkzeug wie im Chat.

    Request

    (Body)* object Die Argumente des Werkzeugs request_data_release (zaehlpunkt, plz, energy_direction).

    Antwort

    JSON: dasselbe Result wie im Chat

    Fehler

    • 401 no_session: Keine Sitzung.
    • 415 unsupported_media_type: Content-Type ist nicht application/json.
    • 422 invalid_body: Body kein gültiges JSON-Objekt.
    • 429 rate_limited: Zu viele Aufrufe von dieser IP-Adresse.

    Limits

    • 60 Aufrufe pro Minute je IP-Adresse
  • POST /app/rueckfrage Session-Cookie

    Beantwortet eine offene Lastgang-Rückfrage aus der App heraus, dasselbe Werkzeug wie im Chat.

    Request

    (Body)* object Die Argumente des Werkzeugs answer_lastgang_question.

    Antwort

    JSON: dasselbe Result wie im Chat

    Fehler

    • 401 no_session: Keine Sitzung.
    • 415 unsupported_media_type: Content-Type ist nicht application/json.
    • 422 invalid_body: Body kein gültiges JSON-Objekt.
    • 429 rate_limited: Zu viele Aufrufe von dieser IP-Adresse.

    Limits

    • 60 Aufrufe pro Minute je IP-Adresse
  • POST /app/tool Session-Cookie

    Ruft ein lesendes Werkzeug des Katalogs über das Sitzungs-Cookie auf, dieselbe Antwort wie im Chat.

    Request

    name* string Werkzeugname aus dem Katalog.
    arguments object Eingabe des Werkzeugs, wie im inputSchema beschrieben.

    Antwort

    JSON: dasselbe Result wie ein MCP-tools/call

    Fehler

    • 401 no_session: Keine Sitzung.
    • 403 nur_lesend: Das Werkzeug ist über diesen Weg nicht aufrufbar. Nur lesende Werkzeuge, unbekannte Namen bekommen dieselbe Antwort.
    • 415 unsupported_media_type: Content-Type ist nicht application/json.
    • 422 invalid_body: Body kein gültiges JSON-Objekt oder 'name' fehlt.
    • 429 rate_limited: Zu viele Aufrufe von dieser IP-Adresse.

    Limits

    • 60 Aufrufe pro Minute je IP-Adresse

Lastgang-Rückfragen

  • GET /lastgang/antwort signierter Link aus einer E-Mail

    Zeigt die Antwortmöglichkeiten zu einer Auffälligkeit aus der Alarm-Mail.

    Request

    t* query Verschlüsselter Token aus der Mail (Ereignis + Option).

    Antwort

    HTML-Seite

    Fehler

    • 404 kein fester Code: Link gilt nicht mehr: schon beantwortet oder abgeschnitten.
  • POST /lastgang/antwort signierter Link aus einer E-Mail

    Speichert die gewählte oder frei getippte Antwort zu einer Auffälligkeit.

    Request

    t* string (Form) Derselbe Token wie beim Anzeigen.
    freitext string (Form), bis 200 Zeichen Eigene Erklärung, wenn keine der Optionen passt.

    Antwort

    HTML-Seite (Dankesseite, ggf. mit Rückschau)

    Fehler

    • 404 kein fester Code: Link gilt nicht mehr.
    • 429 kein fester Code: Zu viele Versuche von dieser IP-Adresse in kurzer Zeit.
    • 500 kein fester Code: Speichern fehlgeschlagen, später erneut versuchen.

    Limits

    • 10 Antworten pro Minute je IP-Adresse

Match meinen Strom

  • POST /matchme/demo öffentlich

    Rechnet ohne Anmeldung ein Beispielergebnis der Partnersuche vor, aus frei eingegebenen Eckdaten.

    Request

    jahresverbrauch_kwh* number Jahresverbrauch laut Rechnung.
    waermepumpe* boolean Wärmepumpe vorhanden?
    eauto* boolean E-Auto vorhanden?
    warmwasser_elektrisch* boolean Warmwasser elektrisch?

    Antwort

    JSON

    Fehler

    • 422 validation_rejected: Eingabe außerhalb der erlaubten Werte.
    • 429 rate_limited: Zu viele Aufrufe: je IP-Adresse und insgesamt.

    Limits

    • 5 Aufrufe pro Minute je IP-Adresse
    • 60 Aufrufe pro Minute insgesamt (zusätzliche Bremse, nicht an eine einzelne IP-Adresse gebunden)
  • POST /matchme/einladung Session-Cookie

    Erzeugt einen persönlichen Einladungslink fürs Matching mit einer bestimmten Person.

    Antwort

    JSON, Status 201

    • link: Einladungslink zum Weitergeben.
    • gueltig_bis: Ablaufdatum.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • POST /matchme/einladung/einloesen Session-Cookie

    Nimmt eine Matching-Einladung an und bildet das Paar.

    Request

    token* string Token aus dem Einladungslink.

    Antwort

    JSON

    • partner_label: E-Mail-Adresse der einladenden Person.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • POST /matchme/einladung/vorschau öffentlich

    Zeigt vor der Anmeldung, von wem eine Matching-Einladung kommt.

    Request

    token* string Token aus dem Einladungslink, nur im Body, nie in der Adresszeile.

    Antwort

    JSON

    • von: E-Mail-Adresse der einladenden Person.

    Fehler

    • 429 rate_limited: Zu viele Anfragen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • POST /matchme/einladung/widerruf Session-Cookie

    Zieht eine eigene, noch offene Einladung zurück.

    Request

    id* string Kennung der Einladung.

    Antwort

    JSON

    Fehler

    • 401 no_session: Keine Sitzung.
    • 422 validation_rejected: 'id' fehlt.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • GET /matchme/einladungen Session-Cookie

    Listet die eigenen Einladungen, verschickte und erhaltene.

    Antwort

    JSON

    • einladungen: Eine Zeile je Einladung: Status, Rolle, Zeitpunkte, Partner (nur bei eingelöst).

    Fehler

    • 401 no_session: Keine Sitzung.
  • GET /matchme/matches Session-Cookie

    Zeigt die Rangliste passender Partnerprofile, ohne Namen, ohne Zählpunkte, ohne genaue Jahresmengen.

    Antwort

    JSON

    Fehler

    • 401 no_session: Keine Sitzung.
    • 403 kein_optin: Eigenes Profil ist nicht für die Partnersuche freigegeben.
    • 409 kein_lastgang: Keine verwertbare eigene Verbrauchsserie.
  • GET /matchme/optin Session-Cookie

    Zeigt, ob das eigene Profil gerade für die Partnersuche anderer mitgerechnet werden darf.

    Antwort

    JSON

    • status: 'active' | 'revoked' | 'none'.

    Fehler

    • 401 no_session: Keine Sitzung.
  • POST /matchme/optin Session-Cookie

    Gibt das eigene Profil für die Partnersuche frei.

    Antwort

    JSON

    • status: 'active'.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • POST /matchme/optin/widerruf Session-Cookie

    Nimmt das eigene Profil wieder aus der Partnersuche.

    Antwort

    JSON

    • status: 'revoked' oder 'none', wenn es ohnehin keinen aktiven Opt-in gab.

    Fehler

    • 401 no_session: Keine Sitzung.
    • 429 rate_limited: Zu viele Aktionen von dieser IP-Adresse.

    Limits

    • 20 Aktionen pro Minute je IP-Adresse
  • GET /matchme/status Session-Cookie

    Zeigt den eigenen Stand: Opt-in, Zählpunkt (maskiert), Datenlage.

    Antwort

    JSON

    Fehler

    • 401 no_session: Keine Sitzung.

Kontakt

  • POST /contact öffentlich

    Kontaktformular der Website, schickt eine Nachricht.

    Request

    (Formularfelder)* object Name, E-Mail-Adresse, Nachricht.

    Antwort

    JSON, Status 202

    • status: 'accepted'.

    Fehler

    • 422 kein fester Code: Body fehlt oder ungültig.
    • 429 rate_limited: Zu viele Nachrichten von dieser IP-Adresse.

    Limits

    • 5 Nachrichten pro Minute je IP-Adresse

OAuth / Verbindung

  • GET /.well-known/oauth-authorization-server öffentlich

    Discovery-Dokument des Authorization Servers (RFC 8414): Endpunkte, Scopes, unterstützte Verfahren.

    Antwort

    JSON

  • GET /.well-known/oauth-protected-resource öffentlich

    Discovery-Dokument der geschützten Ressource (RFC 9728). Bindet mcp.gridbert.at an seinen Authorization Server.

    Antwort

    JSON

  • GET /authorize öffentlich (Anmelde-/Verbindungsseite)

    Zeigt die Anmelde- bzw. Registrierungsseite, über die ein Mensch die Verbindung seines Chatbots bestätigt.

    Request

    client_id, redirect_uri, …* query OAuth-Parameter des Connectors (PKCE, Scope, State).

    Antwort

    HTML-Seite

    Fehler

    • 400 kein fester Code: Pflichtangaben fehlen oder die Anfrage ist unbekannt.
  • POST /authorize öffentlich (Zugangsdaten im Formular, kein Cookie/Header)

    Verarbeitet Anmeldung, Registrierung oder Bestätigung auf der Verbindungsseite und leitet mit einem Autorisierungs-Code zurück.

    Request

    intent string 'login' (Default), 'register' oder 'connect': welches der drei Formulare abgeschickt wurde.
    email, password string Bei Anmeldung oder Registrierung.
    client_id, redirect_uri, code_challenge, …* string OAuth-Parameter, unverändert aus dem vorherigen Schritt übernommen.

    Antwort

    302-Redirect mit Code (oder erneut die HTML-Seite bei einem Fehler)

    Fehler

    • 401 access_denied: Anmeldedaten falsch.
    • 429 rate_limited: Zu viele Anmeldeversuche von dieser IP-Adresse.

    Limits

    • 30 Anmeldungen pro Minute je IP-Adresse
  • GET /jwks.json öffentlich

    Öffentlicher Schlüssel, mit dem Zugangs-Token signiert sind (JWK Set).

    Antwort

    JSON

  • POST /mcp Bearer-Token

    Der eine JSON-RPC-Endpunkt, über den ein verbundener Chatbot alle Werkzeuge aufruft.

    Request

    jsonrpc, id, method, params* JSON-RPC 2.0 method ist eine der unterstützten Methoden (initialize, tools/list, tools/call, notifications/initialized).

    Antwort

    JSON-RPC-Antwort mit content[] und structuredContent

    Fehler

    • 401 invalid_token: Kein oder ungültiges Bearer-Token.
    • 400 parse error: Body kein gültiges JSON.
    • 400 unknown method: Nicht unterstützte Methode.

    Limits

    • 30 Aufrufe Startguthaben
    • füllt mit 0.5 Aufrufen pro Sekunde nach
    • 2000 Aufrufe pro Tag je Konto
    • Anfrage bis 256 KB
    • Werkzeug-Eingabe bis 150 KB
  • POST /register öffentlich (Client-Registrierung, DCR)

    Dynamic Client Registration (RFC 7591). Ein Chatbot meldet sich einmalig als Client an, bevor er eine Verbindung anbietet.

    Request

    client_name string Anzeigename des Clients.
    redirect_uris* string[] Erlaubte Rücksprung-Adressen nach der Anmeldung.

    Antwort

    JSON, Status 201

    • client_id: Neue Client-Kennung.
    • token_endpoint_auth_method: 'none': öffentlicher Client mit PKCE.

    Fehler

    • 400 invalid_client_metadata: Body kein gültiges JSON.
    • 422 invalid_redirect_uri: Rücksprung-Adresse nicht erlaubt oder Cap erreicht.

    Limits

    • 5 Registrierungen pro Minute je IP-Adresse
    • höchstens 20 offene Client-Registrierungen je IP-Adresse
  • POST /token öffentlich (Client-Zugangsdaten im Body)

    Tauscht einen Autorisierungs-Code oder ein Refresh-Token gegen ein Zugangs-Token.

    Request

    grant_type* string 'authorization_code' oder 'refresh_token'.
    code, redirect_uri, client_id, code_verifier string Bei grant_type=authorization_code (PKCE).
    refresh_token, client_id string Bei grant_type=refresh_token.

    Antwort

    JSON

    • access_token: Kurzlebiges Zugangs-Token.
    • refresh_token: Langlebiges Token zum Erneuern.
    • expires_in: Gültigkeit des Zugangs-Tokens in Sekunden.

    Fehler

    • 400 unsupported_grant_type: grant_type ist weder authorization_code noch refresh_token.
    • 400 invalid_grant: Code, Token oder PKCE passt nicht oder ist abgelaufen.
    • 429 rate_limited: Zu viele Anfragen von diesem Client.

    Limits

    • 30 Token-Anfragen pro Minute je Client

Grenzen und Regeln

Rate-Limits

  • 10 Anmeldeversuche pro Minute je IP-Adresse
  • 10 Antworten pro Minute je IP-Adresse
  • 10 Bestätigungen pro Minute je IP-Adresse
  • 20 Aktionen pro Minute je IP-Adresse
  • 20 neue Konten pro Tag je IP-Adresse
  • 2000 Aufrufe pro Tag je Konto
  • 3 Rechnungs-Uploads pro Minute je IP-Adresse
  • 3 Uploads pro Minute je IP-Adresse
  • 30 Anmeldungen pro Minute je IP-Adresse
  • 30 Aufrufe Startguthaben
  • 30 Token-Anfragen pro Minute je Client
  • 5 Anfragen pro Minute je IP-Adresse
  • 5 Aufrufe pro Minute je IP-Adresse
  • 5 Nachrichten pro Minute je IP-Adresse
  • 5 Registrierungen pro Minute je IP-Adresse
  • 60 Aufrufe pro Minute insgesamt (zusätzliche Bremse, nicht an eine einzelne IP-Adresse gebunden)
  • 60 Aufrufe pro Minute je IP-Adresse
  • füllt mit 0.5 Aufrufen pro Sekunde nach
  • höchstens 20 MB pro Rechnungsdatei

Größe der Anfragen

  • Anfrage bis 16 KB auf den OAuth-/Konto-Endpunkten
  • Anfrage bis 256 KB auf /mcp
  • Werkzeug-Eingabe bis 150 KB je Werkzeug-Aufruf
  • Datei bis 20 MB beim Lastgang-Upload

Herkunft im Browser

Nur eine hinterlegte Liste von Ursprüngen darf die Konto-Endpunkte per Browser-JavaScript ansprechen. Ein weiterer Ursprung wird über den Kontakt freigeschaltet, nicht selbst eingetragen.

Umgang mit Daten

  • Rechnungs- und Lastgang-Inhalte stehen nie in Server-Logs, nur in den Tool-Ergebnissen selbst.
  • Ein Fehler nennt die Fehlerklasse (z. B. rate_limited, not_configured), nie den internen Grund dahinter.
  • Jede Euro-Zahl aus einem Werkzeug trägt ihren Rechenweg mit. Es wird nichts im Sprachmodell nachgerechnet.

Nicht Teil der Schnittstelle

  • GET /lastgang/antwort, POST /lastgang/antwort: Ein Link aus einer E-Mail, kein Aufruf für einen eigenen Client. Der Token gehört zu genau einer Mail.
  • GET /alarm/abmelden: Ebenfalls ein Link aus einer E-Mail, kein Aufruf für einen eigenen Client.
  • /admin/*: Betreiber-Werkzeug, nicht öffentlich. Kein Bearer-Token dieser Schnittstelle öffnet es.

Stand

29. Oktober 2026, erzeugt aus dem Code, nicht von Hand nachgetragen. Ändert sich ein Werkzeug oder ein Endpunkt im Gateway, zieht diese Seite beim nächsten Build nach.

Du willst selbst etwas auf Gridbert bauen? Die Einrichtung je Agent zeigt den Klickweg, diese Seite die Referenz dahinter. Fragen dazu gehen an den Kontakt.