1. Was das ist und was es nicht ist
Dies ist die Referenz für v1 der Parkena-Betreiber-API. Sie ist die Alternative dazu, Ihren Bestand in der Parkena-Konsole zu führen: Ihre Parkplätze und Ihre Preise hineinschieben, Ihre Parkena-Buchungen wieder herausziehen. Beide Wege schreiben durch dieselbe Absicherung in dieselben Tabellen, und keiner von beiden kommt an die Daten eines anderen Betreibers. Sie können beide nutzen — die Konsole für das, was ein Mensch entscheidet, die API für die nächtliche Synchronisation — und sie geraten sich nicht in die Quere.
Was sie nicht ist: ein Produkt, das Sie unbeaufsichtigt anbinden. Ein angemeldeter Inhaber oder Manager erzeugt den Schlüssel in der Konsole; es gibt keine Sandbox zum Üben, und die erste Anbindung wird mit einem Menschen auf unserer Seite gebaut. Das ist eine Aussage über den Stand, an dem Parkena steht, und keine Warteschlange, die Sie überspringen könnten.
Fünf Dinge, die diese API nicht tut — vorweg
Buchungen werden abgeholt, über einen Cursor, und dieser Cursor ist die maßgebliche Quelle. Ein registrierter Endpunkt kann einen signierten booking.changed-Hinweis erhalten, der „jetzt abholen“ sagt — §13 —, aber in einem Webhook reisen nie Buchungsdaten, bewusst, und §12 sagt, was es weiterhin nicht gibt.
Es gibt keinen Verfügbarkeitskalender. Sperrzeiträume — §7 — nehmen ganze Datumsbereiche aus dem Verkauf, aber Sie können nicht „heute Nacht freie Stellplätze“ als Zahl melden, und das Feld, das danach aussieht — capacity — sind die Stellplätze INSGESAMT. Wer dort die Verfügbarkeit einträgt, meldet seinen Parkplatz falsch und macht jede Nacht die Prüfung seines Eintrags hinfällig.
Über diese API bewegt sich kein Geld. Keine Abbuchungen, keine Erstattungen, keine Auszahlungsdaten, keine Provisionszahlen.
Nichts hier legt eine Buchung an, ändert, storniert, checkt sie ein oder erstattet sie. Es gibt keinen Scope dafür und keine Rechtevergabe dahinter.
Die Basis-URL ist https://api.parkena.com/v1 — siehe §3. Auf nichts anderes sollte gezeigt werden.
2. Zwei Regeln, die sitzen müssen, bevor Sie eine Zeile schreiben
Das sind die beiden Dinge, die auch eine kompetente Anbindung noch falsch macht, weil in beiden Fällen das Falsche so aussieht, als hätte es funktioniert. Genau deshalb stehen sie ganz oben auf dieser Seite; alles danach ist gewöhnliches Referenzmaterial.
Regel eins: vergleichen, bevor Sie aktualisieren
Wenn ein Prüfer von Parkena Ihren Eintrag freigibt, gibt er bestimmte Inhalte frei. Ändern sich diese Inhalte, beschreibt die Freigabe nicht mehr das, was veröffentlicht ist; sie wird deshalb hinfällig, und der Parkplatz geht zurück in die Prüfung — und er verkauft auf parkena.com nicht weiter, bis ein Mensch ihn erneut freigibt.
Das ist richtiges Verhalten, und vor einer Maschine, die jede Nacht ihren ganzen Bestand erneut hineinschiebt, ist es zugleich ein Weg, für immer jede Nacht um 03:00 Uhr aus dem Schaufenster genommen zu werden. Deshalb setzt diese API keine Aktualisierung ab, die nichts ändert. Beide Schreibrouten lesen die aktuelle Zeile, vergleichen sie Feld für Feld, und wenn sich nichts unterscheidet, setzen sie überhaupt keine Anweisung ab — kein wirkungsloses UPDATE; gar keine Anweisung. Sie bekommen "result": "unchanged" und "changed": [].
Sie müssen dafür nichts tun. Es ist kein Schalter, und es gibt keinen Header, den Sie senden müssten. Ein nächtliches vollständiges erneutes Hineinschieben identischer Daten ist ein Leerlauf, der einen Lesevorgang pro Parkplatz kostet.
# an identical re-push of an approved car park
{ "result": "unchanged", "changed": [], "listing_review_superseded": false }
#
# the same car park, with one word of the name changed
{ "result": "updated", "changed": ["name"], "listing_review_superseded": true }Die zweite Antwort ist keine Warnung, die Sie ignorieren können: Dieser Parkplatz hat das Schaufenster verlassen und bleibt draußen, bis er erneut freigegeben ist. §8 listet jedes Feld auf, das Sie eine erneute Prüfung kostet, und jedes, das es nicht tut.
Regel zwei: GET liefert ein Feld, das PUT zurückweist
GET /lots liefert jeden Parkplatz MIT seinem rates-Objekt, weil das die nützliche Sache zum Lesen ist. PUT /lots/{external_id} nimmt keines entgegen — Preise werden unter PUT /lots/{external_id}/rates gesetzt. Die naheliegende Schleife, einen Parkplatz lesen, ein Feld ändern und ihn zurückschreiben, scheitert deshalb so lange, bis Sie rates entfernen. Entfernen Sie external_id gleich mit: Die steht im Pfad.
curl "$BASE/lots" -H "Authorization: Bearer $KEY.$SECRET" \
| jq '.lots[0] | del(.rates, .external_id)' > lot.json
# edit lot.json
curl -X PUT "$BASE/lots/edge-main" -H "Authorization: Bearer $KEY.$SECRET" \
-H 'Content-Type: application/json' --data @lot.jsonSchicken Sie ihn mit noch angehängtem rates zurück, bekommen Sie 422 {"error":"field_belongs_to_another_route","field":"rates"}. Wir weisen das zurück, statt es zu ignorieren, und der Unterschied ist der ganze Punkt: Hätten Sie einen neuen Preis in den Body eines Parkplatzes geschrieben und wir hätten mit 200 geantwortet, dann würden Sie mit gutem Grund glauben, der Preis habe sich geändert. Er hätte sich nicht geändert.
3. Die Basis-URL
Jeder Pfad auf dieser Seite ist relativ zu https://api.parkena.com/v1. Alles ist JSON, hinein wie heraus.
api.parkena.com ist ein Hostname, der Parkena gehört
Er steht vor der Funktion, die antwortet; die Pfade unten ändern sich nicht, wenn sich ändert, was dahinterliegt. Halten Sie die Basis trotzdem in der Konfiguration und nicht im Quelltext.
Die lokale Entwicklung gegen den Stack dieses Repositorys nutzt dieselben Pfade unter /functions/v1/operator-api/v1 auf dem lokalen Supabase-Origin — dieselben Pfade, eine andere Basis.
Es gibt keine Sandbox und keine Test-Basis. Die Basis oben ist die produktive, und auf welchen Betreiber eine Anfrage wirkt, ergibt sich aus den Zugangsdaten, die Sie vorlegen — nie aus irgendetwas in einer URL oder in einem Body. Siehe §4 und §12 zur Mandantenwahl.
4. Authentifizierung
Jede Anfrage trägt einen Header:
Authorization: Bearer <key_id>.<secret>key_id und secret sind die beiden Hälften eines Satzes Zugangsdaten, verbunden durch einen Punkt. Der erste Punkt trennt sie; alle weiteren Punkte gehören zum Secret.
curl "$BASE/ping" \
-H "Authorization: Bearer pk_api_3f9c….pk_sec_a71b…"key_id—pk_api_gefolgt von 32 Hex-Zeichen. Das ist die öffentliche Hälfte. Sie taucht bewusst in unseren Logs auf, und Sie können sie gefahrlos in eine Konfigurationsdatei schreiben oder in einer Supportanfrage nennen. Wer sie kennt, kommt so weit wie jemand, der einen Benutzernamen kennt.secret—pk_sec_gefolgt von 64 Hex-Zeichen, also 256 Bit. Wir speichern es nicht. Wir speichern einen gesalzenen HMAC-SHA256 davon, in zwei Spalten, die keine Rolle in unserer Datenbank lesen kann. Wir können es nie für Sie wiederherstellen. Wenn Sie es verlieren, stellen Sie neue Zugangsdaten aus und widerrufen die alten.
Es gibt keinen Rückfallweg über ?key= in der Query, und es wird keinen geben: Ein Secret in einer URL ist ein Secret in einem Proxy-Log, in einem Browserverlauf und in einem Referer-Header.
Jeder Fehlschlag bei der Authentifizierung liefert dasselbe 401
Eine unbekannte key_id, ein falsches Secret, widerrufene Zugangsdaten, abgelaufene Zugangsdaten, ein gesperrtes Betreiberkonto, Zugangsdaten ohne den Scope, den eine Route verlangt, und eine external_id, die zu keinem Ihrer Parkplätze gehört — all das ist 401 {"error":"unauthorized"}. Sie sind bewusst nicht voneinander zu unterscheiden — §9 sagt, was das einbringt und was es Sie kostet.
Prüfen Sie Zugangsdaten mit GET /ping, statt aus einem 401 irgendetwas abzuleiten.
5. Zugangsdaten ausstellen, rotieren und widerrufen
Zugangsdaten werden von einem angemeldeten Inhaber oder Manager Ihres Betreiberkontos erzeugt — nie über diese API. Ein API-Schlüssel, der API-Schlüssel erzeugen kann, ist ein Schlüssel, der seine eigenen Rechte ausweiten kann; deshalb stellt hier keine Route einen aus. Mitarbeiter-Zugänge können weder ausstellen noch widerrufen.
Die beiden Aufrufe dahinter sind api_credential_issue und api_credential_revoke. Sie laufen über PostgREST mit dem Token eines angemeldeten Benutzers und nicht mit API-Zugangsdaten. Sie sind hier dokumentiert, weil ein Betreiber mit einem Entwicklungsteam sie direkt ansprechen will; der gewöhnliche Weg, welche auszustellen, ist die Konsolenansicht unter Account → API keys.
Ausstellen
curl -X POST '<project rest url>/rpc/api_credential_issue' \
-H "apikey: <anon key>" \
-H "Authorization: Bearer <a signed-in owner or manager’s JWT>" \
-H 'Content-Type: application/json' \
-d '{
"p_label": "nightly sync",
"p_scopes": ["read_supply", "write_supply", "read_bookings"],
"p_expires_in_days": 365
}'| Parameter | Typ | Hinweise |
|---|---|---|
p_label | String, 1–80 Zeichen | Wie Sie diesen Schlüssel in sechs Monaten nennen werden. Erforderlich. |
p_scopes | Array aus Scopes | Nicht leere Menge unterschiedlicher Scopes. Erforderlich. |
p_expires_in_days | Ganzzahl 1–3650 oder null | Null heißt: läuft nicht ab. Optional. |
[{
"credential_id": "01a01a73-a650-710e-9be1-08ffc77b4696",
"key_id": "pk_api_3f9c…",
"secret": "pk_sec_a71b…",
"label": "nightly sync",
"scopes": ["read_supply", "write_supply", "read_bookings"],
"created_at": "2026-08-19T14:22:07.113904Z",
"expires_at": "2027-08-19T14:22:07.113904Z"
}]Es gibt kein p_expires_at, und es wird nie eines geben. Der Ablauf ist eine Anzahl von Tagen, die der Server gegen seine eigene Uhr umrechnet; diese API nimmt nirgendwo, auf keiner Route, einen vom Aufrufer gelieferten Zeitstempel entgegen.
Rotieren
Es gibt keinen Aufruf zum Rotieren, denn eine Rotation ohne Ausfallzeit sind schlicht zwei Aufrufe in der richtigen Reihenfolge:
- Stellen Sie zweite Zugangsdaten mit denselben Scopes aus.
- Rollen Sie sie in Ihrem System aus und bestätigen Sie, dass Verkehr fließt —
GET /pingmit dem neuen Schlüssel, dann beobachten Sie dessenlast_used_at. - Widerrufen Sie die alten.
Zwischen dem ersten und dem dritten Schritt sind beide Zugangsdaten gültig. Keine Grenze hindert Sie daran, zwei zu halten.
Widerrufen
curl -X POST '<project rest url>/rpc/api_credential_revoke' \
-H "apikey: <anon key>" \
-H "Authorization: Bearer <a signed-in owner or manager’s JWT>" \
-H 'Content-Type: application/json' \
-d '{"p_credential_id": "01a01a73-a650-710e-9be1-08ffc77b4696"}'Ein Widerruf wirkt sofort und ist endgültig. Widerrufene Zugangsdaten werden nicht wieder eingesetzt — sie werden ersetzt. Zweimal widerrufen liefert dieselbe Zeile mit dem ursprünglichen revoked_at, denn ein Schlüssel stirbt nicht zweimal. Eine ID, die nicht Ihnen gehört, liefert [], genau wie eine ID, die es nie gegeben hat.
6. Scopes
Zugangsdaten tragen eine Menge von Scopes. Es gibt drei davon:
| Scope | Was er erlaubt |
|---|---|
read_supply | Ihre Parkplätze, deren Parkena-Preiskorridore und die Sperrzeiträume jedes Parkplatzes auflisten — einschließlich der aggregierten overlapping_bookings-Zahl an jedem. Eine Zahl, nie eine Buchung: keine Referenz, kein Name, kein Kennzeichen reist mit. |
write_supply | Parkplätze, deren Parkena-Preise und deren Sperrzeitraum-Listen anlegen und ändern. |
read_bookings | Ihre Parkena-Buchungen abholen. |
Vergeben Sie so wenig wie möglich. Eine Synchronisation, die nur Bestand hineinschiebt, braucht read_bookings nicht; ein Auswertungsjob, der nur Buchungen abholt, darf write_supply nicht halten.
Die Prüfung des Scopes ist kein bloßer Hinweis. Sie ist ein Argument desselben Datenbankaufrufs, der festlegt, auf wessen Zeilen die Anfrage überhaupt zugreifen darf; es gibt also keinen Weg zu Ihren Daten, der sie überspringt. Zugangsdaten ohne den Scope, den eine Route verlangt, bekommen das einheitliche 401 — dieselbe Antwort wie bei einem falschen Secret.
Es gibt bewusst kein write_bookings. Nichts in Parkena lässt eine Maschine eine Buchung anlegen, ändern oder stornieren; ein Scope, der diese Fähigkeit benennt, läse sich für Sie also wie eine Grenze und wäre nichts dergleichen.
7. Routen
| Methode | Pfad | Erforderlicher Scope |
|---|---|---|
GET | /ping | beliebige gültige Zugangsdaten |
GET | /lots | read_supply |
PUT | /lots/{external_id} | write_supply |
PUT | /lots/{external_id}/rates | write_supply |
GET | /lots/{external_id}/blocked-periods | read_supply |
PUT | /lots/{external_id}/blocked-periods | write_supply |
GET | /bookings | read_bookings |
{external_id} ist IHR EIGENER Bezeichner für den Parkplatz — wie auch immer Ihr System ihn nennt. Für uns ist er undurchsichtig: Wir zerlegen ihn nie, und er muss keine UUID sein. Kodieren Sie ihn prozentual, wenn er ein / oder ein Leerzeichen enthält. Die internen IDs von Parkena werden Ihnen nie geschickt und nie von Ihnen entgegengenommen.
Unbekannte Query-Parameter werden zurückgewiesen statt ignoriert, auf jeder Route. Ein vertippter Parameter, der stillschweigend verworfen wird, ist ein Filter, von dem Sie glauben, er greife, und der nicht greift.
GET /ping
Prüft Zugangsdaten und sagt Ihnen, was sie dürfen. Das ist die Route, die man nimmt, wenn eine Anbindung nicht funktioniert — jede andere Zurückweisung dieser API kann Ihnen bewusst nicht sagen, welche von sechs Sachen schiefgegangen ist. Sie stellt keinen Betreiberkontext her und liest keine Zeilen.
curl "$BASE/ping" -H "Authorization: Bearer $KEY.$SECRET"
#
{
"ok": true,
"key_id": "pk_api_3f9c…",
"scopes": ["read_supply", "write_supply", "read_bookings"],
"server_time": "2026-08-19T15:12:09.870151Z"
}server_time ist unsere Uhr, in UTC. Der Wert ist informativ — Sie schicken uns nie eine Zeit zurück.
GET /lots
Ihre Parkplätze, jeder mit seinem Parkena-Preiskorridor.
| Query-Parameter | Typ | Standard | Hinweise |
|---|---|---|---|
after | String | — | Nach dieser external_id fortsetzen. Nehmen Sie das next_after der vorherigen Seite. |
limit | Ganzzahl 1–200 | 50 | Mehr zu verlangen wird zurückgewiesen, nicht stillschweigend gekürzt. |
Die Seitenbildung läuft über Ihre eigene external_id, aufsteigend — kein Offset, damit eine Seitengrenze sich nicht unter einem gleichzeitigen Schreibvorgang verschiebt.
curl "$BASE/lots?limit=50" -H "Authorization: Bearer $KEY.$SECRET"
#
{
"lots": [
{
"external_id": "LOT-1",
"name": "Terminal Park",
"timezone": "Europe/Berlin",
"handover": "self",
"online_bookable": true,
"capacity": 250,
"latitude": "52.520000",
"longitude": "13.405000",
"city": "Berlin",
"country_code": "DE",
"features": ["indoor", "valet"],
"arrival": null,
"media": null,
"min_advance_days": 1,
"min_stay_days": 1,
"max_stay_days": 60,
"cancellation": {
"free_until_hours_before_check_in": 48,
"penalty_percent_after": "50.00",
"no_show_forfeits_full": true
},
"rates": {
"channel": "parkena",
"currency": "EUR",
"base_price": "12.50",
"floor_price": "9.00",
"ceiling_price": "18.00",
"min_first_day_price": "11.00",
"dynamic": false,
"valid_from": null,
"valid_to": null
}
}
],
"next_after": null
}next_after ist nur vorhanden, wenn die Seite voll war. Folgen Sie ihm, bis es null ist, und Sie haben Ihren gesamten Bestand genau einmal gesehen. rates ist null für einen Parkplatz, der noch keinen Parkena-Preis hat.
PUT /lots/{external_id}
Legt EINEN Parkplatz an oder aktualisiert ihn. Eine Anfrage, ein Parkplatz — es gibt keinen Sammel-Endpunkt, und die Form der Route ist es, die das durchsetzt. Ein Array als Body wird mit too_many_lots zurückgewiesen.
PUT ist eine vollständige Darstellung. Ein fehlender Schlüssel bedeutet null, nicht „lass es, wie es war“. Schicken Sie jedes Mal den ganzen Parkplatz. Die Alternative macht es unmöglich, ein Feld zu leeren, und verwandelt einen vertippten Schlüssel in einen dauerhaften, stillen Leerlauf.
| Feld | Typ | Erforderlich | Hinweise |
|---|---|---|---|
external_id | String | nein | Wenn vorhanden, muss sie der im Pfad entsprechen. Wird geprüft, nicht verwendet. |
name | String, ≤ 200 | ja | — |
timezone | String | ja | IANA-Name, z. B. Europe/Berlin. Jede Berechnung einer Tagesgrenze löst darüber auf. |
handover | String | ja | self oder attended. |
online_bookable | Boolean | ja | Ihr Verkaufsschalter. Erforderlich gerade deshalb, weil ein Standardwert einen laufenden Parkplatz beim ersten unvollständigen Push aus dem Verkauf nehmen würde. |
capacity | Ganzzahl 0–1000000 | nein | Stellplätze INSGESAMT. Nicht die heute Nacht freien Stellplätze — siehe die Warnung unten. |
latitude | Zahl oder String | nein | Wird auf 6 Nachkommastellen gerundet. Muss zusammen mit longitude geschickt werden. |
longitude | Zahl oder String | nein | Wird auf 6 Nachkommastellen gerundet. Muss zusammen mit latitude geschickt werden. |
city | String, ≤ 120 | nein | — |
country_code | String | nein | ISO 3166-1 alpha-2, z. B. DE. |
features | Array aus Strings | nein | Siehe die Liste unten. Die Reihenfolge spielt keine Rolle — wir sortieren sie. |
arrival | Objekt | nein | Frei geformte Hinweise zur Anfahrt. |
media | Objekt | nein | Frei geformte Verweise auf Medien. |
min_advance_days | Ganzzahl 0–365 | nein | Standard ist 0. |
min_stay_days | Ganzzahl 1–365 | nein | — |
max_stay_days | Ganzzahl 1–365 | nein | Alle drei enden bei 365, denn so weit im Voraus beauskunftet Parkena einen Aufenthalt überhaupt. Ein größerer Wert würde angenommen und nie eingehalten. |
cancellation | Objekt oder null | nein | Alle drei Schlüssel oder keinen — siehe unten. |
features nimmt an: indoor, security, cameras, gate_automation, plate_recognition, ev_charging, disabled_access, oversize_vehicle, valet.
cancellation ist ein einzelnes verschachteltes Objekt, und es gilt ganz oder gar nicht:
"cancellation": {
"free_until_hours_before_check_in": 48,
"penalty_percent_after": "50.00",
"no_show_forfeits_full": true
}Einen oder zwei der drei Schlüssel zu schicken ist 422 incomplete_cancellation_policy. Eine Regelung mit einem kostenlosen Zeitfenster und ohne genannte Gebühr ist keine teilweise bekannte Regelung, sondern eine unbeantwortbare Frage nach der Erstattung. Schicken Sie null oder lassen Sie den Schlüssel weg für „keine Regelung“.
capacity sind die STELLPLÄTZE INSGESAMT, nicht die Verfügbarkeit
Wenn Ihr nächtlicher Feed „heute Nacht freie Stellplätze“ in capacity schreibt, sagen Sie Parkena, Ihr Parkplatz sei geschrumpft, UND Sie machen jede einzelne Nacht die Prüfung Ihres Eintrags hinfällig, denn capacity gehört zu den geprüften Inhalten.
Die Verfügbarkeit in Echtzeit ist ein anderer Mechanismus und nicht Teil von v1. Diese API kann den Fehler nicht erkennen, denn eine Zahl ist eine Zahl.
curl -X PUT "$BASE/lots/LOT-1" \
-H "Authorization: Bearer $KEY.$SECRET" \
-H 'Content-Type: application/json' \
-d '{
"name": "Terminal Park",
"timezone": "Europe/Berlin",
"handover": "self",
"online_bookable": true,
"capacity": 250,
"latitude": 52.5200,
"longitude": 13.4050,
"city": "Berlin",
"country_code": "DE",
"features": ["valet", "indoor"],
"min_advance_days": 1,
"min_stay_days": 1,
"max_stay_days": 60,
"cancellation": {
"free_until_hours_before_check_in": 48,
"penalty_percent_after": "50.00",
"no_show_forfeits_full": true
}
}'{
"external_id": "LOT-1",
"result": "created",
"changed": ["name", "timezone", "handover", "…"],
"listing_review_superseded": false,
"lot": { "…": "the car park as it now stands" }
}201, wenn der Parkplatz angelegt wurde, sonst 200.| Feld | Bedeutung |
|---|---|
result | created, updated oder unchanged. |
changed | Die Feldnamen, die sich tatsächlich unterscheiden. Bei unchanged leer. |
listing_review_superseded | true, wenn dieser Schreibvorgang die Prüfung Ihres Parkena-Eintrags hinfällig gemacht hat — siehe §8. |
lot | Der gespeicherte Parkplatz, zurückgelesen. |
result: "unchanged" heißt, dass überhaupt keine Anweisung abgesetzt wurde — keine Sperre auf der Zeile, kein Schreibvorgang, kein Trigger. Das ist das normale und erwartete Ergebnis eines nächtlichen erneuten Pushs, und es ist das, was diese API davon abhält, Sie jede Nacht aus dem Schaufenster zu nehmen.
PUT /lots/{external_id}/rates
Setzt den Parkena-Preiskorridor für einen Parkplatz. Ein Preisplan ist kein Preis — er ist die SPANNE, innerhalb derer Sie auf dem Parkena-Kanal zu verkaufen bereit sind: eine Untergrenze, eine Obergrenze und ein Basispreis dazwischen.
Diese Route schreibt den Kanal parkena und nur diesen Kanal. Ihre eigene Direktpreisgestaltung gehört Ihnen, und diese API kann sie nicht anfassen.
Zwei Formen des Bodys werden angenommen. Eine Spanne:
{
"floor_price": "9.00",
"base_price": "12.50",
"ceiling_price": "18.00",
"min_first_day_price": "11.00",
"dynamic": false
}Oder ein fester Preis, der zu Untergrenze = Basis = Obergrenze aufgeht:
{ "fixed_price": "12.50" }| Feld | Typ | Erforderlich | Hinweise |
|---|---|---|---|
fixed_price | Dezimalzahl | die eine oder die andere Form | Nicht mit den Feldern der Spanne kombinierbar. |
floor_price | Dezimalzahl | mit der Spanne | Muss ≤ base_price sein. |
base_price | Dezimalzahl | mit der Spanne | Muss zwischen Unter- und Obergrenze liegen. |
ceiling_price | Dezimalzahl | mit der Spanne | Muss ≥ base_price sein. |
min_first_day_price | Dezimalzahl oder null | nein | Muss innerhalb des Korridors liegen. |
currency | String | nein | ISO 4217. Standard ist Ihre Abrechnungswährung, und etwas anderes kann es nicht sein. |
dynamic | Boolean | nein | Standard ist false. |
valid_from | YYYY-MM-DD oder null | nein | Ein Kalenderdatum. Ein Zeitstempel wird zurückgewiesen, nicht abgeschnitten. |
valid_to | YYYY-MM-DD oder null | nein | Darf nicht vor valid_from liegen. |
Geld ist eine Zeichenkette, und es wird zurückgewiesen statt gerundet. Schicken Sie "12.50", nicht 12.345. Beträge tragen zwei Nachkommastellen; eine dritte ist 422 invalid_body, denn ein Preis, den Sie nicht getippt haben, ist kein Preis, dem Sie zugestimmt haben. Bei Koordinaten ist es umgekehrt — sie sind eine Messung, also werden sie gerundet.
Sowohl fixed_price als auch eine Spanne zu schicken ist 422. Zu raten, was Sie gemeint haben, ist der Weg, auf dem ein Parkplatz am falschen Ende seiner eigenen Spanne landet.
{
"external_id": "LOT-1",
"result": "updated",
"changed": ["floor_price"],
"envelope_widened": true,
"rates": {
"channel": "parkena",
"currency": "EUR",
"base_price": "12.50",
"floor_price": "7.00",
"ceiling_price": "18.00",
"min_first_day_price": "11.00",
"dynamic": false,
"valid_from": null,
"valid_to": null
}
}201 beim Anlegen, sonst 200.envelope_widened ist true, wenn dieser Schreibvorgang den Korridor nach AUSSEN verschoben hat — Untergrenze herunter, Obergrenze herauf, Währung gewechselt oder dynamic umgelegt — oder wenn es vorher gar keinen Parkena-Korridor gab. Die Weitung ist das, was Sie eine Prüfung des Eintrags kosten kann.
Es ist ein vorsichtiges Signal, und wir sagen das lieber, als es zu überhöhen: Wir vergleichen mit dem Korridor, der einen Augenblick zuvor gültig war, und nicht mit dem, den ein Prüfer freigegeben hat, denn diese API kann den Stand Ihrer Prüfung bewusst nicht lesen. Sie kann also true melden in einem Fall, der nichts hinfällig macht. Sie wird nicht false melden, wenn etwas hinfällig geworden ist.
GET /lots/{external_id}/blocked-periods
Die Sperrliste des Parkplatzes: jeder Datumsbereich, in dem er auf Parkena aus dem Verkauf genommen ist, egal wer ihn eingetragen hat — Ihre Synchronisationen und die Konsole schreiben dieselbe Liste. Ein Sperrzeitraum stoppt NEUE Parkena-Verkäufe für Aufenthalte, die ihn berühren, und tut sonst nichts: Er storniert nichts, und Ihre eigenen Direktverkäufe rührt er nicht an. Beide Daten sind einschließlich gemeint — ends_on ist der letzte gesperrte Tag, nicht der Tag danach, und ein Zeitraum mit starts_on gleich ends_on sperrt genau diesen einen Tag.
curl "$BASE/lots/LOT-1/blocked-periods" -H "Authorization: Bearer $KEY.$SECRET"
#
{
"external_id": "LOT-1",
"blocked_periods": [
{ "starts_on": "2026-11-02",
"ends_on": "2026-11-08",
"reason": "resurfacing",
"overlapping_bookings": 2 },
{ "starts_on": "2026-12-24",
"ends_on": "2026-12-26",
"reason": null,
"overlapping_bookings": 0 }
]
}reason ist Ihre eigene Beschriftung, null, wenn keine angegeben wurde.overlapping_bookings ist eine Transparenzzahl: wie viele Buchungen dieses Parkplatzes — jeder Kanal, jeder Status außer cancelled — einen Aufenthalt haben, der den Zeitraum berührt. Beide Vergleiche sind einschließlich, auf den Kalendertagen des Parkplatzes selbst: Eine Buchung, die am ersten Morgen des Zeitraums abreist, zählt noch, ebenso eine, die an seinem letzten Abend ankommt. Beachten Sie, was „außer cancelled“ einschließt: Ein ABGESCHLOSSENER Aufenthalt zählt ebenfalls. Die Zahl beantwortet „was wurde in diese Tage hinein verkauft“, Geschichte eingeschlossen — sie ist nicht die Zahl der kommenden Autos, die ein Sperrzeitraum stranden ließe, und kann darum höher ausfallen als das Warnfeld der Konsole, das diese engere Frage stellt. Sie ist eine Zahl und sonst nichts: keine Referenz, kein Name, kein Kennzeichen und kein Datum irgendeiner Buchung steht in der Antwort.
PUT /lots/{external_id}/blocked-periods
Ersetzt die GESAMTE Sperrliste des Parkplatzes durch die im Body. Es gibt keinen Aufruf „einen Zeitraum hinzufügen“ und keinen Weg, einen einzelnen Zeitraum zu adressieren — eine Synchronisation, die nur zusammenführen kann, ist eine Synchronisation, die nie löschen kann, und ein in Ihrem System aufgehobener Zeitraum bliebe für immer auf Parkena. Höchstens 100 Einträge; jedes Datum ein echter Kalendertag zwischen 2020-01-01 und 2032-12-31; ends_on nie vor starts_on; und zwei Einträge dürfen sich nicht überlappen — einschließlich gerechnet: Ein Paar, das sich einen einzigen Tag teilt, kollidiert.
Die Liste, die Sie schicken, ist die Liste, die existiert
PUT ist hier eine vollständige Repräsentation, dieselbe Regel wie bei PUT /lots/{external_id} — und auf dieser Route hat die Regel eine Konsequenz, die Großbuchstaben verdient: IN DER KONSOLE ANGELEGTE SPERRZEITRÄUME GEHÖREN ZU DERSELBEN LISTE. Trägt eine Person am Dienstag einen Sperrzeitraum in der Konsole ein und schiebt Ihre nächtliche Synchronisation am Dienstagabend nur ihre eigenen Zeiträume, entfernt die Synchronisation den Zeitraum dieser Person — still und korrekt, denn Sie haben uns gesagt, die geschickte Liste sei die ganze Liste.
Eine Maschine, der diese Route gehört, der gehört der ganze Kalender. Lesen Sie entweder diese Route zurück und führen Sie die in der Konsole angelegten Zeiträume in Ihrem eigenen System mit, oder verabreden Sie mit Ihren eigenen Leuten, welchem System die Sperrzeiträume gehören. Die API wird nicht schlichten.
curl -X PUT "$BASE/lots/LOT-1/blocked-periods" \
-H "Authorization: Bearer $KEY.$SECRET" \
-H 'Content-Type: application/json' \
-d '{
"blocked_periods": [
{ "starts_on": "2026-11-02", "ends_on": "2026-11-08", "reason": "resurfacing" },
{ "starts_on": "2026-12-24", "ends_on": "2026-12-26" }
]
}'reason, was erlaubt ist.| Feld | Typ | Erforderlich | Hinweise |
|---|---|---|---|
starts_on | YYYY-MM-DD | ja | Der erste gesperrte Tag, einschließlich. Ein Kalenderdatum — ein Zeitstempel wird zurückgewiesen, nicht gekürzt. |
ends_on | YYYY-MM-DD | ja | Der LETZTE gesperrte Tag, einschließlich — nicht der Tag danach. Darf starts_on nicht vorausgehen. |
reason | String ≤200, oder null | nein | Eine Beschriftung, die die Konsole zeigt. Leer wird zurückgewiesen; null und weggelassen bedeuten beide „ohne Grund“. Wortwörtlich gespeichert, nie beschnitten — ein geändertes Leerzeichen ist ein geänderter Zeitraum. |
Jede inhaltliche Zurückweisung auf dieser Route trägt denselben Code, 422 {"error":"invalid_blocked_periods"}, mit field, das den fehlerhaften Eintrag in den Koordinaten Ihres eigenen Bodys benennt — blocked_periods[3].ends_on. Die zwei Fehler auf Hüllenebene behalten die Codes, die der Rest der API ihnen gibt: Ein überzähliger Schlüssel auf oberster Ebene ist unknown_field, ein fehlender Schlüssel blocked_periods ist missing_field.
| Was falsch war | `field` |
|---|---|
Mehr als 100 Einträge, oder blocked_periods ist kein Array | blocked_periods |
Kein echter Kalendertag (2026-02-30), ein Zeitstempel, oder außerhalb von 2020..2032 | das starts_on / ends_on des Eintrags |
ends_on vor starts_on | das ends_on des Eintrags |
| Zwei Einträge überlappen sich (einschließlich — ein geteilter Tag genügt) | das starts_on des später beginnenden Eintrags |
reason leer, kein String, oder länger als 200 Zeichen | das reason des Eintrags |
Zeiträume, die verkaufte Buchungen überlappen, werden NICHT zurückgewiesen. Eine maschinelle Synchronisation darf sich nicht an einem Auto verkeilen, das letzte Woche verkauft wurde; die Buchungen bleiben bestehen — ein Sperrzeitraum stoppt nur NEUE Verkäufe. Stattdessen trägt die Antwort die overlapping_bookings-Zahl jedes Zeitraums (Bedeutung oben, abgeschlossene Aufenthalte eingeschlossen), damit Ihr System, und der Mensch dahinter, genau sieht, in welche Sperrzeiträume bereits Autos hinein verkauft sind.
{
"external_id": "LOT-1",
"result": "updated",
"added": 1,
"removed": 1,
"blocked_periods": [
{ "starts_on": "2026-11-02",
"ends_on": "2026-11-08",
"reason": "resurfacing",
"overlapping_bookings": 2 },
{ "starts_on": "2026-12-24",
"ends_on": "2026-12-26",
"reason": null,
"overlapping_bookings": 0 }
]
}201, wenn der Parkplatz vorher gar keine Zeiträume hatte, sonst 200.| Feld | Bedeutung |
|---|---|
result | created (vorher null Zeiträume, jetzt welche), updated (alles andere, das geschrieben hat — auch ein PUT von [], das die Liste geleert hat) oder unchanged. |
added / removed | Die Zeilenzahlen, die der Schreibvorgang tatsächlich bewegt hat. Beide 0 bei unchanged. |
blocked_periods | Die gespeicherte Liste, NACH dem Schreiben neu gelesen, mit frischen overlapping_bookings-Zahlen — was die Datenbank jetzt hält, nie ein Echo Ihres Bodys. |
Die Regel „vergleichen, bevor Sie aktualisieren“ aus §2 gilt hier in Listenform: result: "unchanged" heißt, die gespeicherte Liste und Ihr Body sagten bereits dasselbe, und es wurde überhaupt keine Anweisung ausgegeben. Eine unveränderte Sperrliste jede Nacht erneut zu schieben kostet einen Lesevorgang.
GET /bookings
Ihre Parkena-Buchungen, als PULL-CURSOR. Dieser Cursor ist die maßgebliche Quelle: Ein registrierter Webhook-Endpunkt kann einen signierten booking.changed-Hinweis erhalten, der Ihnen sagt, ihn früher abzuholen — §13 —, aber ein Hinweis trägt keine Buchungsdaten, und nichts, was Sie auf diesem Cursor bauen, ist je vergebens.
| Query-Parameter | Typ | Standard | Hinweise |
|---|---|---|---|
since | String | — | Das next-Token aus Ihrem vorigen Aufruf. Weglassen für „von Anfang an“. |
limit | Ganzzahl 1–500 | 200 | Mehr zu verlangen wird zurückgewiesen, nicht stillschweigend gekürzt. |
curl "$BASE/bookings?since=$CURSOR" -H "Authorization: Bearer $KEY.$SECRET"
#
{
"bookings": [
{
"reference": "PK0000000040",
"external_reference": "OPS-9911",
"lot_external_id": "LOT-1",
"lot_name": "Terminal Park",
"timezone": "Europe/Berlin",
"check_in_local": "2026-09-01T08:00:00",
"check_out_local": "2026-09-05T19:30:00",
"parking_days": 5,
"status": "confirmed",
"payment_status": "paid",
"currency": "EUR",
"total": "56.00",
"channel": "direct",
"flight_number": "LH401",
"customer": {
"first_name": "Ada",
"last_name": "Lovelace",
"email": "[email protected]",
"phone": "+49301234567"
},
"vehicle_plates": ["BXY4242"],
"created_at": "2026-08-19T15:18:01.099213Z",
"cancelled_at": null
}
],
"next": "eyJ2IjoxLCJ0IjoiMjAyNi0wOC0xOVQxNToxMzowOC44MjQxMDhaIiwi…",
"has_more": false
}| Feld | Hinweise |
|---|---|
reference | Die Buchungsreferenz von PARKENA. Das ist der Schlüssel zur Deduplizierung. |
external_reference | Ihre eigene Zeichenkette, sofern eine gesetzt war. Wird zurückgespiegelt, nie als Schlüssel verwendet. |
check_in_local / check_out_local | Die Wanduhr des Parkplatzes selbst, OHNE Zeitzonenversatz. Lesen Sie sie in timezone. |
total | Eine Dezimalzahl als Zeichenkette, kein Float. |
status | pending, confirmed, checked_in, completed oder cancelled. |
payment_status | pending, paid, partial, refunded, expired oder not_required. |
Wie man richtig pollt
cursor = load_saved_cursor() # null on the first run
loop:
r = GET /bookings?since=<cursor>&limit=200
for b in r.bookings:
upsert_into_your_system(b, key = b.reference) # NOT external_reference
cursor = r.next
save_cursor(cursor)
if not r.has_more: sleep(60)Drei Regeln, und jede einzelne trägt:
- Deduplizieren Sie über
reference. Die Zustellung erfolgt mindestens einmal, nie genau einmal. Sie werden dieselbe Buchung mehr als einmal sehen, und das ist richtiges Verhalten und kein Fehler. - Speichern Sie
nextERST, nachdem Sie den Stapel dauerhaft abgelegt haben, nicht davor. Stirbt Ihr Prozess mitten im Stapel, liefert der nicht gespeicherte Cursor erneut aus — was Regel 1 harmlos macht. - Bauen Sie keinen Cursor selbst. Er ist ein Token, das wir ausgegeben haben. Es gibt kein
?since=<an instant I chose>, und dieses Fehlen ist Absicht: Ein Partner, der einen Zeitpunkt in der Zukunft nennt, würde stillschweigend aufhören, seine eigenen Buchungen zu bekommen, und das erste Anzeichen wäre ein Auto an einer Schranke, von dem sein System nie gehört hat. Ein Cursor jenseits unserer Hochwassermarke wird auf sie zurückgezogen; das Schlimmste, was ein manipuliertes Token anrichten kann, ist also, Zeilen doppelt auszuliefern.
Warum Sie eine Buchung sofort erneut sehen können: Der Cursor, den wir zurückgeben, zeigt nie über now() − 5 minutes hinaus. Buchungen, die neuer sind, werden trotzdem geliefert — Sie wollen Ihre Buchung jetzt und nicht in fünf Minuten —, wir schreiben den Cursor nur nicht auf sie fest. Das ist keine Vorsicht um ihrer selbst willen. Eine Datenbanktransaktion trägt den Stempel ihrer STARTZEIT, sodass eine langsame Transaktion eine Zeile festschreiben kann, die früher gestempelt ist als eine, die Sie bereits bekommen haben; ohne den Nachlauf würde ein Cursor, der direkt auf die neueste Zeile springt, dauerhaft und stillschweigend über sie hinwegsteigen.
8. Was Sie eine erneute Prüfung kostet
§2 hat die Regel; dies ist die Feldliste dahinter. Ändert sich eines der folgenden Felder über PUT /lots/{external_id}, wird ein anhängiger oder freigegebener Eintrag hinfällig, und die Antwort sagt es mit "listing_review_superseded": true:
name · timezone · handover · capacity · latitude · longitude · city · country_code · features · arrival · media · min_advance_days · min_stay_days · max_stay_days · cancellation_free_until_hours_before_check_in · cancellation_penalty_percent_after · cancellation_no_show_forfeits_full
Was Sie keine kostet
online_bookable. Es ist Ihr Verkaufsschalter, kein geprüfter Inhalt. Ihn auszuschalten nimmt den Parkplatz sofort aus dem Verkauf, ohne irgendetwas hinfällig zu machen — und genau deshalb ist es bei jedemPUTein Pflichtfeld.- Alles auf der Rates-Route, was den Korridor nicht weitet. Ihre Untergrenze anzuheben oder Ihre Obergrenze zu senken bleibt innerhalb eines Versprechens, das Sie bereits gegeben haben. Die Untergrenze zu senken, die Obergrenze anzuheben, die Währung zu wechseln oder
dynamicumzulegen heißt, ein neues zu geben, und kann die Prüfung hinfällig machen —envelope_widenedsagt Ihnen, wann. valid_from/valid_to. Ablauf und Verlängerung werden live gelesen; eine Änderung des Zeitfensters nimmt den Parkplatz also ohne Zutun eines Menschen aus dem Schaufenster und stellt ihn wieder hinein.
Zwei Fallen, die man kennen sollte
- Die Reihenfolge der Merkmale ist Ihnen egal, uns war sie einmal nicht egal. Wir sortieren
features, bevor wir sie speichern;["valet","indoor"]und["indoor","valet"]sind also derselbe Push. Sie müssen nicht sortieren. - Die Genauigkeit der Koordinaten spielt keine Rolle. Wir runden vor dem Vergleich auf sechs Nachkommastellen; jede Nacht
52.5200001zu schicken meldetlatitudealso nicht für immer als geändert.
Es gibt kein Löschen
Diese API kann weder einen Parkplatz noch einen Preis löschen, und es gibt keine Rechtevergabe, die es ihr erlauben würde. delete-dann-insert ist die verbreitetste Upsert-Redewendung in Import-Code, und auf diesen Daten ist sie verheerend: Der Parkplatz bekäme eine neue Identität und nähme seine Eintragshistorie, seine Preise und seine Verfügbarkeit mit — aus einem Job heraus, dem niemand zusah. Einen Parkplatz zu entfernen ist etwas, das ein Mensch in der Konsole absichtlich tut, wo die Bestätigung alles benennt, was mit ihm verschwindet. Ein Parkplatz, der je eine Buchung angenommen hat, kann überhaupt nicht mehr entfernt werden, von niemandem: Eine Buchung ist ein Finanzbeleg, sie wird aufbewahrt, und der Parkplatz, für den sie angenommen wurde, ebenso. Um einen aus dem Verkauf zu nehmen, setzen Sie "online_bookable": false.
9. Fehler
Jeder Fehler ist JSON mit einem stabilen, maschinenlesbaren error-Code:
{ "error": "invalid_price_envelope", "field": "floor_price" }field ist, wo vorhanden, IHR EIGENER Feldname, zurückgespiegelt. Wir geben nie eine Datenbankmeldung, einen Constraint-Namen oder einen Tabellennamen zurück — die gehören uns und dürfen von uns umbenannt werden, und Ihre Anbindung sollte nicht brechen, wenn wir das tun. Verzweigen Sie über error.
Zugangsdaten
| HTTP-Status | Fehlercode | Bedeutung |
|---|---|---|
401 | unauthorized | Siehe unten — er deckt sechs verschiedene Lagen ab und weigert sich zu sagen, welche. |
Form der Anfrage
| HTTP-Status | Fehlercode | Bedeutung |
|---|---|---|
404 | not_found | Diese ROUTE gibt es nicht. Nie für eine Ressource verwendet. |
405 | method_not_allowed | Richtiger Pfad, falsches Verb. Der Allow-Header nennt das Verb, das wir wollten. |
413 | payload_too_large | Body über 64 KiB. |
415 | unsupported_media_type | Content-Type war nicht application/json. |
422 | invalid_json | Der Body war kein parsbares JSON. |
422 | invalid_body | Wohlgeformtes JSON, falsche Form oder ein unbrauchbarer Wert. |
422 | missing_field | Ein Pflichtfeld fehlte. |
422 | unknown_field | Ein Feld, das wir nicht kennen. Zurückgewiesen, nicht ignoriert. |
422 | field_belongs_to_another_route | Ein Feld, das es gibt, das aber woanders gesetzt wird. Heute ist das einzige rates — siehe §2. |
422 | invalid_query | Ein fehlerhafter oder unbekannter Query-Parameter. |
422 | invalid_cursor | Das since-Token war keines von unseren. |
422 | too_many_lots | Ein Array als Body. Eine Anfrage, ein Parkplatz. |
Inhalt
| HTTP-Status | Fehlercode | Bedeutung |
|---|---|---|
422 | invalid_timezone | Kein IANA-Zeitzonenname. |
422 | invalid_coordinates | Außerhalb des Bereichs oder eine Breite ohne Länge. |
422 | invalid_country_code | Nicht ISO 3166-1 alpha-2. |
422 | invalid_stay_bounds | min_stay_days / max_stay_days widersprechen einander. |
422 | incomplete_cancellation_policy | Einer oder zwei der drei Schlüssel zur Stornierung. |
422 | invalid_price_envelope | Untergrenze, Basis und Obergrenze stehen nicht in der richtigen Ordnung. |
422 | invalid_first_day_price | min_first_day_price liegt außerhalb des Korridors. |
422 | invalid_validity_window | valid_to liegt vor valid_from. |
422 | currency_is_not_settlement_currency | Nicht Ihre Abrechnungswährung. |
422 | invalid_blocked_periods | Ein Sperrzeitraum-Eintrag ist unbrauchbar; field benennt ihn in den Koordinaten Ihres eigenen Bodys (blocked_periods[3].ends_on). Die Tabelle der Zurückweisungen steht in §7. |
409 | conflict | Ein echtes Wettrennen: Zwei Pushes haben dieselbe external_id gleichzeitig angelegt. Wiederholen. |
Unsere
| HTTP-Status | Fehlercode | Bedeutung |
|---|---|---|
429 | rate_limited | Über einer Grenze. Beachten Sie Retry-After. |
503 | try_again | Ein vorübergehender Konflikt in der Datenbank. Wiederholen; Retry-After ist gesetzt. |
500 | internal_error | Unser Fehler. Er steht vollständig in unseren Logs. Ein Wiederholungsversuch ist vernünftig. |
Warum das 401 Ihnen nicht mehr sagt
unauthorized deckt all das hier ab und weigert sich, es zu unterscheiden:
- Kein
Authorization-Header, oder einer, den wir nicht lesen können. - Eine
key_id, die keine Zugangsdaten benennt. - Eine
key_id, die es gibt, mit dem falschen Secret. - Zugangsdaten, die widerrufen oder abgelaufen sind oder deren Betreiberkonto gesperrt ist.
- Gültige Zugangsdaten, denen der Scope fehlt, den diese Route verlangt.
- Gültige Zugangsdaten, die eine
external_idnennen, die keiner ihrer Parkplätze ist.
Die Fälle 2–5 sind nicht einmal innerhalb unseres eigenen Prozesses zu unterscheiden — die Datenbank beantwortet sie alle gleich, mit derselben verrichteten Arbeit, damit niemand die Liste der Betreiber aufzählen oder kartieren kann, welche Fähigkeiten eines gestohlenen Schlüssels noch funktionieren.
Fall 6 kostet Sie etwas Bequemlichkeit, und wir sagen lieber, warum, statt es nur zu behaupten: Zugangsdaten, die nur write_supply halten, können keine Parkplätze auflisten. Würde eine unbekannte external_id mit lot_not_found antworten, hätten diese Zugangsdaten ein funktionierendes Aufzählungsorakel über genau den Bestand, den ihre Scopes ihnen verwehren — gebaut aus einer Zurückweisung. Also bekommen sie dasselbe 401.
GET /ping ist die Antwort darauf. Es sagt Ihnen, dass Ihr Schlüssel gültig ist und was er darf, ohne dass Sie an einem 401 herumraten müssten. Gelingt der Ping und antwortet eine Route mit 401, dann sehen Sie einen Scope, den Sie nicht halten, oder eine external_id, die Ihnen nicht gehört — prüfen Sie beides gegen GET /lots.
10. Ratenbegrenzungen, Größenbegrenzungen und was wir protokollieren
| Grenze | Wert |
|---|---|
| Body der Anfrage | 64 KiB |
| Parkplätze pro Anfrage | 1 |
Seite von GET /lots | 1–200, Standard 50 |
Seite von GET /bookings | 1–500, Standard 200 |
Sperrzeiträume pro PUT …/blocked-periods | 100 — eine Liste, ein Parkplatz |
Die Ratenbegrenzungen sind Token-Buckets — eine Burst-Kapazität, die sich fortlaufend wieder auffüllt.
| Bucket | Gezählt gegen | Burst-Kapazität | Auffüllung |
|---|---|---|---|
| Alle Anfragen | Quelladresse | 240 | 4 / Sekunde |
| Fehlgeschlagene Authentifizierungen | Quelladresse | 20 | 1 pro 3 Sekunden |
| Lesevorgänge | Zugangsdaten | 120 | 2 / Sekunde |
| Schreibvorgänge (Burst) | Zugangsdaten | 60 | 1 / Sekunde |
| Schreibvorgänge (pro Stunde) | Zugangsdaten | 1000 | 1000 / Stunde |
Eine Zurückweisung ist ein 429 mit einem Retry-After-Header in ganzen Sekunden. Zurückgewiesen zu werden verbraucht kein Token; ein Wiederholungsversuch schiebt Ihre eigene Erholung also nicht weiter weg.
Schreibvorgänge sind doppelt begrenzt, und das Stundenfenster ist das, worauf es ankommt. Ein durchgesickerter Schreibschlüssel, der einen Bestand auf null setzt, sieht genau aus wie eine rechtmäßige nächtliche Synchronisation — dieselben Zugangsdaten, dieselbe Route, dieselbe Form, dieselbe Nachtstunde —, eine Burst-Grenze allein kann die beiden also nicht auseinanderhalten, denn eine echte Synchronisation ist ebenfalls ein Burst. Die Stundengrenze begrenzt, wie viel von einem Bestand ein gestohlener Schlüssel umschreiben kann, bevor ein Mensch plausibel hinsehen könnte. Ein Betreiber mit 1000 Parkplätzen, der jeden einmal pro Nacht schiebt, passt hinein; ist Ihr Bestand größer, fragen Sie uns, und wir heben die Grenze an, statt Sie zu einem Umweg zu zwingen.
Die Sperrzeitraum-Routen zehren von denselben Buckets wie alles andere: Das GET kostet ein Lese-Token, und das PUT ist ein Schreibvorgang, gezählt gegen beide Schreib-Buckets — eine nächtliche Sperrzeitraum-Synchronisation zählt gegen dieselbe Grenze von 1000 pro Stunde wie Ihre Parkplatz-Pushs, bewusst, denn ein durchgesickerter Schlüssel, der einen Bestand aus dem Verkauf nimmt, ist genau die Form, für deren Begrenzung diese Grenze existiert.
Fehlgeschlagene Authentifizierungen werden GEGEN DIE QUELLADRESSE gezählt, nie gegen die vorgelegte key_id. Sie gegen den Schlüssel zu zählen wäre ein Denial-of-Service, der auf Sie zielt: key_id ist die nicht geheime Hälfte und taucht bewusst in Logs und Konfigurationsdateien auf; wer eine gelesen hat, könnte Sie also mit ein paar Dutzend falschen Secrets aus Ihrer eigenen Anbindung aussperren.
Beachten Sie, dass eine unbekannte external_id ebenfalls vom Fehlerbudget zehrt, denn sie liefert dasselbe 401 wie alles andere. Eine Begrenzung, die beides unterschiedlich behandelte, wäre ein Existenzorakel, gebaut aus einem 429. Wenn Sie eine veraltete Zuordnung mit sich führen, gleichen Sie sie gegen GET /lots ab, statt zu sondieren.
Was wir protokollieren
Jede Anfrage erzeugt auf unserer Seite eine Logzeile; jeder Schreibvorgang, der etwas geändert hat, erzeugt eine zweite, die die FELDNAMEN trägt, die sich geändert haben, und ob die Änderung Sie eine Prüfung gekostet hat. Feldnamen, nie Werte — das Log beantwortet „was ist letzte Nacht mit diesem Bestand geschehen“, und Ihre Preise stehen nicht darin. Ihre key_id schon, und darüber können Sie uns fragen, welche Ihrer Anbindungen etwas getan hat. Ihr Secret steht nie darin, in keiner Form.
11. Bevor Ihr Parkplatz verkaufen kann
Einen Parkplatz über diese API hineinzuschieben legt ihn an, aber ein neuer Parkplatz beginnt nicht in dem Moment auf parkena.com zu verkaufen, in dem die API 201 zurückgibt. Ein Teil dessen, was dafür nötig ist, sind Inhalte, die diese API liefern kann, und ein Teil ist eine Entscheidung, die ein Mensch in der Konsole bestätigen muss.
Ein vollständiges PUT /lots/{external_id} plus ein PUT …/rates erfüllt die Anforderungen an Kapazität, Preis, Geografie, Stadt und Land sowie an die Stornierungsregelung. Offen bleibt, und nur in der Konsole zu erledigen:
- Die Zeitzone und die Buchungsregeln bestätigen — sie sind abgeleitet und vorbelegt, und eine plausible falsche Antwort bepreist Buchungen stillschweigend falsch, deshalb bestätigt ein Mensch sie einmal.
- Hinweise zur Anfahrt und ein Titelbild, für den öffentlichen Eintrag.
- Die Parkena-Eintragsvereinbarung annehmen, einmal für das ganze Konto.
- Den Parkplatz zur Prüfung einreichen.
Das Letzte ist Absicht: Einen Parkplatz einem menschlichen Prüfer vorzulegen ist eine Aussage, die Sie über Ihr Geschäft treffen, und eine Maschine, die einen Schlüssel hält, sollte sie nicht in Ihrem Namen treffen können.
Die Konsole zeigt jede Anforderung, ob sie erfüllt ist und was sie schützt. Das ist keine Einschränkung, die wir in v1 aufheben wollen.
12. Was v1 nicht tut
Klar gesagt, denn eine Anbindung, die auf einer Annahme steht, die wir nie gemacht haben, ist schlimmer als eine, die auf einer dokumentierten Lücke steht.
- Keine Echtzeit-Verfügbarkeitszahlen. Sie können jetzt mit
PUT /lots/{external_id}/blocked-periods— §7 — ganze Datumsbereiche aus dem Verkauf nehmen, aber es gibt weiterhin keinen Weg, „heute Nacht freie Stellplätze“ als Zahl zu melden, undcapacitysind die Stellplätze insgesamt — die Echtzeitverfügbarkeit dort einzutragen meldet Ihren Parkplatz falsch und schickt ihn jede Nacht in die erneute Prüfung. Ein zahlenbasierter Verfügbarkeitskalender ist nicht Teil von v1. - Keine Webhook-Nutzlasten. Ein registrierter Endpunkt erhält den signierten
booking.changed-HINWEIS aus §13 — eine Buchungsreferenz und sonst nichts —, und der Pull-Cursor bleibt die maßgebliche Quelle, genau wie diese Liste es versprochen hat, bevor es den Hinweis gab. Was es weiterhin nicht gibt: Nutzlasten je Ereignis (in einem Webhook reisen nie Buchungsdaten), Ordnungsgarantien (Hinweise verschmelzen und wiederholen sich; die Reihenfolge ist Sache des Cursors) oder eine Replay-API (nichts zum Wiederabspielen — holen Sie den Cursor erneut ab). Eine Anbindung muss mit abgeschalteten Hinweisen funktionieren, denn ein Hinweis, der fünf Zustellungen scheitert, stirbt leise und mit Absicht. - Kein Schreiben von Buchungen. Sie können über diese API keine Buchung anlegen, ändern, stornieren, einchecken oder erstatten. Es gibt keinen Scope dafür und keine Rechtevergabe dahinter.
- Keine Zahlungen. Keine Abbuchungen, keine Erstattungen, keine Auszahlungsdaten, keine Provisionszahlen. Der Buchungsstrom trägt den Gesamtpreis des Verkaufs und die Währung und nichts darüber, wie das Geld geflossen ist.
- Keine Verteilung an OTAs oder Channel-Manager. Diese API schreibt nur den Kanal
parkena. Sie ist kein Channel-Manager und schiebt zu niemandem sonst. - Kein Löschen. Siehe §8.
- Kein Sammel-Endpunkt. Eine Anfrage, ein Parkplatz.
- Kein Einreichen von Einträgen und kein Prüfstatus. Sie können über die API weder etwas zur Prüfung einreichen noch Ihren Prüfstatus lesen.
listing_review_supersededundenvelope_widenedsind die einzigen Signale in der Nähe der Prüfung, und das zweite ist bewusst vorsichtig. - Keine Mandantenwahl. Es gibt kein
tenant_idin irgendeinem Body und in keiner Query, die diese API auswertet. Auf welchen Betreiber eine Anfrage wirkt, ergibt sich aus den Zugangsdaten und aus sonst nichts — eine vom Aufrufer gelieferte Betreiber-ID wäre ein Schlüssel zum Schreiben über Mandantengrenzen hinweg. - Keine vom Aufrufer gelieferten Zeitstempel, nirgendwo. Kein
as_of, keinwatermark, keinupdated_since. Die einzigen Daten, die Sie schicken dürfen, sindvalid_fromundvalid_to, und das sind Kalendertage, die Sie über Ihren eigenen Preis erklären. Alles andere ist unsere Uhr.
13. Webhooks: der `booking.changed`-Hinweis
Sie können je Betreiberkonto einen HTTPS-Endpunkt registrieren, und wir schicken ihm per POST einen signierten Hinweis, wann immer eine Ihrer Buchungen angelegt wird oder sich ändert — jeder Kanal, jedes Feld. Lesen Sie die Warnung darunter, bevor Sie irgendetwas darum herum entwerfen.
Ein Hinweis trägt keine Daten. Der Cursor trägt die Daten.
Der gesamte Body eines Hinweises ist ein Ereignisname, eine Buchungsreferenz und ein Zeitstempel. Kein Status, keine Aufenthaltsdaten, kein Reisender, keine Beträge — nichts, worauf Ihr System direkt handeln könnte, und nichts, das unterwegs veraltet. Die einzig richtige Antwort auf einen Hinweis ist das, was Ihre Anbindung ohnehin tut: GET /bookings mit Ihrem gespeicherten Cursor abholen.
Ein Partner, dessen Endpunkt einen Tag lang ausfällt, verliert Latenz, nie Daten — der Cursor liefert beim nächsten Abruf alles nach. Wenn Ihre Anbindung mit abgeschalteten Hinweisen nicht überleben kann, ist sie falsch gebaut.
Diese Arbeitsteilung ist der Grund, warum §12 nicht mehr „keine Webhooks“ sagt: Was wir uns weigerten auszuliefern, war ein Webhook, der die Buchung TRÄGT, denn ein Webhook, der still fehlschlägt, ist eine Buchung, von der Sie nie erfahren, während das Auto trotzdem an Ihrer Schranke ankommt. Ein Hinweis darf still fehlschlagen und kostet Sie nichts.
Die Zustellung
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Parkena-Webhooks/1
Parkena-Signature: t=1767139200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
#
{"event":"booking.changed","booking_reference":"PK0000000040","occurred_at":"2026-08-27T09:15:12.114532Z"}booking.changed, ein Name für Anlegen, Ändern und Stornieren gleichermaßen, denn der Hinweis sagt nicht, was passiert ist; das sagt der Cursor.booking_referenceist dieselbereference, die der Buchungsstrom trägt — geben Sie sie in Ihre Deduplizierung, genau wie Sie Cursor-Zeilen deduplizieren.occurred_atist der Moment, in dem die Änderung bei uns verzeichnet wurde, nicht der, in dem dieser Versuch geschickt wurde. Wiederholungen schicken ihn wortwörtlich erneut.
Aufeinanderfolgende Änderungen derselben Buchung VERSCHMELZEN, solange ein Hinweis zu ihr noch unzugestellt ist: Fünf Änderungen in einer Minute erzeugen einen einzigen Ping, und das ist korrekt, gerade weil der Hinweis keinen Zustand trägt — für wie viele Schreibvorgänge er auch steht, Ihr nächster Cursor-Abruf sieht die endgültige Zeile. Die Zustellung ist mindestens einmal, wie alles andere auf dieser API: Sie können zwei Hinweise für eine Änderung erhalten, und die Deduplizierung auf booking_reference macht das kostenlos.
Einen Endpunkt registrieren, und die Regeln, die er erfüllen muss
Endpunkte werden in der Konsole registriert, auf derselben Seite Account → API keys, auf der Schlüssel ausgestellt werden, von einem Inhaber oder Manager — nicht über diese API, aus demselben Grund wie bei den Schlüsseln. Das Signaturgeheimnis (whsec_ gefolgt von 64 Hexadezimalzeichen) wird in Ihrem Browser erzeugt und EINMAL gezeigt: Wir speichern es zum Signieren, aber kein Konsolenbildschirm und keine Abfrage kann es je zurücklesen. In v1 darf je Konto ein Endpunkt aktiv sein. Ein Endpunkt wird nie bearbeitet — eine neue URL oder ein rotiertes Geheimnis ist ein neuer Endpunkt (entwaffnen Sie zuerst den alten); das Entwaffnen ist der einzige Schalter, den die Konsole nach der Geburt anbietet.
- Nur HTTPS, nur Port 443.
http://, und jeder explizite Port außer 443, wird nie versucht. - Ein Hostname, keine Adresse. IP-Literale (v4 oder v6),
localhostund alles auf den Domains unserer eigenen Plattform werden zurückgewiesen. - Umleitungen werden nie befolgt. Eine Umleitung ist eine zweite URL, die niemand geprüft hat; der Versuch schlägt stattdessen fehl.
- Fünf Sekunden Zeitlimit, und über den Statuscode hinaus wird Ihre Antwort nie gelesen. Antworten Sie schnell und arbeiten Sie danach — die richtige Form ist „einreihen und 204 zurückgeben“.
Der 2xx-Vertrag, Wiederholungen und der Tod
Jedes 2xx innerhalb des Zeitlimits heißt zugestellt. Alles andere — ein 4xx, ein 5xx, eine Zeitüberschreitung, eine verweigerte Verbindung — wird nach einem festen Backoff wiederholt, und nach dem fünften gescheiterten Versuch ist der Hinweis tot: Der letzte HTTP-Status und der Grund des Scheiterns werden verzeichnet und sind in der Konsole sichtbar, und kein weiterer Versuch findet statt. Die Buchung selbst wartet, wie immer, auf dem Cursor. Einen Endpunkt zu entwaffnen tötet seine ausstehenden Hinweise beim nächsten Durchlauf, statt sie später einem Endpunkt zuzustellen, den Sie abgeschaltet haben.
| Gescheiterte Versuche bisher | Nächster Versuch |
|---|---|
| 1 | nach 1 Minute |
| 2 | nach 5 Minuten |
| 3 | nach 30 Minuten |
| 4 | nach 2 Stunden |
| 5 | keiner — der Hinweis ist tot, letzter Status und Grund verzeichnet |
Die Signatur prüfen
Jede Zustellung trägt einen Parkena-Signature-Header: t=<unix-seconds>,v1=<hex HMAC-SHA-256>. Die signierte Nutzlast ist die wörtliche Zeichenkette t + . + der ROHE Anfrage-Body — signieren Sie die empfangenen Bytes, nie eine Re-Serialisierung des geparsten JSON. Das ist mit Absicht genau das Schema, das Stripe für seine Webhooks verwendet, whsec_-Präfix des Geheimnisses eingeschlossen: Jeder Stripe-Webhook-Prüfer, den Sie ohnehin betreiben — oder der im eigenen Repository von Parkena veröffentlichte —, prüft diese Zustellungen unverändert.
# header: "t=1767139200,v1=5257a869…" secret: "whsec_…" exactly as shown once
const pairs = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = hexHmacSha256(secret, `${pairs.t}.${rawBody}`);
const fresh = Math.abs(nowSeconds() - Number(pairs.t)) <= 300;
const ok = fresh && timingSafeEqual(pairs.v1, expected);Drei Details, die eine schnelle Implementierung falsch macht: Vergleichen Sie mit einer Gleichheit in KONSTANTER ZEIT, nicht ===, damit die Antwortzeit nicht verrät, wie viel einer gefälschten Signatur richtig war; weisen Sie ein t zurück, das älter als ein paar Minuten ist — wir empfehlen 300 Sekunden —, denn das macht eine mitgeschnittene Zustellung für ein späteres Wiederabspielen wertlos; und wenn Sie den Header sauber parsen, statt ihn naiv zu zerteilen, prüfen Sie gegen JEDES vorhandene v1=-Paar und akzeptieren Sie, wenn eines passt — das ist es, was eine künftige Rotation des Signaturgeheimnisses davon abhält, Sie mitten im Fenster zu brechen.
Ein Hinweis, der Ihre Prüfung nicht besteht, ist keine Parkena-Zustellung. Antworten Sie ihm 401 und tun Sie sonst nichts — insbesondere holen Sie den Cursor nicht in dem Takt ab, den er Ihnen diktiert. Den Cursor abzuholen ist immer UNGEFÄHRLICH; die Weigerung dient dazu, keinen unauthentifizierten Aufrufer den Takt Ihres Systems bestimmen zu lassen.
14. Zugang bekommen und Hilfe bekommen
Schlüssel werden in der Konsole von einem Inhaber oder Manager unter Account → API keys ausgestellt. Es gibt keine Sandbox, und der Zugang zum Pilotbetrieb wird mit uns einzeln je Betreiber verabredet — wenn das, was Sie hier gelesen haben, zu dem System passt, das Sie ohnehin betreiben, dann ist das die Sache, die Sie uns schreiben sollten.
Schreiben Sie an [email protected]. Für Unterstützung bei einer bereits laufenden Anbindung: Nennen Sie Ihre key_id — nie Ihr Secret — sowie die external_id und den Zeitstempel der Anfrage, um die es geht. Beide stehen in unseren Logs und identifizieren zusammen eine einzelne Anfrage.
Fragen Sie nach dem Pilotbetrieb.
Sagen Sie uns, was Ihr System ist und was es hineinschieben soll. Wir sagen Ihnen ehrlich, ob v1 das abdeckt — §12 ist die vollständige Liste dessen, was es nicht kann, ausgeschrieben, damit Sie sich dagegen entscheiden können, bevor Sie irgendetwas bauen.
