{"openapi":"3.1.0","info":{"title":"TankPuls API","version":"2026.09","description":"Die TankPuls-API ist eine REST-API mit JSON-Antworten (UTF-8). Sie liefert Stammdaten und aktuelle Preise aller Tankstellen, die an die **Markttransparenzstelle für Kraftstoffe** (MTS-K) beim Bundeskartellamt melden, dazu Preisverläufe, regionale Auswertungen und Live-Updates. Dieselbe API versorgt die TankPuls-Apps und [tankpuls.de](https://tankpuls.de).\n\nDiese Dokumentation wird direkt aus dem Code der API erzeugt; Parameter, Grenzen und Antwortfelder entsprechen immer dem laufenden Stand. Maschinenlesbar: [OpenAPI 3.1](https://api.tankpuls.de/docs/openapi.json).\n\n## Konventionen\n\n| Thema | Regel |\n| --- | --- |\n| Basis-URL | `https://api.tankpuls.de` — alle Pfade beginnen mit `/api/`, außer `GET /health` |\n| Preise | Zehntel-Cent als Ganzzahl: `1799` = 1,799 €/Liter |\n| Kraftstoffe | `e5`, `e10`, `diesel` |\n| Koordinaten | WGS84, Dezimalgrad; Bounding-Box als `minLat`, `minLng`, `maxLat`, `maxLng` (Südwest → Nordost) |\n| Zeitstempel | UTC. Preise, Verläufe und Live-Events: ISO 8601, z. B. `2026-09-30T08:15:02.000Z`. Stationsfelder (`priceTs`, `statusUpdatedAt`, `lastSeenAt`): PostgreSQL-Format, z. B. `2026-09-30 08:15:02.321+00` |\n| IDs | UUID; `externalId` ist die ID der Tankerkönig-Datenquelle |\n| Seitenweise Abfrage | nicht vorgesehen — `limit` je Endpunkt, Ergebnisse ohne Folgeseite |\n\n## Authentifizierung\n\nAlle Lese-Endpunkte sind **ohne API-Schlüssel** nutzbar; es gelten dann die Limits für anonyme Zugriffe je IP-Adresse. Wer mehr Anfragen braucht, beantragt einen API-Schlüssel in einem der [Tarife](#description/tarife) über das [Anmeldeformular](https://tankpuls.de/api-anmeldung.html); jede Anmeldung wird von Hand geprüft. Die Nutzung durch Unternehmen der Mineralölwirtschaft ist ausgeschlossen (§ 6 MTSKraftV, siehe [AGB](https://tankpuls.de/agb.html#zweckbindung)).\n\nDer Schlüssel wird im Header `Authorization` übergeben — als `Bearer`, alternativ mit dem Präfix `ApiKey`:\n\n```http\nGET /api/search/cheapest?minLat=52.47&minLng=13.33&maxLat=52.57&maxLng=13.48&fuel=e10\nAuthorization: Bearer 42|••••••••••••••••••••••••••••••••••••••••\nAccept: application/json\n```\n\nEin Schlüssel führt nie zu einem Fehler: Ist er gültig, gelten die Limits Ihres Tarifs; ist er falsch, abgelaufen, gesperrt oder von einer nicht freigegebenen IP-Adresse gesendet, läuft die Anfrage als anonymer Zugriff weiter. Welcher Fall vorliegt, steht im Header `x-api-key-status`:\n\n| Wert | Bedeutung |\n| --- | --- |\n| `ok` | Schlüssel gültig, Tarif-Limits aktiv |\n| `invalid_credentials:malformed` | kein Schlüssel im Format `<id>\\|<geheimnis>` |\n| `invalid_credentials:unknown` | Schlüssel existiert nicht (mehr) |\n| `invalid_credentials:mismatch` | Geheimnis passt nicht zur Kennung |\n| `invalid_credentials:revoked` | Schlüssel abgelaufen oder Zugang gesperrt |\n| `invalid_credentials:ip_not_allowed` | Anfrage kommt nicht von einer freigegebenen IP-Adresse |\n\nIhre Schlüssel verwalten Sie selbst im [Kundenportal](https://api.tankpuls.de/portal): anlegen, erneuern, widerrufen — dort sehen Sie auch Tarif, Limits und Verbrauch. Den Zugang erhalten Sie mit der Freischaltung.\n\n## Tarife\n\nDie Anmeldung ist kostenlos und unverbindlich; wir prüfen jede Anmeldung von Hand und schalten erst danach frei. Preise pro Monat zzgl. USt.\n\n| Tarif | Preis | Anfragen / Monat | Limit / Minute | Schlüssel | Support |\n| --- | --- | --- | --- | --- | --- |\n| **Community** | 0 € | 10.000 | 30 | bis 2 | Dokumentation und E-Mail, ohne Reaktionszeit |\n| **Starter** | 49 € | 100.000 | 60 | bis 3 | E-Mail, Antwort in 2 Werktagen |\n| **Business** | 199 € | 1.000.000 | 300 | bis 5 | E-Mail, Antwort in 1 Werktag |\n| **Partner** | Preis auf Anfrage | individuell | individuell | bis 10 | Fester Ansprechpartner, SLA 99,5 % |\n\nDas Limit pro Minute gilt für alle Schlüssel eines Zugangs zusammen und über alle Endpunkte. Das Monatskontingent zählt je Kalendermonat (deutsche Zeit); die API antwortet auch darüber hinaus weiter. Bei 80 % und 100 % informieren wir die hinterlegten Kontakte per E-Mail, Anfragen über dem Kontingent werden als Mehrverbrauch erfasst. Business- und Partner-Schlüssel sind an die freigegebenen Server-IPs gebunden.\n\n**[Zugang beantragen →](https://tankpuls.de/api-anmeldung.html)**\n\n## Rate-Limits\n\n**Ohne Schlüssel** gelten Limits je Minute, IP-Adresse und Endpunkt-Gruppe; die Gruppe steht bei jedem Endpunkt.\n\n| Gruppe | Anonym / Minute |\n| --- | --- |\n| Listen | 60 |\n| Details | 120 |\n| Regionen | 30 |\n| Feedback | 5 |\n| Alarme | 10 |\n\n**Mit Schlüssel** gilt das Minutenlimit Ihres [Tarifs](#description/tarife) — ein gemeinsames Budget für alle Endpunkte und alle Schlüssel Ihres Zugangs.\n\nJede Antwort trägt `x-ratelimit-limit`, `x-ratelimit-remaining` und `x-ratelimit-reset` (Sekunden bis zum neuen Fenster). Mit Schlüssel kommen `x-quota-used` (Anfragen im laufenden Monat) und, falls Ihr Tarif ein Kontingent hat, `x-quota-limit` hinzu. Bei Überschreitung des Minutenlimits antwortet die API mit `429` und `retry-after`.\n\n## Felder einer Station\n\nListen-Endpunkte antworten mit `count` und `items`; jede Station hat dieselben Felder (Schema bei jedem Endpunkt unter „Responses“).\n\n| Feld | Bedeutung |\n| --- | --- |\n| `status` | `open`, `closed` oder `no_prices` (keine aktuellen Preise gemeldet) |\n| `isActive` | `false` = Station stillgelegt; nur bei `/api/stations/{id}` sichtbar |\n| `priceCents`, `priceTs` | Preis des angefragten Kraftstoffs und seit wann er gilt; nur mit `fuel`, sonst `null` |\n| `distanceM` | Entfernung in Metern; nur bei `/search` und `/postalcode` |\n| `openingHours` | Öffnungszeiten, unverändert wie von der Datenquelle gemeldet — meist `[{ \"days\": [\"mon\", …], \"times\": [{ \"open\": \"06:00\", \"close\": \"22:00\" }] }]`, bei manchen Stationen `[{ \"text\": \"Mo-Fr\", \"start\": \"06:00:00\", \"end\": \"22:00:00\" }]`; `isOpen24h: true` = rund um die Uhr |\n| `statusUpdatedAt` | letzter Wechsel von `status` |\n| `lastSeenAt` | zuletzt von der Datenquelle bestätigt |\n| `nuts1` … `nuts3` | NUTS-Regionen der Station, passend zu `/api/stations/nuts/{code}` und `/api/history/nuts/{code}` |\n\n## Fehler\n\nFehler antworten mit einem Code im Feld `error`; bei `invalid_query` und bei Fehlern im Body zusätzlich mit `details` zu den betroffenen Feldern. Die möglichen Codes stehen bei jedem Endpunkt.\n\n| Status | Anlass |\n| --- | --- |\n| 400 | Parameter fehlt oder ist ungültig (`invalid_query`, `invalid_body`, `invalid_request`, `invalid_id`, `invalid_bbox`) |\n| 404 | Station oder Postleitzahl unbekannt (`not_found`, `postalcode_unknown`), unbekannter Pfad |\n| 429 | Minutenlimit überschritten |\n| 5xx | Fehler auf unserer Seite (`internal_error`) |\n\n## Live-Preise\n\nPreisänderungen werden über **WebSocket nach dem Pusher-Protokoll** verteilt; die offiziellen Pusher-Clients funktionieren unverändert (JavaScript, Swift, Kotlin). Den öffentlichen App-Key erhalten Sie mit der Freischaltung.\n\n| | |\n| --- | --- |\n| Host | `ws.tankpuls.de`, Port 443 (TLS) |\n| Channel | `prices.<geohash>` — eine Kartenkachel als Geohash mit 4 Zeichen (ca. 39 × 20 km), z. B. `prices.u33d` für Berlin-Mitte |\n| Event | `price.updated` |\n| Anmeldung | nicht nötig, alle Channels sind öffentlich |\n\nAbonnieren Sie nur die Kacheln des sichtbaren Ausschnitts, höchstens 20. Neue Preise werden einmal pro Minute gebündelt verteilt (bis zu 50 je Event); zusammen mit dem Import alle zwei Minuten liegt die Verzögerung bei bis zu etwa drei Minuten. Verpasste Events werden nicht nachgeliefert — nach einem Verbindungsabbruch die Preise einmal per REST laden.\n\n```json\n{\n  \"items\": [\n    { \"stationId\": \"3c1e6b0a-8a7d-4f5e-9b0e-2f6d1a7c4e21\", \"fuel\": \"e10\", \"priceCents\": 1729, \"ts\": \"2026-09-30T08:21:00.000Z\" }\n  ]\n}\n```\n\n## Browser und CORS\n\nAus dem Browser sind nur `GET`-Anfragen von `https://www.tankpuls.de`, `https://tankpuls.de`, `http://localhost:5173` erlaubt. Eigene Web-Anwendungen fragen die API über ihren Server ab — so bleibt auch ein API-Schlüssel geheim. Die Endpunkte der Apps sind für die TankPuls-Apps gedacht.\n\n## Lizenz und Namensnennung\n\nDie Preisdaten stammen aus der MTS-K und werden über **Tankerkönig** bezogen. Sie stehen unter der Lizenz [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.de); bei jeder Veröffentlichung ist die Quelle zu nennen, z. B. „Quelle: MTS-K über [tankerkoenig.de](https://www.tankerkoenig.de), CC BY 4.0 · TankPuls“.\n\nDie Daten dürfen nur zur Verbraucherinformation genutzt werden; die Nutzung durch die Mineralölwirtschaft und die Weiterveräußerung der Rohdaten sind ausgeschlossen. Es gelten die [AGB](https://tankpuls.de/agb.html#api).\n\n## Changelog\n\n| Stand | Änderung |\n| --- | --- |\n| **30.09.2026** | Dokumentation direkt aus der API unter `/docs`; Tarife mit gemeinsamem Minutenlimit und Monatskontingent je Zugang; Kundenportal; neue Header `x-api-key-status`, `x-quota-used`, `x-quota-limit`; abgelaufene und gesperrte Schlüssel verlieren sofort ihre Limits. |\n| **09/2026** | Neue Server-Plattform. Live-Preise über das Pusher-Protokoll (`ws.tankpuls.de`) statt `/ws/live`; `/api/stats` im Tankerkönig-Format. |\n| **05/2026** | Öffentlicher Start der REST-Endpunkte für Apps und Website; Preis-Alarme mit Push. |\n"},"servers":[{"url":"https://api.tankpuls.de"}],"tags":[{"name":"Stationen","description":"Stammdaten und Preise von Tankstellen, gefiltert nach Kartenausschnitt, Umkreis, Postleitzahl oder Region."},{"name":"Preise und Verlauf","description":"Aktuelle Preise, Preisänderungen einer Tankstelle und regionale Preisentwicklung."},{"name":"Suche und Statistik"},{"name":"Endpunkte der Apps","description":"Schreibende Endpunkte für die TankPuls-Apps (Rückmeldungen, Preis-Alarme). Im Browser nicht erreichbar (CORS)."},{"name":"Status"}],"security":[{},{"apiKey":[]}],"paths":{"/api/stations":{"get":{"operationId":"stationsInBox","description":"Aktive Tankstellen innerhalb einer Bounding-Box. Mit `fuel` enthält jede Station `priceCents` und `priceTs` des Kraftstoffs.\n\n**Rate-Limit:** Gruppe „Listen“ — ohne Schlüssel 60 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Tankstellen im Kartenausschnitt","tags":["Stationen"],"parameters":[{"name":"minLat","in":"query","required":true,"description":"Südwest-Ecke: Breitengrad.","schema":{"type":"number","minimum":-90,"maximum":90},"example":52.47},{"name":"minLng","in":"query","required":true,"description":"Südwest-Ecke: Längengrad.","schema":{"type":"number","minimum":-180,"maximum":180},"example":13.33},{"name":"maxLat","in":"query","required":true,"description":"Nordost-Ecke: Breitengrad.","schema":{"type":"number","minimum":-90,"maximum":90},"example":52.57},{"name":"maxLng","in":"query","required":true,"description":"Nordost-Ecke: Längengrad.","schema":{"type":"number","minimum":-180,"maximum":180},"example":13.48},{"name":"fuel","in":"query","description":"Kraftstoff; liefert `priceCents` und `priceTs` je Station.","schema":{"type":"string","enum":["e5","e10","diesel"]},"example":"e10"},{"name":"limit","in":"query","description":"Höchstzahl der Stationen.","schema":{"type":"integer","default":200,"minimum":1,"maximum":1000}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/StationResource"}}},"required":["count","items"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_query","invalid_bbox"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/stations/search":{"get":{"operationId":"stationsNearby","description":"Aktive Tankstellen im Umkreis eines Punkts, die nächste zuerst; jede Station mit Entfernung `distanceM` in Metern.\n\n**Rate-Limit:** Gruppe „Listen“ — ohne Schlüssel 60 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Tankstellen im Umkreis","tags":["Stationen"],"parameters":[{"name":"lat","in":"query","required":true,"description":"Mittelpunkt: Breitengrad.","schema":{"type":"number","minimum":-90,"maximum":90},"example":52.52},{"name":"lng","in":"query","required":true,"description":"Mittelpunkt: Längengrad.","schema":{"type":"number","minimum":-180,"maximum":180},"example":13.405},{"name":"rad","in":"query","required":true,"description":"Radius in Kilometern, größer 0.","schema":{"type":"number","maximum":50},"example":5},{"name":"fuel","in":"query","description":"Kraftstoff; liefert `priceCents` und `priceTs` je Station.","schema":{"type":"string","enum":["e5","e10","diesel"]},"example":"e10"},{"name":"limit","in":"query","description":"Höchstzahl der Stationen.","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/StationResource"}}},"required":["count","items"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_query"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/stations/postalcode":{"get":{"operationId":"stationsByPostalcode","description":"Aktive Tankstellen im Umkreis des Mittelpunkts einer Postleitzahl; die Antwort enthält zusätzlich `centroid`. Unbekannte Postleitzahl: `404 postalcode_unknown`.\n\n**Rate-Limit:** Gruppe „Listen“ — ohne Schlüssel 60 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Tankstellen um eine Postleitzahl","tags":["Stationen"],"parameters":[{"name":"postalcode","in":"query","required":true,"description":"Postleitzahl.","schema":{"type":"string","minLength":4,"maxLength":10},"example":"10559"},{"name":"rad","in":"query","description":"Radius in Kilometern um den Mittelpunkt der Postleitzahl, größer 0.","schema":{"type":"number","default":10,"maximum":50}},{"name":"fuel","in":"query","description":"Kraftstoff; liefert `priceCents` und `priceTs` je Station.","schema":{"type":"string","enum":["e5","e10","diesel"]},"example":"e10"},{"name":"limit","in":"query","description":"Höchstzahl der Stationen.","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"centroid":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"items":{"type":"array","items":{"$ref":"#/components/schemas/StationResource"}}},"required":["count","centroid","items"]}}}},"404":{"description":"Keine Station mit dieser Postleitzahl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"postalcode_unknown"}},"required":["error"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_query"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/stations/nuts/{code}":{"get":{"operationId":"stationsByRegion","description":"Aktive Tankstellen einer NUTS-Region (NUTS 1 = 3 Zeichen, NUTS 2 = 4, NUTS 3 = 5 — z. B. `DE1`, `DE11`, `DE111`).\n\n**Rate-Limit:** Gruppe „Listen“ — ohne Schlüssel 60 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Tankstellen einer Region","tags":["Stationen"],"parameters":[{"name":"code","in":"path","required":true,"description":"NUTS-Code der Region.","schema":{"type":"string","minLength":2,"maxLength":8}},{"name":"fuel","in":"query","description":"Kraftstoff; liefert `priceCents` und `priceTs` je Station.","schema":{"type":"string","enum":["e5","e10","diesel"]},"example":"e10"},{"name":"limit","in":"query","description":"Höchstzahl der Stationen.","schema":{"type":"integer","default":500,"minimum":1,"maximum":1000}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/StationResource"}}},"required":["count","items"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_request"]}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/stations/all":{"get":{"operationId":"stationsAll","description":"Alle aktiven Tankstellen in einer Antwort (rund 15.000), sortiert nach ID. Mit `fuel` enthält jede Station `priceCents` und `priceTs` des Kraftstoffs.\n\n**Nur mit Schlüssel der Tarife Business und Partner.** Ohne Schlüssel `401`, mit einem anderen Tarif `403`.\n\n**Rate-Limit:** Partner — das [Minutenlimit des Tarifs](#description/tarife). Business — zusätzlich ein Abruf alle 10 Minuten; früher antwortet die API mit `429`, `retry-after` nennt die Sekunden bis zum nächsten Abruf. Jeder Abruf zählt als eine Anfrage.\n\nDie Antwort ist mehrere MB groß; `Accept-Encoding: gzip` (oder `br`, `zstd`) verkleinert sie deutlich.","summary":"Alle Tankstellen","tags":["Stationen"],"parameters":[{"name":"fuel","in":"query","description":"Kraftstoff; liefert `priceCents` und `priceTs` je Station.","schema":{"type":"string","enum":["e5","e10","diesel"]},"example":"e10"}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/StationResource"}}},"required":["count","items"]}}}},"401":{"description":"Kein oder ungültiger Schlüssel.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"403":{"description":"Der Tarif enthält diesen Endpunkt nicht.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"forbidden"},"requiredPlan":{"type":"array","items":{"type":"string"}}},"required":["error","requiredPlan"]}}}},"429":{"description":"Minutenlimit oder (Business) Abruf-Intervall überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","const":429},"error":{"type":"string","const":"Too Many Requests"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_query"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]}}}}},"security":[{"apiKey":[]}]}},"/api/stations/{id}":{"get":{"operationId":"station","description":"Alle Stammdaten einer Tankstelle — auch stillgelegte Stationen (`isActive: false`). Unbekannte ID: `404 not_found`.\n\n**Rate-Limit:** Gruppe „Details“ — ohne Schlüssel 120 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Eine Tankstelle","tags":["Stationen"],"parameters":[{"name":"id","in":"path","required":true,"description":"ID der Tankstelle.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"`StationResource`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationResource"}}}},"404":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","const":"not_found"}},"required":["error"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_id"]}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/prices/{stationId}/current":{"get":{"operationId":"currentPrices","description":"Die aktuell gültigen Preise aller Kraftstoffe einer Tankstelle, jeweils mit `validFrom`.\n\n**Rate-Limit:** Gruppe „Details“ — ohne Schlüssel 120 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Aktuelle Preise einer Tankstelle","tags":["Preise und Verlauf"],"parameters":[{"name":"stationId","in":"path","required":true,"description":"ID der Tankstelle.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PriceResource"}}},"required":["items"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_id"]}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/history/{stationId}":{"get":{"operationId":"stationHistory","description":"Jede Preisänderung mit `validFrom` und `validTo` (`null` = gilt noch), die neueste zuerst.\n\n**Rate-Limit:** Gruppe „Details“ — ohne Schlüssel 120 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Preisverlauf einer Tankstelle","tags":["Preise und Verlauf"],"parameters":[{"name":"stationId","in":"path","required":true,"description":"ID der Tankstelle.","schema":{"type":"string","format":"uuid"}},{"name":"fuel","in":"query","description":"Nur dieser Kraftstoff; ohne Angabe alle drei.","schema":{"type":"string","enum":["e5","e10","diesel"]}},{"name":"days","in":"query","description":"Zeitraum in Tagen bis heute.","schema":{"type":"integer","default":7,"minimum":1,"maximum":365}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"string"},"count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/PriceHistoryResource"}}},"required":["since","count","items"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_request"]}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/history/nuts/{code}":{"get":{"operationId":"regionHistory","description":"Median, Durchschnitt und Anzahl Stationen einer NUTS-Region je Stunde oder Tag, die älteste zuerst; je Tag zusätzlich Minimum und Maximum.\n\n**Rate-Limit:** Gruppe „Regionen“ — ohne Schlüssel 30 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Preisentwicklung einer Region","tags":["Preise und Verlauf"],"parameters":[{"name":"code","in":"path","required":true,"description":"NUTS-Code der Region.","schema":{"type":"string","minLength":2,"maxLength":8}},{"name":"fuel","in":"query","description":"Nur dieser Kraftstoff; ohne Angabe alle drei.","schema":{"type":"string","enum":["e5","e10","diesel"]}},{"name":"days","in":"query","description":"Zeitraum in Tagen bis heute.","schema":{"type":"integer","default":30,"minimum":1,"maximum":365}},{"name":"granularity","in":"query","description":"Werte je Stunde oder je Tag.","schema":{"type":"string","enum":["hour","day"],"default":"hour"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"},"granularity":{"type":"string","enum":["hour","day"]},"days":{"type":"integer"},"count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/NutsPriceResource"}}},"required":["code","granularity","days","count","items"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_request"]}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/search/cheapest":{"get":{"operationId":"cheapest","description":"Tankstellen eines Kartenausschnitts mit Preis für den gewählten Kraftstoff, aufsteigend nach Preis. Achtung: `limit` begrenzt die (unsortierte) Auswahl im Ausschnitt vor der Sortierung; Stationen ohne Preis entfallen danach. Liegen mehr Stationen im Ausschnitt als `limit`, sind das nicht zwingend die günstigsten — Ausschnitt verkleinern oder `limit` erhöhen.\n\n**Rate-Limit:** Gruppe „Listen“ — ohne Schlüssel 60 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Günstigste Tankstellen","tags":["Suche und Statistik"],"parameters":[{"name":"minLat","in":"query","required":true,"description":"Südwest-Ecke: Breitengrad.","schema":{"type":"number","minimum":-90,"maximum":90},"example":52.47},{"name":"minLng","in":"query","required":true,"description":"Südwest-Ecke: Längengrad.","schema":{"type":"number","minimum":-180,"maximum":180},"example":13.33},{"name":"maxLat","in":"query","required":true,"description":"Nordost-Ecke: Breitengrad.","schema":{"type":"number","minimum":-90,"maximum":90},"example":52.57},{"name":"maxLng","in":"query","required":true,"description":"Nordost-Ecke: Längengrad.","schema":{"type":"number","minimum":-180,"maximum":180},"example":13.48},{"name":"fuel","in":"query","required":true,"description":"Kraftstoff, nach dessen Preis sortiert wird.","schema":{"type":"string","enum":["e5","e10","diesel"]},"example":"e10"},{"name":"limit","in":"query","description":"Höchstzahl der Stationen im Ausschnitt — gilt vor der Sortierung.","schema":{"type":"integer","default":50,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/StationResource"}}},"required":["count","items"]}}}},"400":{"description":"Parameter fehlen oder sind ungültig.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_query","invalid_bbox"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/stats":{"get":{"operationId":"stats","description":"Anzahl, Durchschnitt und Median je Kraftstoff in €/Liter (drei Nachkommastellen), im Format der Tankerkönig-Statistik; alle fünf Minuten neu berechnet.\n\n**Rate-Limit:** Gruppe „Details“ — ohne Schlüssel 120 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Bundesweite Kennzahlen","tags":["Suche und Statistik"],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"license":{"type":"string","const":"CC BY http://www.tankerkoenig.de"},"E5":{"type":"object","properties":{"count":{"type":"integer"},"mean":{"type":"number"},"median":{"type":"number"}},"required":["count","mean","median"]},"E10":{"type":"object","properties":{"count":{"type":"integer"},"mean":{"type":"number"},"median":{"type":"number"}},"required":["count","mean","median"]},"Diesel":{"type":"object","properties":{"count":{"type":"integer"},"mean":{"type":"number"},"median":{"type":"number"}},"required":["count","mean","median"]},"timestamp":{"type":"string"},"apiVersion":{"type":"string","const":"tankpuls"},"fetchedAt":{"type":["string","null"]}},"required":["license","E5","E10","Diesel","timestamp","apiVersion","fetchedAt"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/alerts/register":{"post":{"operationId":"registerAlerts","description":"Registriert ein Gerät für Push-Benachrichtigungen und ersetzt bei jedem Aufruf alle Alarme dieses Geräts (höchstens 50). Eine leere `alerts`-Liste löscht alle Alarme — so melden sich auch Android-Geräte ab.\n\n**Rate-Limit:** Gruppe „Alarme“ — ohne Schlüssel 10 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Preis-Alarme registrieren","tags":["Endpunkte der Apps"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAlertsRequest"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"deviceId":{"type":"string"},"alertCount":{"type":"integer"}},"required":["deviceId","alertCount"]}}}},"404":{"description":"","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","const":"unknown_brand"},"brandSlug":{"type":"string"}},"required":["error","brandSlug"]},{"type":"object","properties":{"error":{"type":"string","const":"unknown_station"},"stationId":{"type":"string"}},"required":["error","stationId"]}]}}}},"400":{"description":"Body fehlt, ist kein gültiges JSON-Objekt oder Felder sind ungültig.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]},{"type":"object","properties":{"statusCode":{"type":"integer"},"code":{"type":"string","enum":["FST_ERR_CTP_EMPTY_JSON_BODY","FST_ERR_CTP_INVALID_JSON_BODY"]},"error":{"type":"string","enum":["Bad Request"]},"message":{"type":"string"}},"required":["statusCode","code","error","message"]}]}}}},"415":{"description":"`Content-Type` ist nicht `application/json` oder `text/plain`.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"code":{"type":"string","enum":["FST_ERR_CTP_INVALID_MEDIA_TYPE"]},"error":{"type":"string","enum":["Unsupported Media Type"]},"message":{"type":"string"}},"required":["statusCode","code","error","message"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/alerts/unregister":{"post":{"operationId":"unregisterAlerts","description":"Löscht alle Alarme eines iOS-Geräts (APNs-Token). Unbekannte Geräte sind kein Fehler. Android-Geräte senden `/api/alerts/register` mit leerer `alerts`-Liste.\n\n**Rate-Limit:** Gruppe „Alarme“ — ohne Schlüssel 10 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Alarme eines iOS-Geräts löschen","tags":["Endpunkte der Apps"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnregisterAlertsRequest"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"deviceId":{"type":"string"},"alertCount":{"type":"integer"}},"required":["deviceId","alertCount"]},{"type":"object","properties":{"deviceId":{"type":"null"},"alertCount":{"type":"integer"}},"required":["deviceId","alertCount"]}]}}}},"400":{"description":"Body fehlt, ist kein gültiges JSON-Objekt oder Felder sind ungültig.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]},{"type":"object","properties":{"statusCode":{"type":"integer"},"code":{"type":"string","enum":["FST_ERR_CTP_EMPTY_JSON_BODY","FST_ERR_CTP_INVALID_JSON_BODY"]},"error":{"type":"string","enum":["Bad Request"]},"message":{"type":"string"}},"required":["statusCode","code","error","message"]}]}}}},"415":{"description":"`Content-Type` ist nicht `application/json` oder `text/plain`.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"code":{"type":"string","enum":["FST_ERR_CTP_INVALID_MEDIA_TYPE"]},"error":{"type":"string","enum":["Unsupported Media Type"]},"message":{"type":"string"}},"required":["statusCode","code","error","message"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/alerts/sync":{"post":{"operationId":"syncAlerts","description":"Format älterer iOS-Versionen (nur APNs-Token); neue Clients nutzen `/api/alerts/register`.\n\n**Rate-Limit:** Gruppe „Alarme“ — ohne Schlüssel 10 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Preis-Alarme synchronisieren (veraltet)","tags":["Endpunkte der Apps"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncAlertsRequest"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"deviceId":{"type":"string"},"alertCount":{"type":"integer"},"translatedCount":{"type":"integer"},"skippedCount":{"type":"integer"},"skipped":{"type":"array","prefixItems":[{"type":"array","items":{"anyOf":[{"type":"array"},{"type":"object","properties":{"reason":{"type":"string","const":"missing_location"},"fuel":{"type":"string"}},"required":["reason","fuel"]}]}},{"type":"array","items":{"type":"object","properties":{"reason":{"type":"string","const":"unknown_station"},"fuel":{"type":"string"},"stationId":{"type":"string"}},"required":["reason","fuel","stationId"]}}],"minItems":2,"maxItems":2}},"required":["deviceId","alertCount","translatedCount","skippedCount","skipped"]}}}},"400":{"description":"Body fehlt, ist kein gültiges JSON-Objekt oder Felder sind ungültig.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]},{"type":"object","properties":{"statusCode":{"type":"integer"},"code":{"type":"string","enum":["FST_ERR_CTP_EMPTY_JSON_BODY","FST_ERR_CTP_INVALID_JSON_BODY"]},"error":{"type":"string","enum":["Bad Request"]},"message":{"type":"string"}},"required":["statusCode","code","error","message"]}]}}}},"415":{"description":"`Content-Type` ist nicht `application/json` oder `text/plain`.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"code":{"type":"string","enum":["FST_ERR_CTP_INVALID_MEDIA_TYPE"]},"error":{"type":"string","enum":["Unsupported Media Type"]},"message":{"type":"string"}},"required":["statusCode","code","error","message"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/api/feedback":{"post":{"operationId":"feedback","description":"Rückmeldung aus der App; Antwort `201` mit `ok` und `id`.\n\n**Rate-Limit:** Gruppe „Feedback“ — ohne Schlüssel 5 Anfragen pro Minute und IP-Adresse, mit Schlüssel das [Minutenlimit Ihres Tarifs](#description/tarife).","summary":"Rückmeldung senden","tags":["Endpunkte der Apps"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoreFeedbackRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"id":{"type":"string"},"createdAt":{"type":["string","null"]}},"required":["ok","id","createdAt"]}}}},"400":{"description":"Body fehlt, ist kein gültiges JSON-Objekt oder Felder sind ungültig.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"details":{"type":"object","description":"Meldungen je Parameter, z. B. `{ \"fieldErrors\": { \"limit\": [\"Number must be less than or equal to 1000\"] } }`","properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"required":["error"]},{"type":"object","properties":{"statusCode":{"type":"integer"},"code":{"type":"string","enum":["FST_ERR_CTP_EMPTY_JSON_BODY","FST_ERR_CTP_INVALID_JSON_BODY"]},"error":{"type":"string","enum":["Bad Request"]},"message":{"type":"string"}},"required":["statusCode","code","error","message"]}]}}}},"415":{"description":"`Content-Type` ist nicht `application/json` oder `text/plain`.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"code":{"type":"string","enum":["FST_ERR_CTP_INVALID_MEDIA_TYPE"]},"error":{"type":"string","enum":["Unsupported Media Type"]},"message":{"type":"string"}},"required":["statusCode","code","error","message"]}}}},"429":{"description":"Minutenlimit überschritten; `retry-after` nennt die Sekunden bis zum nächsten Versuch.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","enum":[429]},"error":{"type":"string","enum":["Too Many Requests"]},"message":{"type":"string","enum":["Rate limit exceeded, retry in 1 minute"]}},"required":["statusCode","error","message"]}}}}}}},"/health":{"get":{"operationId":"health","description":"Antwortet mit `{ \"ok\": true, \"ts\": \"…\" }`, solange die API läuft. Ohne Rate-Limit.","summary":"Erreichbarkeit","tags":["Status"],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"ts":{"type":"string"}},"required":["ok","ts"]}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","description":"API-Schlüssel aus dem Kundenportal, Format `<id>|<geheimnis>`. Optional — ohne Schlüssel gelten die anonymen Limits.","scheme":"bearer"}},"schemas":{"NutsPriceResource":{"type":"object","properties":{"ts":{"type":"string"},"fuel":{"type":"string","enum":["e5","e10","diesel"]},"medianCents":{"type":"integer"},"avgCents":{"type":"integer"},"minCents":{"type":["integer","null"]},"maxCents":{"type":["integer","null"]},"nStations":{"type":"integer"}},"required":["ts","fuel","medianCents","avgCents","minCents","maxCents","nStations"],"title":"NutsPriceResource"},"PriceHistoryResource":{"type":"object","properties":{"fuel":{"type":"string"},"priceCents":{"type":"integer"},"validFrom":{"type":["string","null"]},"validTo":{"type":["string","null"]}},"required":["fuel","priceCents","validFrom","validTo"],"title":"PriceHistoryResource"},"PriceResource":{"type":"object","properties":{"stationId":{"type":"string"},"fuel":{"type":"string"},"priceCents":{"type":"integer"},"validFrom":{"type":["string","null"]}},"required":["stationId","fuel","priceCents","validFrom"],"title":"PriceResource"},"RegisterAlertsRequest":{"type":"object","description":"POST /api/alerts/register — replaces all alerts of a device.\n\nAlerts are a discriminated union on `kind`: `station` targets one station,\n`point` the cheapest station within `radiusM` around `center`.","properties":{"deviceToken":{"type":"string","description":"APNs-Token (iOS) oder FCM-Token (Android).","minLength":32,"maxLength":4096},"timezone":{"type":"string","description":"IANA-Zeitzone des Geräts, für die Uhrzeitfenster der Alarme.","pattern":"^[A-Za-z0-9_\\-+\\/]+$","examples":["Europe/Berlin"],"minLength":1,"maxLength":64},"platform":{"type":"string","description":"Plattform des Geräts.","enum":["ios","android"],"default":"ios"},"appVersion":{"type":"string","description":"Version der App.","maxLength":64},"alerts":{"type":"array","description":"Alle Alarme des Geräts (ersetzt die bisherigen). `station`: eine Tankstelle (`stationId`); `point`: günstigste Station im Umkreis (`center`, `radiusM` 100–50000, optional `brandSlug`). `maxPriceCents` 1–9999; `notifyFromHour`/`notifyUntilHour` 0–23, ein Fenster über Mitternacht ist möglich.","items":{"type":"object","properties":{"kind":{"type":"string","enum":["station","point"]},"fuel":{"type":"string","enum":["e5","e10","diesel"]},"maxPriceCents":{"type":"integer"},"stationId":{"type":"string"},"center":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"radiusM":{"type":"integer"},"brandSlug":{"type":["string","null"]},"notifyFromHour":{"type":"integer"},"notifyUntilHour":{"type":"integer"}},"required":["kind","fuel","maxPriceCents","notifyFromHour","notifyUntilHour"]}}},"required":["deviceToken","timezone","alerts"],"title":"RegisterAlertsRequest"},"StationResource":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"brandSlug":{"type":"string"},"brandName":{"type":"string"},"street":{"type":["string","null"]},"postcode":{"type":["string","null"]},"city":{"type":["string","null"]},"state":{"type":["string","null"]},"isOpen24h":{"type":"boolean"},"status":{"type":"string","enum":["open","closed","no_prices"]},"isActive":{"type":"boolean"},"lat":{"type":"number"},"lng":{"type":"number"},"priceCents":{"type":["integer","null"]},"priceTs":{"type":["string","null"]},"distanceM":{"type":"number"},"openingHours":{"anyOf":[{"type":"array","items":{"anyOf":[{"type":"object","properties":{"days":{"type":"array","items":{"type":"string"}},"times":{"type":"array","items":{"type":"object","properties":{"open":{"type":"string"},"close":{"type":"string"}},"required":["open","close"]}}},"required":["days","times"]},{"type":"object","properties":{"text":{"type":"string"},"start":{"type":"string"},"end":{"type":"string"}},"required":["text","start","end"]}]}},{"type":"object","additionalProperties":{}},{"type":"null"}]},"phone":{"type":["string","null"]},"website":{"type":["string","null"]},"externalId":{"type":["string","null"]},"nuts1":{"type":["string","null"]},"nuts2":{"type":["string","null"]},"nuts3":{"type":["string","null"]},"statusUpdatedAt":{"type":["string","null"]},"lastSeenAt":{"type":["string","null"]}},"required":["id","name","brandSlug","brandName","street","postcode","city","state","isOpen24h","status","isActive","lat","lng","priceCents","priceTs","openingHours","phone","website","externalId","nuts1","nuts2","nuts3","statusUpdatedAt","lastSeenAt"],"title":"StationResource"},"StoreFeedbackRequest":{"type":"object","description":"POST /api/feedback — anonymous app feedback.","properties":{"message":{"type":"string","description":"Text der Rückmeldung.","minLength":1,"maxLength":5000},"deviceInfo":{"type":"string","description":"Gerät und App-Version.","examples":["iPhone 15 · iOS 19.1 · TankPuls 3.2"],"maxLength":500},"timestamp":{"type":"string","description":"Zeitpunkt auf dem Gerät, ISO 8601 mit Zeitzone.","pattern":"^((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?(Z|([+-]\\d{2}:?\\d{2}))$","examples":["2026-09-30T10:15:00+02:00"]}},"required":["message","timestamp"],"title":"StoreFeedbackRequest"},"SyncAlertsRequest":{"type":"object","description":"POST /api/alerts/sync — permanent adapter for the legacy iOS payload.\n\nThe deployed iOS app cannot be updated in the field, so its payload is\ntranslated into the /register shape:\n  - fuelType/stationID/targetPrice (euros) instead of fuel/stationId/maxPriceCents,\n  - inactive alerts are dropped (the backend has no paused state),\n  - alerts without stationID become point alerts around the top-level\n    latitude/longitude with a fixed 10 km radius,\n  - no notify window: pushes are allowed at any hour.","properties":{"deviceToken":{"type":"string","pattern":"^[a-f0-9]{64,200}$"},"timezone":{"type":"string","pattern":"^[A-Za-z0-9_\\-+\\/]+$","minLength":1,"maxLength":64},"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"appVersion":{"type":"string","maxLength":64},"alerts":{"type":"array","items":{"type":"string"},"maxItems":50}},"required":["deviceToken","timezone","alerts"],"title":"SyncAlertsRequest"},"UnregisterAlertsRequest":{"type":"object","description":"POST /api/alerts/unregister — legacy companion of /sync.","properties":{"deviceToken":{"type":"string","description":"APNs-Token des iOS-Geräts (hexadezimal).","pattern":"^[a-f0-9]{64,200}$"}},"required":["deviceToken"],"title":"UnregisterAlertsRequest"}}}}