TourAPI – API-Handbuch v1
Käufer-API v1: Ablauf, Regeln, Fehlerkatalog und Beispiele – jedes Beispiel läuft bei jedem Build als Test.
Inhalt
- 1. Einstieg
- 2. Ablauf einer Anbindung
- 3. Preis: /v1/price und /v1/prices (BA)
- 4. Suche: /v1/search (BA)
- 4a. Offene Suche: POST /v1/search/open (BA)
- 4b. Termin-Matrix: POST /v1/search/open/dates (BA)
- 4c. Grenzen des Keys: GET /v1/limits
- 5. Buchung (B) und Buchungsinfo
- 6. Storno (S): POST /v1/cancel
- 7. Fehlerkatalog
- 8. Fairness: Rate-Limit und Such-Gate
- 9. Idempotenz und Nebenläufigkeit
- 10. EDF-Lieferung (Cache-Export)
- 11. Sandbox: Test-Keys und Testbuchungen
- 12. Hotelinhalte: /v1/content/*
- 13. Geplant und Grenzen
- Anhang: Grenzen auf einen Blick
Diese Doku ist ohne Anmeldung lesbar und enthält keine Zugangsdaten. Für Werkzeuge und KI-Agenten: llms.txt · llms-full.txt · handbuch.de.md · openapi.yaml
Im Handbuch suchen
Für Entwickler, die ein Reiseportal oder ein Veranstaltersystem an TourAPI anbinden.
Stand: 30.09.2026, API-Version v1; Änderungen stehen im Changelog. Maschinenlesbar: openapi.yaml
(OpenAPI 3.1). Für KI-Agenten (Claude Code, Codex, Antigravity): Kurzfassung mit Ablauf,
Formaten und Wiederhol-Regeln im Portal unter /doku/agenten.md, nach der Anmeldung mit dem
eigenen Zugang ausgefüllt (Seite „KI-Agenten“, Download als AGENTS.md).
Jedes Beispiel in diesem Handbuch ist ein Test. Die Anfragen stehen unter
beispiele/ (im Format der REST-Clients von JetBrains und VS Code) und laufen bei
jedem Build gegen eine frische TourAPI-Instanz mit Testdaten; die gezeigten Antworten
werden dabei mit den echten verglichen.
| Kürzel | Bedeutung | Endpunkt |
|---|---|---|
| BA | Verfügbarkeit und Preis anfragen, suchen | /v1/search, /v1/price, /v1/prices |
| BA | offene Suche ohne festen Termin (Recht je Key) | /v1/search/open (Abschnitt 4a) |
| BA | Termin-Matrix eines Hotels (Recht je Key) | /v1/search/open/dates (Abschnitt 4b) |
| – | wirksame Grenzen des eigenen Keys | /v1/limits (Abschnitt 4c) |
| B | buchen | /v1/book |
| – | Buchungsinfo lesen | /v1/booking |
| S | stornieren | /v1/cancel |
| – | Gesundheit des Knotens | /v1/health (Abschnitt 1.8) |
| – | EDF-Lieferung (Cache-Export) | /v1/export/edf/full, /changes, /ack (Abschnitt 10) |
| – | Sandbox: Test-Keys, Testbuchungen, Szenarien | alle Endpunkte mit Test-Key (Abschnitt 11) |
| – | Hotelinhalte (Stamm, Lage, Texte, Bilder, Ausstattung) | /v1/content/hotels, /changes, /catalog (Abschnitt 12) |
Konsole/Agenten: nicht Teil der Käufer-API (POST /changes der Veranstalter-Konsole,
Anmeldung über die Sitzung, kein API-Key; Beschreibung und Grenzen auf Anfrage beim Betreiber).
1. Einstieg
1.1 Zugang
- Jede Anfrage trägt den API-Key im Kopf
X-Api-Key. Es gibt keine Sitzung, kein Token. - Den Key vergibt der Veranstalter (Zugangspaket). Ein Key gehört zu genau einem
Veranstalter (Mandant) und optional zu einer Kundengruppe. Die Kundengruppe bestimmt
Sonderpreise und, als Kontingent-Gruppe, das Kontingent, gegen das gebucht wird. Ein Key
ohne Gruppe arbeitet auf dem Basisvertrag. Welche Art eine Kundengruppe ist, legt der
Veranstalter fest:
- Kontingent-Gruppe (Vertriebspartner mit Kontingent): sieht nur die Hotels, für die sie
eine Zuteilung hat, und bucht gegen diese Zuteilung. Andere Hotels gibt es für den Key
nicht (
404 ERR_HOTEL_NOT_FOUND, nicht in Suche und/v1/destinations). Hat sie gerade keine Zuteilung, sieht sie kein Hotel. - Preisgruppe: sieht alle Hotels des Veranstalters und bucht aus dem allgemeinen Bestand wie ein Key ohne Gruppe – nur zu ihrem Preis.
- Kontingent-Gruppe (Vertriebspartner mit Kontingent): sieht nur die Hotels, für die sie
eine Zuteilung hat, und bucht gegen diese Zuteilung. Andere Hotels gibt es für den Key
nicht (
- Lässt sich der Sonderpreis einer Kundengruppe (mit oder ohne Zuteilung) für ein Hotel nicht
rechnen (fehlerhafte Preisaktion beim Veranstalter), gibt es das Hotel für diese Gruppe
nicht (
404 ERR_HOTEL_NOT_FOUND, in der Suche GrundERR_HOTEL_NOT_FOUNDindiagnostics.reasons, auch/v1/booklehnt mit404ab) – nie still den Basispreis. - Mandant, Kundengruppe und Preise kommen nur aus dem Key, nie aus einem Parameter.
Hotels anderer Veranstalter gibt es für den Key nicht (
404). - Fehlender, unbekannter oder widerrufener Key:
401 ERR_UNAUTHORIZED. Gueltiger Key eines gesperrten Veranstalters:403 ERR_TENANT_SUSPENDED. Key einer deaktivierten Kundengruppe:403 ERR_KEY_GROUP_INACTIVE(nie still Basisvertrag). - Keys gehören nicht in Browser-Code oder Logs. Wer einen Key verloren hat, lässt ihn beim Veranstalter widerrufen – oder sperrt ihn selbst im Abnehmer-Portal (falls der Veranstalter es freigeschaltet hat).
- Keys im Abnehmer-Portal: Dort sehen Sie die Keys Ihres Zugangs, holen vom Veranstalter
freigegebene Live-Keys ab, rotieren Live-Keys und sperren eigene Keys. Den Rohwert zeigt das
Portal genau einmal. Eine Sperre lehnt die API nach wenigen Sekunden ab (
401). - Rotation: Der neue Live-Key hat dieselben Rechte (Kundengruppe, Export, Inhalte, Suchprofil). Der
bisherige gilt noch eine vom Veranstalter festgelegte Zeit (Standard 24 Stunden, 1 Stunde
bis 7 Tage) und wird danach auf die Sekunde abgewiesen (
401 ERR_UNAUTHORIZED). Stellen Sie in dieser Zeit alle Systeme auf den neuen Key um. Den neuen Key rotieren Sie erst wieder, wenn der bisherige abgelaufen ist – so gelten nie mehr als zwei Keys gleichzeitig. - Zum Entwickeln gibt es Test-Keys (
tk_test_…): gleiche Daten wie der Live-Key, Buchungen nur in der Sandbox (Abschnitt 11). Jede Antwort nennt den Modus im KopfX-TourAPI-Mode.
1.2 Basis-URL, Transport, Version
- Basis-URL steht im Zugangspaket; in den Beispielen
{{baseUrl}}. - Alle Pfade beginnen mit
/v1. Was sich innerhalb vonv1ändern darf, regelt die Kompatibilitätszusage (Abschnitt 1.9). Unbekannte Antwortfelder bitte ignorieren. - Anfragen: JSON in UTF-8,
Content-Type: application/json, höchstens 1 MiB Body.GETsind/v1/booking,/v1/destinations,/v1/healthund die EDF-Lieferung (/v1/export/edf/full,/changes), alle übrigen sindPOST; falsche Methode:405 ERR_METHOD_NOT_ALLOWED. - Zeitstempel in Antworten (
bookedAt,updatedAt) sind RFC 3339 in UTC, z. B.2026-09-26T08:15:03Z. - Der Server bricht eine Verbindung nach 15 s Lesen bzw. 15 s Schreiben ab. Ausnahme: Pakete der
EDF-Lieferung (
/v1/export/edf/full,/changes) dürfen bis 10 min schreiben (Abschnitt 10). - Ein unbekannter Pfad (z. B. Tippfehler in
/v1/…) ist404ohne JSON-Körper (text/plain, keinerrorCode); alle anderen Fehler haben die Form aus 1.6.
1.3 Datum, Aufenthalt, Stichtag
- Datumsfelder sind Kalendertage
JJJJ-MM-TTohne Uhrzeit und Zone. - Ein Aufenthalt ist halb offen:
checkInist die erste Nacht,checkOutder Abreisetag.checkIn=2026-10-26,checkOut=2026-10-28sind zwei Nächte. - Stichtag ist das Serverdatum in der Zone Europe/Berlin. Frühbucher- und
Zeitraumregeln rechnen immer damit. Das Anfragefeld
nowist nur noch geduldet: leer lassen. Schickt man ein anderes Datum, rechnet die Auskunft trotzdem mit dem Serverdatum und sagt es inwarnings;/v1/booklehnt ein abweichendespriceCheck.nowab (ERR_NOW_MISMATCH). - Grenzen je Anfrage (greifen vor jeder Rechnung):
| Grenze | Wert | Code (422) |
|---|---|---|
| Nächte je Aufenthalt | 1–30 | ERR_EMPTY_STAY, ERR_STAY_TOO_LONG |
| Anreise frühestens | heute (Stichtag) | ERR_STAY_IN_PAST |
| Anreise spätestens | Stichtag + 732 Tage | ERR_STAY_TOO_FAR |
| Reisende je Anfrage | 1–20 | ERR_NO_TRAVELLERS, ERR_TOO_MANY_TRAVELLERS |
| Alter | 0–120 | ERR_INVALID_AGE |
1.4 Geld
- Alle Beträge sind ganze Cent (
…Cents, Ganzzahl). 18000 = 180,00. - Rundung: kaufmännisch auf 2 Stellen, einmal je Reisendem (
roundingin jeder Preis-Antwort:{"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"}– dieselbe Regel, die das EDF des Hotels deklariert). Die Posten eines Reisenden werden exakt (ungerundet) summiert, erst die Summe wird gerundet: das istperTravellerCents[i],totalCentsist deren Summe. Ein Posten wie „−15 % auf 89,90“ ist −13,485 und bleibt es bis zur Summe. Den exakten Betrag je Posten zeigtbreakdown[].amountExact(Abschnitt 3.1). - Die Währung ist die Vertragswährung des Hotels (
currency, ISO 4217). TourAPI rechnet nicht um.currencyin der Anfrage ist ein Wunsch: weicht er ab, kommt die Antwort in Vertragswährung mit Hinweis inwarnings. Ist am Vertrag keine Währung hinterlegt, bleibtcurrencyleer, ebenfalls mit Hinweis – es wird nicht „EUR“ geraten. - Bei
/v1/bookmitpriceCheckist ein Währungswiderspruch dagegen ein Fehler (ERR_CURRENCY_NOT_AVAILABLE): Beträge ohne gemeinsame Einheit werden nicht verglichen.
1.5 Belegung
occupancy.travellers ist die Liste der Reisenden eines Zimmers, je Reisendem das
age am Anreisetag. name und type dürfen mitkommen, haben aber keine Wirkung auf den
Preis. Kinderpreise, Vollzahler und Mindestbelegung ergeben sich aus dem Vertrag:
- Ob jemand Kind oder Erwachsener ist, entscheidet das Kinderband des Zimmers (z. B. 2–11 Jahre: ein 14-Jähriger zahlt den Erwachsenenpreis), nicht eine feste Grenze.
- Ein Zimmer kann eine Zahl von Vollzahlern verlangen (z. B. 2 im Doppelzimmer). Reisen weniger Erwachsene, besetzt ein Kind die freie Vollzahler-Stelle und zahlt den vollen Grundpreis (Verpflegung zum Kindersatz); Kinderermäßigungen gelten erst für Kinder darüber hinaus.
- Staffeln wie „1. Kind / 2. Kind“ zählen die Kinder in der Reihenfolge, die das Hotel festlegt (ältestes oder jüngstes zuerst). Dieselbe Reihenfolge bestimmt, welches Kind eine freie Vollzahler-Stelle besetzt.
- Wer jünger ist als das Kinderband, ist Säugling: beim Preis je Person ohne Grundpreis, nie auf einer Vollzahler-Stelle; Verpflegung oder ein eigener Säuglingspreis nur, wenn der Vertrag sie nennt. Bei „ab 3 Personen“-Angeboten zählt ein Säugling nur mit, wenn das Zimmer ihn zur Belegung zählt.
- Kein Reisender zahlt weniger als 0. Prozent-Rabatte rechnen auf das, was der Reisende nach seiner Kinder- oder Personenermäßigung schuldet (ein freies Kind bleibt frei, ein Frühbucher −20 % trifft bei −50 % Kinderpreis die Hälfte).
1.6 Antworten, Fehler, Hinweise
- Erfolg:
200mit dem Ergebnis. Fehler:4xx/5xxmit{"errorCode": "ERR_…", "message": "…", "warnings": ["…"]}errorCodeist stabil und für Programme gedacht,messageist ein deutscher Text für Menschen und kann sich ändern. Vollständige Liste: Abschnitt 7. warnings(optional, auch in Erfolgsantworten): Was die API an der Anfrage bemerkt hat, ohne abzulehnen – ignorierte Felder, abweichende Währung, ignoriertesnow. Bitte loggen. Viele Integrationsfehler stehen genau dort.- Rechenzeit: Jede Antwort eines Endpunkts – auch Fehlerantworten und
/v1/health– trägt den KopfServer-Timing: tourapi;dur=<ms>(W3C Server Timing), z. B.tourapi;dur=12.4: Millisekunden vom Eingang der Anfrage am Endpunkt bis zum Beginn der Antwort, einschließlich Lesen des Anfrage-Körpers und Wartezeit an Takt und Such-Gate (Abschnitt 8), ohne die Übertragung der Antwort. Die Differenz zur selbst gemessenen Zeit ist Netz und Übertragung. Der Wert ist eine Auskunft, keine Zusage; wer ihn nicht braucht, ignoriert den Kopf. - Unbekannte Felder: In Auskunftsanfragen (
/v1/price,/v1/prices,/v1/search) wird ein unbekanntes Feld ignoriert und inwarningsgenannt. Innerhalb vonoccupancywird es abgelehnt (422 ERR_UNKNOWN_FIELD), weil ein Tippfehler dort den Preis ändert./v1/bookund/v1/cancellehnen jedes unbekannte Feld ab.
1.7 Die Testwelt der Beispiele
Die Beispiele laufen gegen die Testwelt des Betreibers (Golden Seed): Veranstalter
TEST-TENANT-A, Hotels TEST-HOTEL-…, Zimmer DZ/EZ, Verpflegung RO/BB/HB. Platzhalter:
- Platzhalter
{{baseUrl}}- Bedeutung
- Basis-URL
- Platzhalter
{{apiKey}}- Bedeutung
- Key ohne Kundengruppe (Basisvertrag).
- Platzhalter
{{rabattKey}}- Bedeutung
- Key der Kundengruppe
TEST-PARTNER-DISCOUNT(−20 % aufTEST-HOTEL-DISCOUNT, Preisgruppe)
- Platzhalter
{{partnerKey}}- Bedeutung
- Key der Kontingent-Gruppe
TEST-PARTNER-BASE
- Platzhalter
{{poolKey}}- Bedeutung
- Key der Kontingent-Gruppe
TEST-PARTNER-POOL, ohne Export-Recht
- Platzhalter
{{gesperrterKey}}- Bedeutung
- widerrufener Key
- Platzhalter
{{exportEpoch}},{{exportSeq}}- Bedeutung
epochundto_seqaus dem Manifest des Beispielsexport-voll
- Platzhalter
{{ohnePreisRef}},{{zweiteRef}}- Bedeutung
- Buchungsreferenzen aus den Beispielen
buchen-ohne-preispruefungbzw.buchen-gleiche-kundenreferenz
- Platzhalter
{{D0}},{{D2}}, …- Bedeutung
- Anreisetag D0 der Testwelt plus n Tage. D0 = Tag des ersten Testdaten-Aufbaus + 30 Tage. D0 ist nur ein fester Kalendertag der Testdaten, nicht der Stichtag aus 1.3 (der ist immer das Serverdatum)
- Platzhalter
{{heute}},{{gestern}}- Bedeutung
- Serverdatum (= Stichtag), Vortag
- Platzhalter
{{buchungsRef}}- Bedeutung
- Buchungsreferenz aus dem Beispiel
buchen
- Platzhalter
{{feedToken}},{{inhaltCursor}}- Bedeutung
feedTokenundnextCursoraus dem Beispielinhalt-verzeichnis
- Platzhalter
{{inhaltEtag}}- Bedeutung
ETagaus dem Beispielinhalt-hotel
- Platzhalter
{{*}}- Bedeutung
- (nur in Antworten) beliebiger Wert, z. B. Zeitstempel
Preise der Testwelt (DZ, 2 Erwachsene, Nur Übernachtung): erste Nacht 100,00, jede
weitere 80,00 je Zimmer; Frühstück +20,00, Halbpension +35,00 je Person und Nacht.
Ausnahme TEST-HOTEL-KIND (Ziel ACE): Preis je Person und Nacht (DZ 89,90 / 79,90),
Kinder von 2 bis 11 Jahren −15 %.
1.8 Gesundheit: GET /v1/health
Für Load-Balancer und Überwachung, ohne Key. 200 {"status": "ok"} heißt: Der Knoten hat
den Bestand geladen, und sein letzter Abgleich mit der Datenbank ist frisch (Standard: jünger
als 60 s). Sonst 503 mit status degraded (Abgleich zu alt, reason: "sync_stale") oder
down (Bestand nicht geladen, reason: "view_not_loaded"), dazu detail als Text. Die
Antwort enthält keine Veranstalter- oder Bestandsdaten. Mehr als 10 Aufrufe je Sekunde von
einer Absender-IP werden verzögert beantwortet (höchstens 1 s, Abschnitt 8).
Liest der Knoten über eine Lese-Replica, nennt die Antwort zusätzlich deren Verzug in
Sekunden (replica_lag_s). Über 5 s bleibt der Status ok, warnings enthält dann
"replica_lag". 503 degraded kommt, wenn Abgleich-Alter plus Verzug die Grenze
überschreiten (reason: "replica_lag") oder der Verzug nicht messbar ist
(reason: "replica_lag_unknown").
Jede Antwort (auch 503) nennt außerdem die Laufzeit des Prozesses in Sekunden
(uptime_s) und den Build-Stand (version, leer bei einem Build ohne Stand). Sobald der
Knoten die Datenbank gemessen hat, steht db da: "ok" mit db_latency_ms (Dauer einer
Rundreise, gemessen im Abgleich-Takt, nicht beim Health-Aufruf) oder "error" (letzte
Messung gescheitert). "error" allein kippt den Status nicht — der Knoten antwortet aus
seinem geladenen Bestand; bei ok steht dann "db_error" in warnings, und bleibt der
Abgleich aus, folgt nach der Grenze 503 degraded (sync_stale).
"capacity_overlap" in warnings (Status bleibt ok) heißt: Die Bestandsprüfung im
Abgleich-Takt hat Kapazitäts-Zeiträume gefunden, die sich je Zimmerart überlappen — ein
Betreiber-Befund (Bestand am Schreibweg vorbei geändert), kein Fehler der Anfrage.
### gesundheit
GET {{baseUrl}}/v1/health{
"status": "ok",
"db": "ok",
"db_latency_ms": "{{*}}",
"uptime_s": "{{*}}",
"version": "{{*}}"
}1.9 Kompatibilitätszusage für /v1
Innerhalb von /v1 ändert sich die API nur additiv:
Darf innerhalb /v1 kommen | Kommt nie innerhalb /v1 (das wäre /v2) |
|---|---|
| neue optionale Anfragefelder | neue Pflichtfelder in der Anfrage |
| neue Antwortfelder | Felder entfernen oder umbenennen |
| neue Endpunkte | Typ oder Bedeutung eines Feldes ändern |
neue ERR_*-Codes, jeweils mit dokumentiertem Umgang im Fehlerkatalog | Bedeutung eines bestehenden ERR_*-Codes ändern |
Was der Abnehmer dafür tun muss:
- Unbekannte Antwortfelder ignorieren, nicht ablehnen.
- Unbekannte
ERR_*-Codes nach dem HTTP-Status behandeln (4xx: nicht blind wiederholen,5xx/429: mit Abstand wiederholen, Abschnitt 7). messageundwarningssind Texte für Menschen und nicht Teil der Zusage.
Fest innerhalb /v1: Zeitstempel sind RFC 3339 in UTC (Z), Beträge ganze Cent (1.4),
Textlängen werden vor dem Schreiben geprüft (422 mit Feldnamen, nie 500, Anhang),
/v1/health braucht keinen Key (1.8).
1.10 Anmeldung im Abnehmer-Portal
Das Portal (Doku, eigene Zugänge und Keys) gehört nicht zur API; die API braucht nur den Key. Für die Anmeldung im Portal gilt:
- Den Portal-Zugang legt der Veranstalter an. Sie bekommen einen Benutzer und ein Einmal-Passwort (30 Tage gültig).
- Erste Anmeldung: Benutzer und Einmal-Passwort, dann den zweiten Faktor einrichten (Authenticator-App nach RFC 6238: QR-Code scannen oder Schlüssel abtippen, sechsstelligen Code eingeben), danach ein eigenes Passwort vergeben. Ohne gültiges Einmal-Passwort lässt sich der zweite Faktor nicht einrichten.
- Jede weitere Anmeldung: Passwort und aktueller Code. Ein Code gilt nur einmal.
- Falsche Codes: nach 5 endet die Anmeldung. 10 Fehlversuche innerhalb von 7 Tagen sperren den Zugang, auch für den richtigen Code. Entsperren kann nur der Veranstalter.
- Gesperrt oder Telefon verloren: beim Veranstalter melden. „Sperre aufheben“ gibt den Zugang mit dem bisherigen zweiten Faktor wieder frei. „2. Faktor neu“ vergibt ein neues Einmal-Passwort; danach richten Sie zweiten Faktor und Passwort neu ein, das alte Passwort gilt nicht mehr.
- Portal und Veranstalter-Konsole sind getrennt: Portal-Zugangsdaten gelten an der Konsole nicht.
2. Ablauf einer Anbindung
/v1/destinations
-> /v1/search
-> /v1/price | /v1/prices
-> /v1/book idemKey, priceCheck
-> /v1/booking
-> /v1/cancel idemKey/v1/destinations: gültigedestination-Codes des Keys./v1/search: buchbare Hotels mit Ab-Preis und Verfügbarkeit./v1/price: Preisauskunft für Hotel, Zimmer, Verpflegung und Belegung (verbindlich erst/v1/book);/v1/prices: alle Verpflegungen (und Zimmer) auf einmal./v1/book: bucht, mitidemKeyundpriceCheck(dem Preis von eben)./v1/booking: liest die Buchung über unsere oder die eigene Referenz./v1/cancel: storniert über denselbenidemKey.
Regeln, die man kennen muss:
- Suche und Preis sind Auskunft, verbindlich ist erst
/v1/book. Zwischen Preis und Buchung kann der Veranstalter Preise oder Kontingent ändern. MitpriceChecklehnt/v1/bookeinen geänderten Preis ab (409 ERR_PRICE_DRIFT) statt still zum neuen zu buchen. - Jede Buchung hat einen eigenen
idemKey(vom Aufrufer vergeben, z. B. die eigene Vorgangsnummer). Wiederholungen mit demselben Key buchen nie doppelt (Abschnitt 9). - Fehler nicht blind wiederholen. Welche Fehler eine Wiederholung lohnen, steht im Fehlerkatalog (Spalte „Aufrufer“).
3. Preis: /v1/price und /v1/prices (BA)
3.1 POST /v1/price – ein Preis
Bepreist genau ein Zimmer eines Hotels für einen Aufenthalt, eine Verpflegung und eine Belegung.
- Feld
hotel- Pflicht
- ja
- Bedeutung
- Hotel-Code
- Feld
room- Pflicht
- nein
- Bedeutung
- Zimmer-Code. Fehlt er, nimmt die API das günstigste verfügbare Zimmer, das die Belegung zulässt und die Verpflegung anbietet; ist keines verfügbar, das günstigste davon (dann
available=false, 3.3). Ein Zimmer, dessen Vertragsdaten der Rechenkern ablehnt (7.5, z. B.ERR_INVALID_AMOUNT,ERR_NO_SECTION), wird dabei ausgelassen und inwarningsmit Zimmer und Code genannt; rechnet kein Zimmer, kommt der Fehler als 422. Mitroomkommt der Fehler dieses Zimmers immer als 422.
- Feld
board- Pflicht
- ja
- Bedeutung
- Verpflegungs-Code, genau wie im Vertrag (
RO, nichtro). Fehlt:ERR_BOARD_MISSING; bietet das Zimmer (ohneroom: kein Zimmer) ihn nicht an:422 ERR_BOARD_NOT_OFFERED
- Feld
checkIn,checkOut- Pflicht
- ja
- Bedeutung
- Aufenthalt (Abschnitt 1.3)
- Feld
occupancy.travellers[]- Pflicht
- ja
- Bedeutung
- Reisende mit
age
- Feld
currency- Pflicht
- nein
- Bedeutung
- Wunschwährung, nur Hinweis (1.4)
- Feld
now- Pflicht
- nein
- Bedeutung
- geduldet, wird ignoriert (1.3)
Antwort:
- Feld
room- Bedeutung
- das bepreiste Zimmer (ohne
roomin der Anfrage: das günstigste verfügbare); so an/v1/bookübergeben
- Feld
currency- Bedeutung
- Vertragswährung, leer = am Vertrag nicht hinterlegt
- Feld
totalCents- Bedeutung
- Gesamtpreis des Zimmers für den Aufenthalt
- Feld
rounding- Bedeutung
- Rundungsregel der Antwort (Abschnitt 1.4)
- Feld
perTravellerCents[]- Bedeutung
- Preis je Reisendem, geordnet nach Alter absteigend (ältester zuerst, bei gleichem Alter Anfrage-Reihenfolge): die exakte Summe seiner Posten, einmal gerundet. Summe =
totalCents. Bei Zimmerpreisen (Objektpreis) trägt der erste Reisende die Zimmerposten, die Verpflegung steht beim jeweiligen Reisenden.
- Feld
breakdown[]- Bedeutung
- Einzelposten:
chargeType(BaseCharge,PhantomBaseCharge,GuestCharge,BoardCharge,Extra),code,traveller(Index inperTravellerCents),night(Index ab 0,-1= je Aufenthalt),amountExact(exakter Betrag als Dezimalzahl, z. B."-13.485"),amountCents, bei ExtrasapplianceCode, bei Extras einer Extra-Familie (mehrere Extras mit demselbenapplianceCode) zusätzlichvariant(die Variante; erst beide zusammen nennen das Extra), bei den Posten einer Freinacht zusätzlichfreeNight: true(diese Nacht ist ganz oder anteilig erlassen, z. B. „7=6“: die siebte Nacht trägt je Reisendem einen Posten, der Grundpreis und – je nach Angebot – Verpflegung aufhebt). Reihenfolge: Nacht, darin Reisender, darin Rechenschritt (Grundpreis, Personenrabatt/-zuschlag, Verpflegung samt ihrem Personenrabatt, Extras); Posten je Aufenthalt zuletzt.
- Feld
separateExtras[]- Bedeutung
- nur wenn vorhanden: einzeln ausgewiesene Extras.
inTotal=true: Pflicht-Extra, im Gesamtpreis enthalten.inTotal=false: optionales Extra, nicht im Gesamtpreis.codenennt das Extra, bei einer Extra-Familie zusammen mitvariant.amountCentsist die exakte Summe des Extras, einmal gerundet; beiinTotal=truekann sie von der Summe seinerbreakdown-Zeilen um mehrere Cent abweichen – maßgeblich istbreakdown.
- Feld
availability- Bedeutung
configured(Kontingent hinterlegt),available(jede Nacht offen),minFree(kleinste freie Anzahl über die Nächte, exakt bis 99;-1= jede Nacht mehr als 99 frei oder Freiverkauf;0= mindestens eine Nacht nicht buchbar)
- Feld
warnings[]- Bedeutung
- Hinweise (1.6); ohne
roomauch je ausgelassenem Zimmer mit Vertragsfehler:zimmer 'EZ' ausgelassen: ERR_INVALID_AMOUNT (…)
### preis-einzeln
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"currency": "EUR",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{
"room": "DZ",
"currency": "EUR",
"rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
"totalCents": 18000,
"perTravellerCents": [18000, 0],
"breakdown": [
{"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "100.00", "amountCents": 10000},
{"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "80.00", "amountCents": 8000}
],
"availability": {"configured": true, "available": true, "minFree": 5}
}Ohne room sucht die API das günstigste passende Zimmer – für eine Person hier das EZ;
room in der Antwort nennt es.
### preis-guenstigstes-zimmer
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}]}
}{
"room": "EZ",
"currency": "EUR",
"rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
"totalCents": 12500,
"perTravellerCents": [12500],
"breakdown": [
{"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "70.00", "amountCents": 7000},
{"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "55.00", "amountCents": 5500}
],
"availability": {"configured": true, "available": true, "minFree": 5}
}Ausgebuchte, gesperrte, nicht mit Kontingent hinterlegte oder – beim Key einer Kundengruppe –
nicht zugeteilte Zimmer überspringt die Wahl ohne room, auch wenn sie billiger sind: Im Hotel unten hat das EZ (70 EUR) kein Kontingent
und ist nicht buchbar, die API nimmt das teurere, buchbare DZ.
### preis-guenstigstes-verfuegbares
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-STOP",
"board": "RO",
"checkIn": "{{D10}}",
"checkOut": "{{D11}}",
"occupancy": {"travellers": [{"age": 40}]}
}{
"room": "DZ",
"currency": "EUR",
"rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
"totalCents": 10000,
"perTravellerCents": [10000],
"breakdown": [
{"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "100.00", "amountCents": 10000}
],
"availability": {"configured": true, "available": true, "minFree": 5}
}Preis je Person bei Zimmerpreisen. Viele Verträge bepreisen das Zimmer, nicht die Person
(Objektpreis). Dann stehen alle Zimmerposten – Grundpreis, Extras je Zimmer oder Vorgang –
im Topf des ersten Reisenden in perTravellerCents, die anderen tragen dafür 0 (im
Beispiel preis-einzeln: [18000, 0]). Verpflegung ist immer ein Preis je Person und Nacht,
auch beim Objektpreis: sie steht beim jeweiligen Reisenden (mit Frühstück [22000, 4000],
siehe preis-alle-verpflegungen); ein Alleinreisender zahlt den vollen Zimmerpreis, aber nur
eine Verpflegung. Ein fester Rabatt je Person (z. B. Frühbucher −20 € je Person) mindert beim
Objektpreis den Zimmerpreis und steht darum ebenfalls im ersten Topf. Das ist Absicht und bleibt so
(EDF 5.1.6: „objektbasierte Zusatzleistungen werden der 1. Person zugerechnet“; jeder Topf wird
einmal gerundet, die Summe ist immer genau totalCents). Der erste Topf gehört dem ältesten
Reisenden – perTravellerCents und breakdown[].traveller sind nach Alter absteigend
geordnet, nicht nach der Reihenfolge der Anfrage. Personenbezogene Posten (z. B.
Kinderpreise) stehen beim jeweiligen Reisenden. Für eine Anzeige „pro Person“ ist
perTravellerCents deshalb nicht gedacht: totalCents durch die Zahl der Reisenden teilen
und selbst runden.
Der Preis hängt am Key: Dieselbe Anfrage mit dem Key einer Kundengruppe liefert deren
Sonderpreis (hier −20 %). TEST-PARTNER-DISCOUNT ist eine Preisgruppe: Sie bucht aus dem
allgemeinen Bestand, availability ist dieselbe wie ohne Gruppe (1.1).
### preis-kundengruppe
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{rabattKey}}
{
"hotel": "TEST-HOTEL-DISCOUNT",
"room": "DZ",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{
"room": "DZ",
"currency": "EUR",
"rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
"totalCents": 14400,
"perTravellerCents": [14400, 0],
"breakdown": [
{"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "80.00", "amountCents": 8000},
{"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "64.00", "amountCents": 6400}
],
"availability": {"configured": true, "available": true, "minFree": 5}
}Preis je Person mit Kinderermäßigung: −15 % auf 89,90 sind −13,485 – ein Bruchteil eines
Cents. Der Posten bleibt exakt (amountExact); gerundet wird einmal die Summe des Kindes:
89,90 − 13,485 + 79,90 − 11,985 = 144,33 → perTravellerCents[2] = 14433. Die amountCents
eines Reisenden sind so verteilt, dass sie genau seinen Preis ergeben: jede Zeile ist die
gerundete laufende Summe des Reisenden bis zu dieser Zeile minus die bis zur Vorzeile
(8990, −1348, 7990, −1199 = 14433). Wer Posten einzeln rundet und addiert (−13,49, −11,99),
kommt auf 144,32 – das ist nicht der Preis.
### preis-kind-bruchcent
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-KIND",
"room": "DZ",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}, {"age": 8}]}
}{
"room": "DZ",
"currency": "EUR",
"rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
"totalCents": 48393,
"perTravellerCents": [16980, 16980, 14433],
"breakdown": [
{"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "89.90", "amountCents": 8990},
{"chargeType": "BaseCharge", "code": "Base", "traveller": 1, "night": 0, "amountExact": "89.90", "amountCents": 8990},
{"chargeType": "BaseCharge", "code": "Base", "traveller": 2, "night": 0, "amountExact": "89.90", "amountCents": 8990},
{"chargeType": "GuestCharge", "code": "Base", "traveller": 2, "night": 0, "amountExact": "-13.485", "amountCents": -1348},
{"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "79.90", "amountCents": 7990},
{"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 1, "night": 1, "amountExact": "79.90", "amountCents": 7990},
{"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 2, "night": 1, "amountExact": "79.90", "amountCents": 7990},
{"chargeType": "GuestCharge", "code": "ExtraDay", "traveller": 2, "night": 1, "amountExact": "-11.985", "amountCents": -1199}
],
"availability": {"configured": true, "available": true, "minFree": 5}
}Freinächte („7=6“). Der Posten, der eine Nacht erlässt, ist ein Extra mit dem
applianceCode des Angebots und freeNight: true; er ist im Gesamtpreis enthalten und steht in
der erlassenen Nacht je Reisendem direkt hinter dessen Grundpreis. In der Testwelt
(TEST-HOTEL-FREINACHT, DZ, Nur Übernachtung, Preis je Person: 100,00 erste Nacht, 80,00 jede
weitere, checkIn = {{D0}}) kosten 2 Erwachsene für 7 Nächte 1000,00 statt 1160,00: Nacht 6
(die siebte) trägt je Reisendem {"chargeType": "Extra", "code": "PN", "night": 6, "amountExact":
"-80.00", "applianceCode": "FSO1", "freeNight": true}. Mit Halbpension erlässt derselbe Posten
auch die Verpflegung (-115.00). 14 Nächte ergeben zwei Freinächte (Nacht 12 und 13), 6 Nächte
keine.
3.2 POST /v1/prices – alle Verpflegungen auf einmal
Wie /v1/price, aber statt board optional boards[] (fehlt es: alle Verpflegungen des
Zimmers) und room optional (fehlt es: alle Zimmer). Zimmer, die die Belegung nicht
zulassen, fallen weg; lässt keines sie zu, kommt der Grund als Fehler
(ERR_OCCUPANCY_NOT_ALLOWED). Eine angefragte Verpflegung, die kein Zimmer anbietet:
422 ERR_BOARD_NOT_OFFERED. Das Feld board filtert hier nicht: Wer es mitschickt,
bekommt trotzdem alle Verpflegungen und einen Hinweis in warnings (Beispiel unten);
gefiltert wird nur mit boards[].
Antwort: currency, rounding, rooms[] mit room und boards[] (je Verpflegung board,
globalType (Verpflegungsart wie im EDF-Export und in der offenen Suche: AO für RO, sonst die Zuordnung des Veranstalters, sonst der Code selbst, wenn er eine Verpflegungsart ist, sonst XX = nicht zuordenbar), totalCents, perTravellerCents, separateExtras,
availability), je Zimmer nach Preis aufsteigend; warnings. Kein breakdown.
Rechnet eine Verpflegung eines Zimmers für diese Belegung nicht, weil der Vertrag dort
fehlerhaft ist (ERR_NEGATIVE_TRAVELLER_PRICE, ERR_NEGATIVE_PERCENT_BASE, Abschnitt 7.5)
oder weil sie für diese Reisegruppe nicht verkauft wird (ERR_BOARD_NOT_AVAILABLE, 3.5),
fällt nur diese Kombination aus: Sie steht ohne Preis in rooms[].errors[] (board,
globalType, errorCode, message) und in warnings; der Rest der Matrix bleibt bepreist,
boards[] des Zimmers kann dann leer sein. Ebenso bei jedem anderen Vertragsfehler eines
Zimmers (Abschnitt 7.5, z. B. ERR_NO_SECTION: für eine Nacht keine Saison im Vertrag): Die
betroffenen Verpflegungen stehen mit dem Code in rooms[].errors[] und in warnings, alle
anderen Zimmer bleiben bepreist, mit denselben Preisen wie /v1/price mit room und board.
Rechnet keine einzige Kombination, kommt der Code als Fehler (422). Fehler der Anfrage selbst
lehnen sie weiterhin ganz ab.
Zeitgrenze: /v1/prices und /v1/price ohne room rechnen mehrere Zimmer bzw.
Verpflegungen; überschreitet das 2 s (im Normalbetrieb dauert es wenige Millisekunden), kommt
503 ERR_PRICE_TIMEOUT – nie eine halbe Matrix oder ein „günstigstes“ Zimmer aus einem Teil
der Zimmer. Abhilfe: room bzw. boards angeben. Die Frist gilt für /v1/prices immer, auch
mit room und einer einzigen Verpflegung; nur /v1/price mit room hat keine eigene
Frist und ist darum der sichere Ausweg. Die 503 trägt kein Retry-After: erst eingrenzen,
dann wiederholen. Schließt der Aufrufer die Verbindung,
bricht die Rechnung ab und es gibt keine Antwort.
### preis-alle-verpflegungen
POST {{baseUrl}}/v1/prices
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{
"currency": "EUR",
"rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
"rooms": [
{
"room": "DZ",
"boards": [
{"board": "RO", "globalType": "AO", "totalCents": 18000, "perTravellerCents": [18000, 0],
"availability": {"configured": true, "available": true, "minFree": 5}},
{"board": "BB", "globalType": "BB", "totalCents": 26000, "perTravellerCents": [22000, 4000],
"availability": {"configured": true, "available": true, "minFree": 5}},
{"board": "HB", "globalType": "HB", "totalCents": 32000, "perTravellerCents": [25000, 7000],
"availability": {"configured": true, "available": true, "minFree": 5}}
]
}
]
}Mit board statt boards[] kommt dieselbe Matrix wie ohne, dazu der Hinweis
board: 'HB' wird bei /v1/prices ignoriert — Verpflegungen ueber boards waehlen (fehlt boards:
alle des Zimmers):
### preis-alle-verpflegungen-board-ignoriert
POST {{baseUrl}}/v1/prices
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"board": "HB",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}3.3 Verfügbarkeit in Preis-Antworten
availability ist immer gesetzt und bezieht sich auf das bepreiste Zimmer über alle Nächte.
Ein Preis mit available=false ist eine Preisauskunft, kein Angebot: Die Buchung
scheitert (Abschnitt 5.3). Gezählt wird genau das, wogegen /v1/book verkauft: freie
Kapazität je Nacht nach Buchungen und Tagesstatus, und beim Key einer Kontingent-Gruppe
zusätzlich deren Zuteilung (was die Gruppe schon gebucht hat, ist abgezogen; ohne Zuteilung
für Zimmer und Nacht ist nichts frei). Eine Preisgruppe zählt wie ein Key ohne Gruppe (1.1). minFree ist das Kleinste davon über die Nächte.
Je Nacht zählt TourAPI exakt bis 99. Welche Nacht was zu minFree beiträgt:
| Nacht | Beitrag zu minFree | Allotment-Datei (Abschnitt 10) |
|---|---|---|
| 1 bis 99 Einheiten frei | die Anzahl | 01–99 |
| mehr als 99 frei oder Freiverkauf | senkt minFree nicht | ** |
| Stop-Sale | 0 | SS |
| auf Anfrage | 0 | RR |
| ausgebucht, geschlossen, ohne Kapazität, Kundengruppe ohne Zuteilung für Zimmer und Nacht | 0 | 00 |
minFree ist -1 nur, wenn jede Nacht mehr als 99 frei hat oder im Freiverkauf ist; eine
einzige Nacht mit Stop-Sale, Anfrage oder 0 macht minFree zu 0 (available=false). Das
ist dieselbe Darstellung wie im EDF-Allotment der Cache-Lieferung, damit Cache und API
dieselbe Zahl zeigen.
configured=true: TourAPI kennt für dieses Zimmer ein Kontingent. Das sagt noch nichts über jede einzelne Nacht: Eine Nacht, für die keine Kapazität eingetragen ist, macht das Zimmeravailable=false(minFree0), es bleibt aberconfigured=true.configured=false: für dieses Zimmer ist gar kein Kontingent hinterlegt; buchen geht nicht.
Fehlt nur einzelnen Nächten die Kapazität, nennen Suche und /v1/book denselben Grund:
ERR_NO_INVENTORY (Suche 4.2, Buchung 5.3). Maßgeblich für „buchbar“ ist available, nicht
configured.
availability ist ein Stand, keine Zusage; verbindlich prüft erst /v1/book. Eine Buchung
oder ein Storno zeigt der Server-Knoten, der sie ausgeführt hat, in der nächsten Anfrage
(minFree sinkt bzw. steigt). Andere Knoten, Buchungen anderer Kanäle und Vertragsänderungen
ziehen nach wenigen Sekunden nach – ebenso der eigene Knoten in dem seltenen Fall, dass er
den neuen Stand nicht sofort nachladen konnte (die Buchung selbst gilt dann trotzdem).
3.4 Verkaufsregeln des Zimmers: Mindestaufenthalt, Anreisetage, Verkaufsfenster, Release
Ein Hotelvertrag kann festlegen, welche Aufenthalte das Hotel annimmt, zum Beispiel „Hochsaison mindestens 7 Nächte, Anreise nur samstags“, „Halbpension nur bei Anreise bis 31.10.“ oder „Release 14 Tage“ (Buchung spätestens 15 Tage vor Anreise). Solche Regeln ändern keinen Preis; sie entscheiden, ob ein Zimmer für den angefragten Aufenthalt angeboten wird. Ein Aufenthalt, den eine Regel ausschließt, bekommt keinen Preis:
| Code | Die Regel verlangt … | Aufrufer |
|---|---|---|
ERR_STAY_LENGTH_NOT_ALLOWED | eine andere Aufenthaltsdauer (Mindest- oder Höchstnächte) | Dauer ändern |
ERR_ARRIVAL_DAY_NOT_ALLOWED | einen anderen An- oder Abreise-Wochentag | Reisetage verschieben |
ERR_TRAVEL_DATES_NOT_ALLOWED | Reisedaten in einem bestimmten Zeitraum (Verkaufsfenster) | anderer Zeitraum |
ERR_BOARD_NOT_ALLOWED | eine andere Verpflegung für diesen Aufenthalt | andere Verpflegung |
ERR_LEAD_TIME_NOT_ALLOWED | mehr Vorlauf: die Anreise liegt innerhalb der Release-Frist | spätere Anreise |
message nennt Zimmer, Regel und das Verlangte, etwa „Zimmer DZ, Verkaufsregel 1: wenn
Anreise 2026-07-01 bis 2026-08-31, verlangt mindestens 7 Nächte“. Verletzt ein Aufenthalt
mehrere Teile einer Regel, gilt diese Reihenfolge: Dauer, Wochentag, Zeitraum, Verpflegung,
Release.
Release (Vorlauffrist). Eine Regel mit Release n Tagen verlangt mehr als n
Kalendertage zwischen Stichtag (Serverdatum, 1.3) und Anreise: bei Release 3 und Stichtag
10.05. ist die Anreise am 13.05. noch gesperrt, ab dem 14.05. buchbar; bei Release 0 ist nur
die Anreise am Stichtag selbst gesperrt. Die Frist kann an einem Zeitraum hängen (etwa
„ab September 14 Tage“): dann zählt sie, sobald eine Nacht des Aufenthalts in diesem Zeitraum
liegt, gemessen bis zur Anreise. Bezieht sich die Regel auf die Abreise (ApplyTo
Departure), zählt die Frist bis zur Abreise: verlangt sind dann mehr als n Kalendertage
zwischen Stichtag und Abreise. message nennt Stichtag, Anreise und den Vorlauf. Ein
heute gesperrter Aufenthalt kann es morgen nicht mehr werden; eine Wiederholung lohnt nicht.
/v1/price: ohneroomfällt ein Zimmer, dessen Regel den Aufenthalt ausschließt, aus der Auswahl; nimmt kein Zimmer ihn an, kommt der Grund als 422. Er hat Vorrang vorERR_OCCUPANCY_NOT_ALLOWEDeines anderen Zimmers, weil er genauer sagt, was zu ändern ist./v1/prices: Regeln können je Verpflegung gelten; es fällt nur die betroffene Verpflegung weg. Bleibt nichts übrig, kommt der Grund als 422./v1/search: das Hotel fällt weg, der Code steht indiagnostics.reasons./v1/book: mitpriceCheckgilt die Regel für die angefragte Verpflegung; ohnepriceCheck(Verpflegung unbekannt) wird verkauft, sobald eine Verpflegung des Zimmers den Aufenthalt annimmt.
ERR_ROOM_RESTRICTION_INVALID (422) heißt: eine Regel im Vertrag ist nicht auswertbar. Das
ist ein Vertragsfehler – melden.
3.5 Verpflegung nur für bestimmte Reisegruppen
Ein Verpflegungszuschlag kann an die Zusammensetzung der Reisegruppe gebunden sein, etwa
„Halbpension-Zuschlag gilt für Gruppen mit mindestens 2 Erwachsenen“. Passt die angefragte
Belegung nicht dazu, ist diese Verpflegung für diese Belegung nicht buchbar:
422 ERR_BOARD_NOT_AVAILABLE. TourAPI rechnet nie einen Preis für eine Verpflegung ohne die
Verpflegung. message nennt Verpflegung, Reisenden, Nacht und die verlangte Gruppe. Das ist
kein Vertragsfehler; eine andere Verpflegung oder Belegung kann rechnen (Aufrufer: andere
Verpflegung bzw. Belegung).
/v1/price: ohneroomfällt das Zimmer für diese Verpflegung aus der Auswahl (wie bei einer Verkaufsregel, 3.4); rechnet kein Zimmer, kommt der Grund als 422./v1/prices: nur diese Verpflegung des Zimmers fällt aus, als Eintrag inrooms[].errors[]miterrorCode(3.2); der Rest bleibt bepreist./v1/search: das Hotel fällt weg, der Code steht indiagnostics.reasons./v1/bookmitpriceCheck: 422, nichts wird gebucht.
4. Suche: /v1/search (BA)
4.1 Anfrage und Antwort
Findet die buchbaren Hotels des Veranstalters für einen Aufenthalt, eine Verpflegung und
eine Belegung, je Hotel mit dem günstigsten verfügbaren Zimmer (wie /v1/price ohne
room): Ein ausgebuchtes billigeres Zimmer lässt das Hotel nicht aus der Liste fallen,
solange ein anderes Zimmer buchbar ist.
| Feld | Pflicht | Bedeutung |
|---|---|---|
destination | nein | Zielgebiet- oder Flughafen-Code des Hotels (unten); leer = alle Hotels |
board, checkIn, checkOut, occupancy, currency, now | wie /v1/price | |
includeUnavailable | nein | true: nicht buchbare Hotels kommen gekennzeichnet mit (Standard false) |
pageSize, cursor | nein | Seiten, Abschnitt 4.3 |
Antwort:
- Feld
results[]- Bedeutung
- Treffer der Seite, nach
fromTotalCentsaufsteigend, bei Gleichstand nach Hotel-Code
- Feld
results[].hotel,name,room- Bedeutung
- Hotel und das Zimmer, auf das sich der Preis bezieht
- Feld
results[].fromTotalCents,currency- Bedeutung
- Ab-Preis (günstigstes verfügbares Zimmer; bei
bookable=falsedas günstigste überhaupt) in Vertragswährung des Hotels
- Feld
results[].availability- Bedeutung
- wie bei
/v1/price
- Feld
results[].bookable- Bedeutung
true= alle Nächte verfügbar
- Feld
results[].reason,priceInformational- Bedeutung
- nur bei
bookable=false(nur mitincludeUnavailable): Grund und Kennzeichen „nur Preisauskunft“
- Feld
diagnostics- Bedeutung
- je Seite:
considered= geprüfte Hotels =returned+skipped;reasons[]= warum Hotels wegfielen, je Grund mit Anzahl;timeBudgetExhausted= Seite wegen Zeitbudget gekappt (4.3);roomErrors[]= Zimmer der geprüften Hotels, die wegen eines Vertragsfehlers (7.5) ausgelassen wurden, je Code mit Anzahl – auch wenn das Hotel mit einem anderen Zimmer trifft
- Feld
nextCursor- Bedeutung
- gesetzt, solange weitere Hotels zu prüfen sind (4.3)
- Feld
warnings[]- Bedeutung
- Hinweise; bei gemischten Vertragswährungen ein Sammelhinweis
Die Suche nennt keine Hotel-Codes der weggefallenen Hotels, nur Gründe und Anzahlen. Leere
Trefferliste heißt also nicht „kein Bestand“: diagnostics.reasons sagt, ob z. B. die Saison
nicht passt (ERR_NO_SECTION), alles ausgebucht ist (ERR_NOT_AVAILABLE), für eine Nacht
kein Kontingent hinterlegt ist (ERR_NO_INVENTORY) oder kein Zimmer
des Hotels die angefragte Verpflegung anbietet (ERR_BOARD_NOT_OFFERED).
Woher die Codes für destination kommen: Jedes Hotel hat im Vertrag des Veranstalters
einen Zielgebiet-Code (z. B. PMI) und optional Flughafen-Codes; ein Hotel passt, wenn
destination genau einem davon gleicht (Groß-/Kleinschreibung zählt). Zielgebiet-Codes
bestehen nur aus A-Z a-z 0-9 . _ - (1 bis 64 Zeichen, kein Freitext); Flughafen-Codes sind
IATA-Codes aus drei Großbuchstaben. Die gültigen Codes
des Keys liefert GET /v1/destinations (4.5). Ein Code, zu dem es für diesen Key kein Hotel
gibt, ist ein Fehler: 422 ERR_UNKNOWN_DESTINATION (Beispiel suche-unbekanntes-ziel), nie
eine still leere Liste. Geprüft wird die erste Seite (ohne cursor); verschwindet das Ziel
beim Blättern, endet die Suche mit einer leeren Schlussseite (4.3).
### suche-ziel
POST {{baseUrl}}/v1/search
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"destination": "PMI",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{
"results": [
{"hotel": "TEST-HOTEL-BASE", "name": "TEST Basis Palma", "room": "DZ", "currency": "EUR",
"fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
"bookable": true},
{"hotel": "TEST-HOTEL-DISCOUNT", "name": "TEST Rabatt Palma", "room": "DZ", "currency": "EUR",
"fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
"bookable": true},
{"hotel": "TEST-HOTEL-TWIN", "name": "TEST Zwilling A Palma", "room": "DZ", "currency": "EUR",
"fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
"bookable": true}
],
"diagnostics": {"considered": 3, "returned": 3, "skipped": 0}
}Derselbe Code klein geschrieben ist für die API ein anderes, unbekanntes Ziel:
### suche-unbekanntes-ziel
POST {{baseUrl}}/v1/search
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"destination": "pmi",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{"errorCode": "ERR_UNKNOWN_DESTINATION",
"message": "destination: kein Hotel mit diesem Ziel-Code fuer diesen Key (gueltige Codes: GET /v1/destinations; Gross-/Kleinschreibung zaehlt)"}4.2 Nicht buchbare Hotels
Standardmäßig liefert die Suche nur buchbare Treffer und zählt den Rest in diagnostics.
Mit includeUnavailable=true kommen nicht buchbare Hotels mit bookable=false, reason
und priceInformational=true. Die Gründe bedeuten dasselbe wie bei /v1/book:
ERR_NO_INVENTORY: für mindestens eine Nacht ist gar kein Kontingent hinterlegt – für das Zimmer überhaupt keins (configured=false) oder nur für diese Nacht nicht./v1/booklehnt denselben Aufenthalt mitERR_NO_INVENTORYab.ERR_NOT_AVAILABLE: jede Nacht hat Kontingent, aber nicht jede ist offen (ausgebucht, Stop-Sale, geschlossen, auf Anfrage, Zuteilung der Kundengruppe erschöpft oder nicht vorhanden). Welcher Fall genau, nennt/v1/book(ERR_SOLD_OUT,ERR_STOP_SALE,ERR_INVENTORY_CLOSED,ERR_GROUP_LIMIT, Abschnitt 5.3).
Im Beispiel hat TEST-HOTEL-SOLD für die Nacht keine Kapazität, TEST-HOTEL-STOP steht
auf Stop-Sale.
### suche-nicht-buchbare
POST {{baseUrl}}/v1/search
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"destination": "IBZ",
"includeUnavailable": true,
"board": "RO",
"checkIn": "{{D11}}",
"checkOut": "{{D12}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{
"results": [
{"hotel": "TEST-HOTEL-SOLD", "name": "TEST Ausgebucht Ibiza", "room": "DZ", "currency": "EUR",
"fromTotalCents": 10000, "availability": {"configured": true, "available": false, "minFree": 0},
"bookable": false, "reason": "ERR_NO_INVENTORY", "priceInformational": true},
{"hotel": "TEST-HOTEL-STOP", "name": "TEST Stop-Sale Ibiza", "room": "DZ", "currency": "EUR",
"fromTotalCents": 10000, "availability": {"configured": true, "available": false, "minFree": 0},
"bookable": false, "reason": "ERR_NOT_AVAILABLE", "priceInformational": true}
],
"diagnostics": {"considered": 2, "returned": 2, "skipped": 0}
}4.3 Seiten und Cursor
- Eine Anfrage prüft eine Seite von höchstens
pageSizeHotels (Standard 50, höchstens 100; außerhalb 1–100:422 ERR_BAD_PAGE_SIZE) in Hotel-Code-Reihenfolge. - Gibt es weitere Hotels, steht
nextCursorin der Antwort. Die nächste Seite: dieselbe Anfrage plus"cursor": "<nextCursor>". Auf der letzten Seite fehltnextCursor. - Die Preis-Sortierung gilt je Seite. Wer eine Gesamtliste nach Preis braucht, blättert
durch und sortiert selbst.
diagnosticsgilt je Seite. - Der Cursor ist undurchsichtig und an Anfrage und Key gebunden. Andere Anfrage (Ziel,
Zeitraum, Belegung, Verpflegung,
includeUnavailable) oder anderer Key:422 ERR_CURSOR_MISMATCH. Unlesbarer oder abgelaufener Cursor (z. B. nach einem Neustart des Servers):422 ERR_BAD_CURSOR– dann ohnecursorneu beginnen.pageSizeundcurrencydürfen sich zwischen Seiten ändern. Die Länge des Cursors ist fest und sagt nichts über das Hotel. - Gekappte Seite: Unter Last beginnt eine Seite nach 60 ms kein weiteres Hotel. Sie ist
dann kürzer als
pageSize,diagnostics.timeBudgetExhausted=true, undnextCursorführt weiter. Das ist kein Fehler: einfach weiterblättern. - Schickt man weder
pageSizenochcursorund gibt es mehr Hotels, steht zusätzlich ein Hinweis inwarnings– die Liste ist dann nur die erste Seite.
### suche-seitenweise
POST {{baseUrl}}/v1/search
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"pageSize": 3,
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{
"results": [
{"hotel": "TEST-HOTEL-ALTAKTION", "name": "TEST Altaktion Teneriffa", "room": "DZ", "currency": "EUR",
"fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
"bookable": true},
{"hotel": "TEST-HOTEL-BASE", "name": "TEST Basis Palma", "room": "DZ", "currency": "EUR",
"fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
"bookable": true}
],
"diagnostics": {"considered": 3, "returned": 2, "skipped": 1,
"reasons": [{"reason": "ERR_NO_INVENTORY", "count": 1}]},
"nextCursor": "{{*}}"
}4.4 Last und Zeitgrenzen der Suche
Die Suche ist die teuerste Anfrage. Je Veranstalter laufen bis zu 8 Suchen gleichzeitig; ihre
Rechenzeit teilen sie sich (je Veranstalter ein Viertel der Kerne des Servers), damit ein
lauter Veranstalter die anderen nicht ausbremst. Ist das Kontingent voll, kommt sofort
429 ERR_SEARCH_BUSY mit Retry-After: 1. Eine Suche, die länger als 10 s rechnet, bricht
mit 503 ERR_SEARCH_TIMEOUT ab – nie mit einer still gekürzten Liste (eine unter Last
gekappte Seite hat dagegen immer nextCursor, 4.3). Abhilfe: destination setzen oder
kleinere pageSize. Schließt der Aufrufer die Verbindung, bricht die Suche ihre Rechnung
ab und antwortet nicht mehr.
4.5 GET /v1/destinations – gültige Ziel-Codes
Liefert alle Codes, auf die destination in der Suche dieses Keys filtern kann: Zielgebiet-
und Flughafen-Codes der Hotels des Veranstalters, aufsteigend sortiert, je Code einmal. Nur
die eigenen – ein Key sieht nie Ziele eines anderen Veranstalters, der Key einer
Kontingent-Gruppe nur die Ziele ihrer Hotels mit Zuteilung (1.1; ohne Zuteilung ist die Liste
leer). Ohne Parameter; die Liste ändert sich, wenn der Veranstalter Hotels anlegt oder
entfernt – beim Key einer Kontingent-Gruppe auch, wenn der Veranstalter ihre Zuteilungen
ändert, und bei jedem Key einer Kundengruppe, wenn der Veranstalter deren Art umstellt.
| Feld | Bedeutung |
|---|---|
destinations[].code | Code, genau so in destination zu übergeben |
destinations[].name | Klartext zur Anzeige; fehlt, wenn TourAPI den Code nicht kennt |
warnings[] | Hinweise, z. B. zu mitgeschickten Parametern (werden ignoriert) |
### ziele
GET {{baseUrl}}/v1/destinations
X-Api-Key: {{apiKey}}{
"destinations": [
{"code": "ACE", "name": "Lanzarote"},
{"code": "AGP", "name": "Malaga / Costa del Sol"},
{"code": "ALC", "name": "Alicante / Costa Blanca"},
{"code": "BCN", "name": "Barcelona"},
{"code": "FAO", "name": "Faro / Algarve"},
{"code": "FUE", "name": "Fuerteventura"},
{"code": "IBZ", "name": "Ibiza"},
{"code": "LPA", "name": "Gran Canaria"},
{"code": "MAH", "name": "Menorca"},
{"code": "PMI", "name": "Palma de Mallorca"},
{"code": "RHO", "name": "Rhodos"},
{"code": "TFS", "name": "Teneriffa Sued"}
]
}4a. Offene Suche: POST /v1/search/open (BA)
4a.1 Wozu
„Wo ist es zwischen dem 1. und 30. Oktober für 5 bis 7 Nächte am billigsten?“ – ohne Hotel-Code und ohne festen Termin. Die offene Suche liefert je Hotel das beste Angebot über alle Anreisetage des Fensters, alle Dauern der Spanne, alle Zimmer und Verpflegungen, global sortiert über alle Seiten. Zwei Zusagen:
- Der Preis ist exakt:
best.totalCentsundbest.perTravellerCentssind bit-gleich zu/v1/pricemit demselben Hotel, Zimmer, Verpflegung, An- und Abreise und derselben Belegung. Keine Ab-Preise aus einem Vorrat, keine Stichprobe. - Die Reihenfolge ist bewiesen: kein Hotel, das nicht gezeigt wurde, hat ein besseres bestes Angebot als der letzte Treffer. Unter Last wird nur die Seite kürzer (4a.4), nie der Preis ungenauer oder die Reihenfolge geraten.
Ablauf einer Webseite: offene Suche (Liste) → „Termine & Preise“ eines Hotels mit der
Termin-Matrix (4b, dasselbe Fenster) → gewähltes Angebot mit /v1/price im Detail
(hotel, room, board, checkIn, checkOut aus best bzw. der Zelle, dieselbe Belegung:
derselbe Preis samt Aufbau; andere Verpflegungen dieses Termins je Verpflegung mit
/v1/price ohne room) → /v1/book mit priceCheck.expectedCents = angezeigter Preis.
Ein Angebots-Token gibt es nicht: die Buchung prüft den Preis ohnehin.
Recht: Die offene Suche ist je Key freigeschaltet (bei neuen Keys aus). Der Veranstalter-
Admin legt im Key das Suchprofil fest (Fenster, Dauern, Ziele, Seitengröße, Zeitbudget,
Takt); es gilt höchstens, was der Betreiber für den Veranstalter freigibt. Ohne Recht: 403
ERR_OPEN_SEARCH_NOT_ALLOWED. Die Grenzen des Profils prüft die API am Feld und nennt sie in
der Fehlermeldung (4a.5); maschinenlesbar liefert sie GET /v1/limits (4c).
4a.2 Anfrage
- Feld
destinations- Pflicht
- nein
- Bedeutung
- Ziel-Codes wie
GET /v1/destinations(4.5), Vereinigung; höchstens so viele, wie das Profil erlaubt
- Feld
hotels- Pflicht
- nein
- Bedeutung
- Hotel-Codes des Keys; nicht zusammen mit
destinations. Ohne beides: ganzer Bestand des Keys – nur wenn das Profil alle Ziele erlaubt
- Feld
arrivalFrom,arrivalTo- Pflicht
- ja
- Bedeutung
- Anreisefenster, beide Tage eingeschlossen;
arrivalFrom≥ Stichtag,arrivalTo≤ Stichtag + 732
- Feld
nightsMin,nightsMax- Pflicht
- ja
- Bedeutung
- Dauer von–bis (1–30 und im Profil); jede Dauer dazwischen zählt
- Feld
occupancy- Pflicht
- ja
- Bedeutung
- ein Zimmer, wie
/v1/price
- Feld
boards- Pflicht
- nein
- Bedeutung
- Verpflegungs-Codes genau wie im Vertrag; ohne = alle
- Feld
boardTypes- Pflicht
- nein
- Bedeutung
- Verpflegungsart wie im EDF-Export (
AO,BB,HB,HB+,FB,FB+,SC,AI,AI+,XX); mitboardszusammen: beide müssen passen
- Feld
minTotalCents,maxTotalCents- Pflicht
- nein
- Bedeutung
- Filter auf den Gesamtpreis, Grenzen eingeschlossen
- Feld
currency- Pflicht
- bedingt
- Bedeutung
- Filter auf die Vertragswährung, keine Umrechnung. Pflicht, wenn die Hotels der Suche in mehreren Währungen rechnen (
422 ERR_CURRENCY_REQUIRED)
- Feld
category- Pflicht
- nein
- Bedeutung
- offizielle Kategorie (Landeskategorie aus dem Hotelstamm):
schemestarsoderkeys(Pflicht),min/maxStufe 1 bis 5 in Halbschritten als Zahl (z. B.3.5, gleichwertig3.50), Grenzen eingeschlossen (ohne: 1 bzw. 5). „4 Superior“ zählt als 4
- Feld
regions- Pflicht
- nein
- Bedeutung
- Regionen aus dem Hotelstamm (Adresse), eine davon; genau wie gepflegt verglichen (Groß-/Kleinschreibung zählt); höchstens 50
- Feld
geo- Pflicht
- nein
- Bedeutung
- Umkreis:
lat,lon(Dezimalgrad als Zahl, höchstens 6 Nachkommastellen) undradiusKm(0,001 bis 500, höchstens 3 Nachkommastellen). Abstand auf der Kugel (Haversine, Erdradius 6371 km), auf ganze Meter gerundet; der Rand zählt dazu. Über die Datumsgrenze und an den Polen richtig
- Feld
sort- Pflicht
- nein
- Bedeutung
price(Standard: Gesamtpreis),pricePerNight(Preis je Nacht, exakt als Bruch verglichen, ohne Rundung),hotel(Hotel-Code)
- Feld
pageSize- Pflicht
- nein
- Bedeutung
- Treffer je Seite, 1 bis Profil; ohne Angabe 20 (oder weniger, wenn das Profil weniger erlaubt)
- Feld
cursor- Pflicht
- nein
- Bedeutung
nextCursorder Vorseite, unverändert (4a.4)
Anders als die übrigen Auskunfts-Endpunkte lehnt die offene Suche jedes unbekannte Feld
ab (422 ERR_UNKNOWN_FIELD) – ein Tippfehler in einem Filter würde sonst die Trefferliste
still verändern. Ein now gibt es nicht; es gilt der Stichtag des Servers (1.3).
Filter nach Hotelstamm (category, regions, geo; zusammen gilt UND) wirken vor dem
Rechnen: ein Hotel außerhalb gehört nicht zum Suchraum – es zählt weder in hotelsInScope
noch gegen die Hotels je Anfrage des Suchprofils (ERR_SEARCH_TOO_BROAD), so geht auch ein
Umkreis über den ganzen Bestand. Fehlt einem Hotel der Wert, den ein Filter braucht (keine
offizielle Kategorie, keine Region, keine Koordinaten), und schließt es kein vorhandener Wert
aus, zählt es im Suchraum mit dem Grund ERR_NO_CATEGORY, ERR_NO_REGION bzw. ERR_NO_GEO
(erster fehlender Wert in dieser Reihenfolge) – Lücken im Hotelstamm bleiben sichtbar. Die
Werte pflegt der Veranstalter im Hotelstamm (Konsole: Hotel → Inhalt → Stamm & Lage); eine
Änderung wirkt nach dem nächsten Abgleich (Sekunden) und wechselt stand (4a.4).
4a.3 Antwort
- Feld
results[].hotel- Bedeutung
- Hotel-Code; Reihenfolge nach dem Kriterium von
sort, bei Gleichstand nach Hotel-Code
- Feld
results[].best- Bedeutung
- bestes Angebot:
checkIn,checkOut,nights,room,board,boardType(Verpflegungsart wie im EDF-Export),currency,totalCents,perTravellerCents,availability(wie/v1/price, immeravailable: true)
- Feld
results[].alternatives- Bedeutung
- Vorprüfung ohne Preis:
dates= Termine (Anreise × Dauer) mit mindestens einem Angebot laut Verfügbarkeit, Verkaufsregeln, Saison und Belegung,boards/boardTypes/roomsentsprechend. Eine Obergrenze: die Preisrechnung kann einzelne davon noch ausschließen. Nie als Preis- oder Trefferaussage lesen
- Feld
coverage.complete- Bedeutung
true: Seite voll oder Liste zu Ende.false: das Zeitbudget hat die Seite gekürzt (4a.4)
- Feld
coverage.timeBudgetExhausted- Bedeutung
- Seite wegen des Zeitbudgets gekürzt (=
complete: false)
- Feld
coverage.standChanged- Bedeutung
- die Daten haben sich seit der Vorseite geändert (4a.4)
- Feld
coverage.hotelsInScope- Bedeutung
- Hotels im Suchraum (Ziele bzw.
hotels, Bestand des Keys, Filter nach Hotelstamm; Hotels ohne den gefilterten Wert zählen mit, 4a.2)
- Feld
coverage.hotelsFeasible- Bedeutung
- davon mit mindestens einem Termin laut Vorprüfung – Obergrenze der Trefferzahl („bis zu N Hotels“), keine Trefferzahl
- Feld
coverage.hotelsPriced- Bedeutung
- auf dieser Seite exakt gerechnete Hotels (hängt auch davon ab, wie viele Hotels der Server parallel rechnet; in den Beispielen daher offen)
- Feld
coverage.undecided- Bedeutung
- Hotels, deren Platz beim Ende des Zeitbudgets noch offen war (0 bei
complete)
- Feld
coverage.reasons[]- Bedeutung
- Hotels ohne einen einzigen Termin, je Grund – für den ganzen Suchraum, auf jeder Seite gleich. Bietet kein Zimmer eine passende Verpflegung an:
ERR_BOARD_NOT_OFFERED. Wie bei/v1/searchdann die Verfügbarkeit: hat kein Zimmer auch nur einen freien Termin, ist sie der Grund (ERR_NO_INVENTORY,ERR_NOT_AVAILABLE). Sonst gilt, was/v1/pricefür den ersten freien Termin (Zimmer, Verpflegung, Anreise, Dauer in dieser Reihenfolge) antwortet, z. B.ERR_OCCUPANCY_NOT_ALLOWED,ERR_NO_SECTION,ERR_STAY_LENGTH_NOT_ALLOWED, oderERR_OUTSIDE_PRICE_FILTER, wenn sein Preis außerhalb des Filters liegt; dazuERR_CURRENCY_NOT_AVAILABLE(Hotel rechnet in anderer oder unbekannter Währung),ERR_HOTEL_NOT_FOUND(kein Angebot der Kundengruppe) undERR_NO_CATEGORY,ERR_NO_REGION,ERR_NO_GEO(dem Hotel fehlt der Wert eines Filters nach Hotelstamm, 4a.2)
- Feld
coverage.priceReasons[]- Bedeutung
- Hotels, die erst beim exakten Rechnen auf dieser Seite ausschieden, je Grund (z. B.
ERR_OUTSIDE_PRICE_FILTER)
- Feld
coverage.roomErrors[]- Bedeutung
- Zimmer mit Vertragsfehler (7.5), je Code – nie still ausgelassen
- Feld
stand- Bedeutung
- Kennung des Datenstands dieser Seite (undurchsichtig)
- Feld
nextCursor- Bedeutung
- gesetzt, solange weitere Treffer folgen können
- Feld
warnings[]- Bedeutung
- Hinweise, z. B. zum Standwechsel
Bestes Angebot eines Hotels: das Minimum des Kriteriums (Gesamtpreis bzw. Preis je
Nacht) über alle Termine × Zimmer × Verpflegungen des Filters, nur buchbare Angebote. Bei
Gleichstand gewinnt die frühere Anreise, dann die kürzere Dauer, dann Zimmer und Verpflegung
in der Reihenfolge des Vertrags. Bei sort=hotel ist das Kriterium der Gesamtpreis.
### suche-offen
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"destinations": ["PMI"],
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D6}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"pageSize": 2
}{
"results": [
{"hotel": "TEST-HOTEL-BASE",
"best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
"boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
"availability": {"configured": true, "available": true, "minFree": 5}},
"alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}},
{"hotel": "TEST-HOTEL-DISCOUNT",
"best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
"boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
"availability": {"configured": true, "available": true, "minFree": 5}},
"alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}}
],
"coverage": {"complete": true, "timeBudgetExhausted": false, "standChanged": false,
"hotelsInScope": 3, "hotelsFeasible": 3, "hotelsPriced": "{{*}}", "undecided": 0,
"reasons": [], "priceReasons": [], "roomErrors": []},
"stand": "{{*}}",
"nextCursor": "{{*}}"
}Die nächste Seite ist dieselbe Anfrage mit cursor:
### suche-offen-weiter
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"destinations": ["PMI"],
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D6}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"pageSize": 2,
"cursor": "{{offenCursor}}"
}{
"results": [
{"hotel": "TEST-HOTEL-TWIN",
"best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
"boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
"availability": {"configured": true, "available": true, "minFree": 5}},
"alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}}
],
"coverage": {"complete": true, "timeBudgetExhausted": false, "standChanged": false,
"hotelsInScope": 3, "hotelsFeasible": 3, "hotelsPriced": "{{*}}", "undecided": 0,
"reasons": [], "priceReasons": [], "roomErrors": []},
"stand": "{{*}}"
}Über den ganzen Bestand, nach Preis je Nacht, nur Frühstück oder Halbpension. Hotels ohne
Kontingent im Fenster stehen als Grund in coverage.reasons:
### suche-offen-je-nacht
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D13}}",
"nightsMin": 3,
"nightsMax": 5,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"boardTypes": ["BB", "HB"],
"sort": "pricePerNight",
"pageSize": 3
}{
"results": [
{"hotel": "TEST-HOTEL-ALTAKTION",
"best": {"checkIn": "{{D0}}", "checkOut": "{{D5}}", "nights": 5, "room": "DZ", "board": "BB",
"boardType": "BB", "currency": "EUR", "totalCents": 62000, "perTravellerCents": [52000, 10000],
"availability": {"configured": true, "available": true, "minFree": 5}},
"alternatives": {"dates": 42, "boards": ["BB", "HB"], "boardTypes": ["BB", "HB"], "rooms": 1}},
{"hotel": "TEST-HOTEL-BASE",
"best": {"checkIn": "{{D0}}", "checkOut": "{{D5}}", "nights": 5, "room": "DZ", "board": "BB",
"boardType": "BB", "currency": "EUR", "totalCents": 62000, "perTravellerCents": [52000, 10000],
"availability": {"configured": true, "available": true, "minFree": 4}},
"alternatives": {"dates": 42, "boards": ["BB", "HB"], "boardTypes": ["BB", "HB"], "rooms": 1}},
{"hotel": "TEST-HOTEL-DISCOUNT",
"best": {"checkIn": "{{D0}}", "checkOut": "{{D5}}", "nights": 5, "room": "DZ", "board": "BB",
"boardType": "BB", "currency": "EUR", "totalCents": 62000, "perTravellerCents": [52000, 10000],
"availability": {"configured": true, "available": true, "minFree": 5}},
"alternatives": {"dates": 42, "boards": ["BB", "HB"], "boardTypes": ["BB", "HB"], "rooms": 1}}
],
"coverage": {"complete": true, "timeBudgetExhausted": false, "standChanged": false,
"hotelsInScope": 19, "hotelsFeasible": 13, "hotelsPriced": "{{*}}", "undecided": 0,
"reasons": [{"reason": "ERR_NO_INVENTORY", "count": 6}], "priceReasons": [], "roomErrors": []},
"stand": "{{*}}",
"nextCursor": "{{*}}"
}Mit Filtern nach Hotelstamm über Palma und Menorca: mindestens 3,5 Sterne, Region Mallorca, 25 km um Palma. Das 3-Sterne-Hotel in Alcúdia fällt heraus, die zwei Hotels auf Menorca ohne gepflegten Stamm stehen als Grund da:
### suche-offen-stamm
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"destinations": ["PMI", "MAH"],
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D6}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"category": {"scheme": "stars", "min": 3.5},
"regions": ["Mallorca"],
"geo": {"lat": 39.57, "lon": 2.65, "radiusKm": 25}
}{
"results": [
{"hotel": "TEST-HOTEL-BASE",
"best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
"boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
"availability": {"configured": true, "available": true, "minFree": 5}},
"alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}},
{"hotel": "TEST-HOTEL-DISCOUNT",
"best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
"boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
"availability": {"configured": true, "available": true, "minFree": 5}},
"alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}}
],
"coverage": {"complete": true, "timeBudgetExhausted": false, "standChanged": false,
"hotelsInScope": 4, "hotelsFeasible": 2, "hotelsPriced": "{{*}}", "undecided": 0,
"reasons": [{"reason": "ERR_NO_CATEGORY", "count": 2}], "priceReasons": [], "roomErrors": []},
"stand": "{{*}}"
}4a.4 Seiten, Cursor, Stand, Zeitbudget
- Seiten:
nextCursorführt zur nächsten Seite: dieselbe Anfrage plus"cursor": "<nextCursor>";pageSizedarf sich ändern. Die Folgeseite setzt hinter dem letzten Treffer fort (Kriterium, dann Hotel-Code) – alle Seiten zusammen sind eine sortierte Liste. Auf der letzten Seite fehltnextCursor; eine Seite kann dann auch leer sein. Beisort=priceundpricePerNightrechnet eine Folgeseite die Hotels der Vorseiten nicht noch einmal: auch tief im Blättern kostet eine Seite etwa so viel wie die erste. - Cursor: undurchsichtig, an Anfrage und Key gebunden, 15 Minuten gültig. Andere
Anfrage (irgendein Feld außer
pageSize) oder anderer Key:422 ERR_CURSOR_MISMATCH. Unlesbar, abgelaufen oder nach einem Server-Neustart ohne festen Cursor-Schlüssel:422 ERR_BAD_CURSOR– dann ohnecursorneu beginnen. Ein Cursor von/v1/searchgilt hier nicht. - Stand:
standwechselt, wenn der Veranstalter Verträge, Aktionen oder Zuteilungen veröffentlicht oder Region, Koordinaten oder offizielle Kategorie eines Hotels im Hotelstamm ändert, und um Mitternacht (Stichtag), nicht aber bei Buchungen oder anderen Inhalten (Texte, Bilder). Hat sich der Stand seit der Vorseite geändert, rechnet die Folgeseite im neuen Stand ab derselben Stelle weiter, mitcoverage.standChanged: trueund einem Hinweis inwarnings. Dann (wie auch bei Buchungen anderer zwischen zwei Seiten) kann ein Hotel doppelt erscheinen oder fehlen: nachhoteldeduplizieren. Die Preise jeder Seite gelten für ihren Stand; beim Buchen sichertpriceCheckden Preis. - Zeitbudget: Jede Seite hat ein Zeitbudget (Suchprofil, z. B. 150 ms), gezählt ab
Eingang der Anfrage. Ist es aufgebraucht und hat die Seite schon einen Treffer, rechnet sie
keine weiteren Hotels mehr; Treffer, deren Platz schon feststeht, kommen noch mit. Dann:
coverage.complete: false,timeBudgetExhausted: true,undecided> 0 undnextCursorfür den Rest. Die gezeigten Treffer sind trotzdem exakt und in bewiesener Reihenfolge – einfach weiterblättern. Eine Seite endet nie ohne Treffer, solange es noch einen gibt. Gekürzte Seiten sind kein Fehler und kommen vor allem bei sehr großen Suchen vor (langes Anreisefenster mit großer Spanne der Dauer im größten Ziel), wenn mehrere Suchen desselben Veranstalters gleichzeitig rechnen. Wer volle Seiten braucht, fragt kleinere Fenster oder Spannen ab oder sucht nacheinander statt gleichzeitig. - Last: Die offene Suche teilt sich mit
/v1/searchdie Such-Plätze und die Rechenzeit des Veranstalters (4.4), belegt aber höchstens die Hälfte der Such-Plätze (bei 8 also 4, alle Keys des Veranstalters und beide Endpunkte zusammen) – der Rest bleibt für/v1/search. Dazu gelten Takt und gleichzeitige Suchen des Suchprofils je Key (429 ERR_RATE_LIMITEDbzw.ERR_SEARCH_BUSY, mitRetry-After); eine mit429 ERR_SEARCH_BUSYabgewiesene Suche verbraucht keinen Takt. Rechnet eine Suche länger als 10 s, endet sie mit503 ERR_SEARCH_TIMEOUT, nie mit einer Teilseite. - Nicht beweisbar: Kann die Suche für eine Anfrage Preis, Reihenfolge oder Verfügbarkeit
nicht beweisen (ein Fehler im Server, kein Anfragefehler), antwortet sie
500 ERR_INTERNALstatt einer möglicherweise falschen Seite.
4a.5 Fehler der offenen Suche
Die Prüfung läuft in dieser Reihenfolge; der erste Befund wird gemeldet: Key → Recht → Body und unbekannte Felder → Feldformat → Grenzen der API (1.3) → Grenzen des Suchprofils → Ziele, Hotels, Verpflegung, Währung → Cursor → Takt und Such-Plätze → Rechnung. Eine abgelehnte Anfrage belegt keinen Such-Platz und zählt nicht als Suche. Die Codes stehen im Fehlerkatalog (7.2, 7.3).
### suche-offen-ohne-recht
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{poolKey}}
{
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D6}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{"errorCode": "ERR_OPEN_SEARCH_NOT_ALLOWED",
"message": "dieser API-Key hat kein Recht fuer die offene Suche — der Veranstalter-Admin schaltet es frei (Suchprofil des Keys)"}### suche-offen-fenster-zu-breit
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D30}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{"errorCode": "ERR_WINDOW_TOO_WIDE", "message": "arrivalFrom..arrivalTo: 31 Tage, erlaubt hoechstens 14 (Suchprofil des Keys)"}Der Key der Kundengruppe darf nur in einem Ziel suchen:
### suche-offen-ziel-nicht-erlaubt
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{partnerKey}}
{
"destinations": ["PMI"],
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D6}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}{"errorCode": "ERR_DESTINATION_NOT_ALLOWED", "message": "destinations[0]: Ziel ausserhalb der erlaubten Ziele dieses Keys (Suchprofil des Keys)"}Ein Filter mit falschem Wert nennt das Feld:
### suche-offen-filter-kaputt
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D6}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"geo": {"lat": 39.57, "lon": 2.65, "radiusKm": 600}
}{"errorCode": "ERR_BAD_FILTER", "message": "geo.radiusKm: 600 ausserhalb 0.001..500"}4b. Termin-Matrix: POST /v1/search/open/dates (BA)
4b.1 Wozu
„Termine & Preise“ eines Hotels: für ein Hotel und dasselbe Fenster wie die offene Suche je Termin (Anreise × Dauer) das günstigste buchbare Angebot oder der Grund, warum es keins gibt. Recht und Grenzen kommen aus demselben Suchprofil wie bei der offenen Suche (4a.1); Takt und gleichzeitige Suchen des Profils gelten für beide Endpunkte zusammen.
- Der Preis ist exakt: jede Zelle mit Angebot ist bit-gleich zu
/v1/pricemit demselben Hotel, Zimmer, Verpflegung, An- und Abreise und derselben Belegung. - Passt zur offenen Suche: Bei gleichem Filter und gleichem
standistbestaus 4a die erste Zelle mit dem kleinsten Kriterium (Gesamtpreis bzw. Preis je Nacht) – dieselbe Gleichstands-Regel (frühere Anreise, kürzere Dauer, Zimmer und Verpflegung in der Reihenfolge des Vertrags). MitperBoardistbestdie Zelle seiner Verpflegung am ersten Termin mit dem kleinsten Kriterium.
4b.2 Anfrage
- Feld
hotel- Pflicht
- ja
- Bedeutung
- Hotel-Code des Keys (z. B.
results[].hotelaus 4a)
- Feld
arrivalFrom,arrivalTo- Pflicht
- ja
- Bedeutung
- Anreisefenster wie 4a.2; höchstens
matrixMaxWindowDaysTage (Suchprofil)
- Feld
nightsMin,nightsMax- Pflicht
- ja
- Bedeutung
- Dauer von–bis wie 4a.2 (erlaubte Dauern des Profils)
- Feld
occupancy- Pflicht
- ja
- Bedeutung
- ein Zimmer, wie
/v1/price
- Feld
boards,boardTypes- Pflicht
- nein
- Bedeutung
- wie 4a.2; jeder Code in
boardsmuss im Hotel angeboten werden (422 ERR_BOARD_NOT_OFFERED)
- Feld
rooms- Pflicht
- nein
- Bedeutung
- Zimmer-Codes des Hotels; ohne = alle (unbekannt:
404 ERR_ROOM_NOT_FOUND)
- Feld
minTotalCents,maxTotalCents- Pflicht
- nein
- Bedeutung
- Filter auf den Gesamtpreis wie 4a.2
- Feld
currency- Pflicht
- nein
- Bedeutung
- Vertragswährung des Hotels; rechnet das Hotel anders oder ohne gültige Währung:
422 ERR_CURRENCY_NOT_AVAILABLE
- Feld
perBoard- Pflicht
- nein
- Bedeutung
true: je Termin eine Zelle je Verpflegung (Standardfalse: eine Zelle je Termin)
- Feld
category,regions,geo- Pflicht
- nein
- Bedeutung
- Filter nach Hotelstamm wie 4a.2, dieselbe Wirkung: liegt das Hotel außerhalb, gehört es nicht zum Suchraum und die Matrix hat keine Zelle (
cellsleer); fehlt ihm der Wert, ist jede ZellenonemitERR_NO_CATEGORY,ERR_NO_REGIONbzw.ERR_NO_GEO
Zellen = Anreisetage × Dauern (× Verpflegungen bei perBoard), höchstens matrixMaxCells
(Suchprofil, sonst 422 ERR_SEARCH_TOO_BROAD). Einen Cursor gibt es nicht. Jedes unbekannte
Feld: 422 ERR_UNKNOWN_FIELD. Die Prüfreihenfolge ist die der offenen Suche (4a.5), ohne Cursor.
4b.3 Antwort
- Feld
hotel,currency- Bedeutung
- Hotel und Vertragswährung
- Feld
stand- Bedeutung
- Datenstand wie in 4a (gleicher
stand= gleiche Daten)
- Feld
boards- Bedeutung
- nur mit
perBoard: die Verpflegungen je Termin in Vertragsreihenfolge (erstes Auftreten über die Zimmer, RO zuerst, wie/v1/prices), in der Reihenfolge der Zellen
- Feld
cells[]- Bedeutung
- dicht in der Reihenfolge
checkIn,nights(beiperBoarddann Verpflegung); jede Zelle mitcheckIn,checkOut,nights,status. Leer nur, wenn das Hotel außerhalb der Filter nach Hotelstamm liegt
- Feld
cells[].status = "offer"- Bedeutung
- Angebot:
room,board,boardType,totalCents,perTravellerCents,availability(wie/v1/price, immeravailable: true) – das günstigste Zimmer/die günstigste Verpflegung des Termins, bei Gleichstand in Vertragsreihenfolge
- Feld
cells[].status = "none"- Bedeutung
- kein Angebot, Grund in
reason: die Codes von/v1/price, die Reihenfolge der offenen Suche (4a) – erst die Verfügbarkeit (ERR_NO_INVENTORY,ERR_NOT_AVAILABLE), dann Verkaufsregeln, Belegung, Saison, Preisfilter (ERR_OUTSIDE_PRICE_FILTER) oder ein Vertragsfehler; mit Filtern nach HotelstammERR_NO_CATEGORY,ERR_NO_REGION,ERR_NO_GEO(dem Hotel fehlt der Wert, jede Zelle)
- Feld
cells[].status = "unchecked"- Bedeutung
- nicht geprüft, weil das Zeitbudget ablief – nie als „kein Angebot“ lesen
- Feld
coverage- Bedeutung
complete(keine Zelleunchecked),timeBudgetExhausted,cells=offers+none+unchecked,roomErrors[](Zimmer mit Vertragsfehler je Code, 7.5)
- Feld
warnings[]- Bedeutung
- Hinweise, z. B. zu ungeprüften Zellen
Die Zellen laufen in Blöcken in der Reihenfolge der Matrix; ist das Zeitbudget des Profils
aufgebraucht, bleiben die restlichen Zellen unchecked (der erste Block wird immer geprüft).
Dann das Fenster oder die Dauern eingrenzen und neu fragen. Rechnet die Matrix länger als 10 s:
503 ERR_SEARCH_TIMEOUT; nicht beweisbar: 500 ERR_INTERNAL (wie 4a.4).
### suche-termine
POST {{baseUrl}}/v1/search/open/dates
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D1}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"maxTotalCents": 20000
}{
"hotel": "TEST-HOTEL-BASE",
"currency": "EUR",
"stand": "{{*}}",
"cells": [
{"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "status": "offer", "room": "DZ", "board": "RO",
"boardType": "AO", "totalCents": 18000, "perTravellerCents": [18000, 0],
"availability": {"configured": true, "available": true, "minFree": 5}},
{"checkIn": "{{D0}}", "checkOut": "{{D3}}", "nights": 3, "status": "none", "reason": "ERR_OUTSIDE_PRICE_FILTER"},
{"checkIn": "{{D1}}", "checkOut": "{{D3}}", "nights": 2, "status": "offer", "room": "DZ", "board": "RO",
"boardType": "AO", "totalCents": 18000, "perTravellerCents": [18000, 0],
"availability": {"configured": true, "available": true, "minFree": 5}},
{"checkIn": "{{D1}}", "checkOut": "{{D4}}", "nights": 3, "status": "none", "reason": "ERR_OUTSIDE_PRICE_FILTER"}
],
"coverage": {"complete": true, "timeBudgetExhausted": false, "cells": 4, "offers": 2, "none": 2, "unchecked": 0,
"roomErrors": []}
}Je Verpflegung, nur Frühstück und Halbpension:
### suche-termine-je-verpflegung
POST {{baseUrl}}/v1/search/open/dates
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D0}}",
"nightsMin": 2,
"nightsMax": 3,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"boards": ["BB", "HB"],
"perBoard": true
}{
"hotel": "TEST-HOTEL-BASE",
"currency": "EUR",
"stand": "{{*}}",
"boards": ["BB", "HB"],
"cells": [
{"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "status": "offer", "room": "DZ", "board": "BB",
"boardType": "BB", "totalCents": 26000, "perTravellerCents": [22000, 4000],
"availability": {"configured": true, "available": true, "minFree": 5}},
{"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "status": "offer", "room": "DZ", "board": "HB",
"boardType": "HB", "totalCents": 32000, "perTravellerCents": [25000, 7000],
"availability": {"configured": true, "available": true, "minFree": 5}},
{"checkIn": "{{D0}}", "checkOut": "{{D3}}", "nights": 3, "status": "offer", "room": "DZ", "board": "BB",
"boardType": "BB", "totalCents": 38000, "perTravellerCents": [32000, 6000],
"availability": {"configured": true, "available": true, "minFree": 5}},
{"checkIn": "{{D0}}", "checkOut": "{{D3}}", "nights": 3, "status": "offer", "room": "DZ", "board": "HB",
"boardType": "HB", "totalCents": 47000, "perTravellerCents": [36500, 10500],
"availability": {"configured": true, "available": true, "minFree": 5}}
],
"coverage": {"complete": true, "timeBudgetExhausted": false, "cells": 4, "offers": 4, "none": 0, "unchecked": 0,
"roomErrors": []}
}31 Anreisetage × 14 Dauern × 3 Verpflegungen sind mehr Zellen, als das Profil erlaubt:
### suche-termine-zu-viele-zellen
POST {{baseUrl}}/v1/search/open/dates
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"arrivalFrom": "{{D0}}",
"arrivalTo": "{{D30}}",
"nightsMin": 1,
"nightsMax": 14,
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"perBoard": true
}{"errorCode": "ERR_SEARCH_TOO_BROAD",
"message": "Termin-Matrix: 1302 Zellen (Anreisen x Dauern x 3 Verpflegungen), erlaubt hoechstens 500 (Suchprofil des Keys)"}4c. Grenzen des Keys: GET /v1/limits
Die wirksamen Grenzen des eigenen Keys, damit eine Webseite Datumswahl, Dauer-Auswahl und
Ziele einstellt, statt Grenzen über 422 zu ertasten. Keine Parameter (jeder: 422
ERR_UNKNOWN_FIELD), zählt nicht als Suche.
- Feld
rate- Bedeutung
- Takt aller Anfragen des Keys (4.4):
perSecond,burst,scope(key= je Key und Knoten;testCircle= gemeinsamer Topf der Test-Keys, 11)
- Feld
export.allowed- Bedeutung
- Recht an der EDF-Lieferung (10)
- Feld
content.allowed- Bedeutung
- Zugang zur Content-API (
/v1/content/*): der Betreiber hat Inhalte für den Veranstalter freigeschaltet und der Key trägt das Inhalts-Recht – dieselbe Regel wie dort; ohne Zugang403 ERR_CONTENT_NOT_ALLOWED
- Feld
openSearch.allowed- Bedeutung
- Recht für die offene Suche und die Termin-Matrix; ohne Recht steht nur dieses Feld da
- Feld
openSearch.maxWindowDays,nightsMin,nightsMax,maxNightsSpan- Bedeutung
- Anreisefenster, erlaubte Dauern und Dauer-Spanne je Anfrage (4a)
- Feld
openSearch.maxDestinations,maxHotels,maxCandidates,maxPageSize- Bedeutung
- Ziele und Hotels je Anfrage, Hotels im Suchraum, Treffer je Seite (4a)
- Feld
openSearch.timeBudgetMs,rate,burst,concurrency- Bedeutung
- Zeitbudget je Seite bzw. Matrix, Takt und gleichzeitige Suchen (beide Endpunkte zusammen)
- Feld
openSearch.matrixMaxWindowDays,matrixMaxCells- Bedeutung
- Fenster und Zellen der Termin-Matrix (4b)
- Feld
openSearch.allDestinations,destinations- Bedeutung
- alle Ziele erlaubt, sonst die erlaubten Ziel-Codes – nur solche mit Hotels im Bestand des Keys (wie
GET /v1/destinations)
Die Werte sind genau die, die /v1/search/open und /v1/search/open/dates anwenden: das
Kleinste aus dem, was der Betreiber für den Veranstalter freigibt, und dem Profil des Keys.
Welche Ebene eine Grenze setzt, steht nicht in der Antwort. Änderungen im Profil wirken nach
wenigen Sekunden.
### grenzen
GET {{baseUrl}}/v1/limits
X-Api-Key: {{apiKey}}{
"rate": {"perSecond": 200, "burst": 400, "scope": "key"},
"export": {"allowed": true},
"content": {"allowed": true},
"openSearch": {"allowed": true, "maxWindowDays": 14, "nightsMin": 1, "nightsMax": 14, "maxNightsSpan": 3,
"maxDestinations": 3, "maxHotels": 20, "maxCandidates": 500, "maxPageSize": 20,
"timeBudgetMs": 150, "rate": 5, "burst": 20, "concurrency": 1,
"matrixMaxWindowDays": 31, "matrixMaxCells": 500, "allDestinations": true}
}Ein Key ohne Recht für die offene Suche:
### grenzen-ohne-recht
GET {{baseUrl}}/v1/limits
X-Api-Key: {{poolKey}}{
"rate": {"perSecond": 200, "burst": 400, "scope": "key"},
"export": {"allowed": false},
"content": {"allowed": false},
"openSearch": {"allowed": false}
}5. Buchung (B) und Buchungsinfo
5.1 POST /v1/book
- Feld
hotel,room- Pflicht
- ja
- Bedeutung
- Hotel und Zimmer-Code (Buchung ist verpflegungsneutral; die Verpflegung steht im
priceCheck). Fehltroom:400 ERR_INVALID_BUCKET, mit und ohnepriceCheck
- Feld
checkIn,checkOut- Pflicht
- ja
- Bedeutung
- Aufenthalt (Abschnitt 1.3)
- Feld
quantity- Pflicht
- ja
- Bedeutung
- Anzahl Zimmer, 1–1.000.000 (
400 ERR_QUANTITY_INVALID)
- Feld
idemKey- Pflicht
- ja
- Bedeutung
- eigener, eindeutiger Schlüssel des Vorgangs (Abschnitt 9), höchstens 128 Zeichen; fehlt er:
400 ERR_INVALID_IDEM_KEY
- Feld
reference- Pflicht
- nein
- Bedeutung
- eigene Buchungsreferenz, höchstens 128 Zeichen; damit ist die Buchung später lesbar
- Feld
leadPaxName- Pflicht
- nein
- Bedeutung
- Name des Hauptreisenden, höchstens 255 Zeichen
- Feld
metadata- Pflicht
- nein
- Bedeutung
- freies JSON-Objekt, wird gespeichert und bei
/v1/bookingzurückgegeben, nicht ausgewertet.metadata.correlationId(bis 64 Zeichen) wird als Korrelations-ID übernommen.
- Feld
priceCheck- Pflicht
- nein, empfohlen
- Bedeutung
- Preisprüfung vor dem Verkauf, siehe unten
Zu lange Texte (idemKey, reference, leadPaxName, metadata.correlationId) lehnt die API
vor jedem Verkauf mit 422 ERR_VALIDATION ab; message nennt Feld, Länge und Grenze. Es wird
nie gekürzt.
priceCheck: board, occupancy.travellers[], expectedCents (der Preis, den der Kunde
gesehen hat), tolerancePercent (erlaubte Abweichung in %, ≥ 0), optional currency und
now. TourAPI rechnet den Preis wie /v1/price neu; weicht er um mehr als die Toleranz ab:
409 ERR_PRICE_DRIFT mit dem aktuellen Preis in message, nichts gebucht. Mit
priceCheck speichert TourAPI den geprüften Preis an der Buchung (totalCents, currency,
board in /v1/booking). Ohne priceCheck ist die Buchung ein reiner Kontingentverkauf ohne
Preis (totalCents: null). Auch dann verkauft /v1/book nur Hotels, die /v1/price für den
Key kennt: Ohne gültigen Vertrag gibt es kein Hotel (404 ERR_HOTEL_NOT_FOUND), auch wenn
Kontingent hinterlegt ist.
Antwort: booked, alreadyBooked (true = Wiederholung, nichts neu verkauft), reference
(unsere Buchungsreferenz TA-…), correlationId. correlationId ist die mitgeschickte
metadata.correlationId; fehlt sie, vergibt TourAPI eine UUID und speichert sie am Beleg.
Eine Wiederholung (alreadyBooked=true) nennt immer die gespeicherte correlationId des
ersten Aufrufs – auch wenn sie keine oder eine andere mitschickt.
### buchen
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "BEISPIEL-0001",
"reference": "KUNDE-4711",
"leadPaxName": "Erika Muster",
"metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
"priceCheck": {
"board": "RO",
"currency": "EUR",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"expectedCents": 18000,
"tolerancePercent": 0
}
}{"booked": true, "alreadyBooked": false, "reference": "{{buchungsRef}}", "correlationId": "kette-0815"}5.2 Wiederholen ist sicher
Dieselbe Anfrage mit demselben idemKey noch einmal – etwa nach einem Netzfehler – verkauft
nicht erneut, sondern bestätigt die bestehende Buchung (alreadyBooked: true, dieselbe
Referenz).
### buchen-wiederholt
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "BEISPIEL-0001",
"reference": "KUNDE-4711",
"leadPaxName": "Erika Muster",
"metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
"priceCheck": {
"board": "RO",
"currency": "EUR",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"expectedCents": 18000,
"tolerancePercent": 0
}
}{"booked": true, "alreadyBooked": true, "reference": "{{buchungsRef}}", "correlationId": "kette-0815"}Derselbe idemKey mit anderen Buchungsdaten (Hotel, Zimmer, Aufenthalt, Menge) ist ein
Fehler im Aufrufer:
### buchen-anderer-vorgang
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 2,
"idemKey": "BEISPIEL-0001"
}{"errorCode": "ERR_IDEMPOTENCY_MISMATCH", "message": "idemKey bereits mit anderen Buchungsdaten (hotel, room, checkIn, checkOut, quantity) vergeben"}5.3 Warum eine Buchung scheitert
Scheitert der Verkauf an einer Nacht, nennt message die erste betroffene Nacht. Nichts
wird gebucht (alle Nächte oder keine).
- Code
ERR_SOLD_OUT- Status
- 422
- Bedeutung
- Nacht ausgebucht
- Code
ERR_STOP_SALE- Status
- 422
- Bedeutung
- Veranstalter hat den Verkauf gestoppt
- Code
ERR_INVENTORY_CLOSED- Status
- 422
- Bedeutung
- Nacht geschlossen oder nur auf Anfrage
- Code
ERR_NO_INVENTORY- Status
- 422
- Bedeutung
- für eine Nacht ist keine Kapazität hinterlegt
- Code
ERR_GROUP_LIMIT- Status
- 422
- Bedeutung
- die Zuteilung der Kundengruppe ist erschöpft (oder für Zimmer und Nacht nicht vorhanden)
- Code
ERR_HOTEL_NOT_FOUND- Status
- 404
- Bedeutung
- Hotel gibt es für diesen Key nicht (auch: kein gültiger Vertrag, mit und ohne
priceCheck; Kundengruppe ohne Angebot, 1.1)
- Code
ERR_PRICE_DRIFT- Status
- 409
- Bedeutung
- Preis weicht vom
priceCheckab
- Code
ERR_STAY_LENGTH_NOT_ALLOWED,ERR_ARRIVAL_DAY_NOT_ALLOWED,ERR_TRAVEL_DATES_NOT_ALLOWED,ERR_BOARD_NOT_ALLOWED,ERR_LEAD_TIME_NOT_ALLOWED- Status
- 422
- Bedeutung
- eine Verkaufsregel des Zimmers schließt den Aufenthalt aus (3.4); nichts wird gebucht
- Code
ERR_BOARD_NOT_AVAILABLE- Status
- 422
- Bedeutung
- die Verpflegung des
priceCheckwird für diese Reisegruppe nicht verkauft (3.5); nichts wird gebucht
### buchen-preis-geaendert
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "BEISPIEL-0002",
"priceCheck": {
"board": "RO",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"expectedCents": 17000,
"tolerancePercent": 2
}
}{"errorCode": "ERR_PRICE_DRIFT", "message": "Preis hat sich geaendert: aktuell 18000 Cent, erwartet 17000 Cent"}### buchen-ausgebucht
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-SOLD",
"room": "DZ",
"checkIn": "{{D13}}",
"checkOut": "{{D14}}",
"quantity": 1,
"idemKey": "BEISPIEL-0003"
}{"errorCode": "ERR_SOLD_OUT", "message": "Nacht {{D13}} ausgebucht"}### buchen-stop-sale
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-STOP",
"room": "DZ",
"checkIn": "{{D11}}",
"checkOut": "{{D12}}",
"quantity": 1,
"idemKey": "BEISPIEL-0004"
}{"errorCode": "ERR_STOP_SALE", "message": "Nacht {{D11}} stop-sale"}Ein Key mit Kundengruppe mit Zuteilung bucht gegen die Zuteilung seiner Gruppe, auch in
Nächten im Freiverkauf. Hier hat TEST-PARTNER-BASE für die Nacht 5 zugeteilt:
### buchen-zuteilung-erschoepft
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{partnerKey}}
{
"hotel": "TEST-HOTEL-FREESALE",
"room": "DZ",
"checkIn": "{{D20}}",
"checkOut": "{{D21}}",
"quantity": 6,
"idemKey": "BEISPIEL-0005"
}{"errorCode": "ERR_GROUP_LIMIT", "message": "Gruppen-Kontingent fuer {{D20}} erschoepft"}Eine Preisgruppe bucht aus dem allgemeinen Bestand wie ein Key ohne Gruppe:
### buchen-preisgruppe
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{rabattKey}}
{
"hotel": "TEST-HOTEL-DISCOUNT",
"room": "DZ",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"quantity": 1,
"idemKey": "BEISPIEL-0006"
}{"booked": true, "alreadyBooked": false, "reference": "{{*}}", "correlationId": "{{*}}"}5.4 GET /v1/booking?ref=… – Buchungsinfo
ref ist unsere Referenz (TA-…) oder die eigene reference aus der Buchung; unsere
Referenz gewinnt. Passt die eigene reference zu mehreren sichtbaren Buchungen (auch
stornierten): 409 ERR_REFERENCE_AMBIGUOUS, message nennt die passenden TA-…-Referenzen
(neueste zuerst, höchstens 10) – dann mit unserer Referenz aus der Buchungsantwort lesen. Antwort:
reference, customerReference, hotel, room, group (nur bei Kundengruppen-Buchungen; Gruppen-Code
aus A-Z a-z 0-9 . _ -, 1 bis 64 Zeichen),
checkIn, checkOut, quantity, status (confirmed | released), bookedAt,
updatedAt (letzte Änderung, bei Storno die Stornozeit; beide RFC 3339 in UTC, z. B.
2026-09-26T08:15:03Z), metadata, totalCents, currency, board. Personendaten der
Reisenden gibt die API nicht zurück.
| Gebucht … | dann in der Buchungsinfo |
|---|---|
mit priceCheck | totalCents = geprüfter Preis, currency = Vertragswährung, board = Verpflegung aus dem priceCheck |
ohne priceCheck | totalCents: null, currency: "", board: "" (leere Texte, kein Preis erfasst) |
ohne reference | customerReference fehlt |
ohne metadata | metadata fehlt |
| mit Key ohne Kundengruppe | group fehlt |
Sichtbarkeit: Ein Key mit Kundengruppe sieht nur Buchungen seiner Gruppe. Ein Key auf dem
Basisvertrag sieht alle Buchungen des Veranstalters, auch die der Kundengruppen – stornieren
kann er aber nur seine eigenen (Abschnitt 6). Unbekannt oder nicht sichtbar:
404 ERR_BOOKING_NOT_FOUND.
### buchung-lesen
GET {{baseUrl}}/v1/booking?ref={{buchungsRef}}
X-Api-Key: {{apiKey}}{
"reference": "{{buchungsRef}}",
"customerReference": "KUNDE-4711",
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"status": "confirmed",
"bookedAt": "{{*}}",
"updatedAt": "{{*}}",
"metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
"totalCents": 18000,
"currency": "EUR",
"board": "RO"
}Über die eigene Referenz:
### buchung-lesen-kundenreferenz
GET {{baseUrl}}/v1/booking?ref=KUNDE-4711
X-Api-Key: {{apiKey}}{
"reference": "{{buchungsRef}}",
"customerReference": "KUNDE-4711",
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"status": "confirmed",
"bookedAt": "{{*}}",
"updatedAt": "{{*}}",
"metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
"totalCents": 18000,
"currency": "EUR",
"board": "RO"
}Eine Buchung ohne priceCheck, ohne reference und ohne metadata – die Antwort trägt eine
vom Server vergebene correlationId, die Buchungsinfo keinen Preis:
### buchen-ohne-preispruefung
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D20}}",
"checkOut": "{{D21}}",
"quantity": 1,
"idemKey": "BEISPIEL-0010"
}{"booked": true, "alreadyBooked": false, "reference": "{{ohnePreisRef}}", "correlationId": "{{*}}"}### buchung-lesen-ohne-preispruefung
GET {{baseUrl}}/v1/booking?ref={{ohnePreisRef}}
X-Api-Key: {{apiKey}}{
"reference": "{{ohnePreisRef}}",
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D20}}",
"checkOut": "{{D21}}",
"quantity": 1,
"status": "confirmed",
"bookedAt": "{{*}}",
"updatedAt": "{{*}}",
"totalCents": null,
"currency": "",
"board": ""
}Dieselbe eigene Referenz an einer zweiten Buchung macht sie mehrdeutig:
### buchen-gleiche-kundenreferenz
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D22}}",
"checkOut": "{{D23}}",
"quantity": 1,
"idemKey": "BEISPIEL-0011",
"reference": "KUNDE-4711"
}{"booked": true, "alreadyBooked": false, "reference": "{{zweiteRef}}", "correlationId": "{{*}}"}### buchung-lesen-mehrdeutig
GET {{baseUrl}}/v1/booking?ref=KUNDE-4711
X-Api-Key: {{apiKey}}{"errorCode": "ERR_REFERENCE_AMBIGUOUS", "message": "ref 'KUNDE-4711' passt zu mehreren Buchungen ({{zweiteRef}}, {{buchungsRef}}) — mit der TourAPI-Referenz (TA-…) lesen"}6. Storno (S): POST /v1/cancel
Einziges Feld: idemKey der Buchung (fehlt er: 400 ERR_INVALID_IDEM_KEY; jedes weitere
Feld: 422 ERR_UNKNOWN_FIELD). Das Kontingent aller Nächte wird zurückgegeben, der
Status wird released. Storniert werden kann nur, was mit demselben Key-Kreis gebucht
wurde: Ein Key mit Kundengruppe storniert die Buchungen seiner Gruppe, ein Key auf dem
Basisvertrag die Buchungen des Basisvertrags. Alles andere ist 404 ERR_BOOKING_NOT_FOUND.
Stornogebühren berechnet die API nicht.
Jeder Storno wird beim Veranstalter mit Zeitpunkt und der Kennung (key_id) des
stornierenden Keys protokolliert und in seiner Buchungsansicht angezeigt; eine
Wiederholung (alreadyReleased) erzeugt keinen zweiten Eintrag.
Antwort: released (true), alreadyReleased (true = war schon storniert).
### stornieren
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{apiKey}}
{"idemKey": "BEISPIEL-0001"}{"released": true, "alreadyReleased": false}Wiederholung ist sicher:
### stornieren-wiederholt
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{apiKey}}
{"idemKey": "BEISPIEL-0001"}{"released": true, "alreadyReleased": true}Die Buchung bleibt lesbar, mit status: released:
### buchung-lesen-storniert
GET {{baseUrl}}/v1/booking?ref={{buchungsRef}}
X-Api-Key: {{apiKey}}{
"reference": "{{buchungsRef}}",
"customerReference": "KUNDE-4711",
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"status": "released",
"bookedAt": "{{*}}",
"updatedAt": "{{*}}",
"metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
"totalCents": 18000,
"currency": "EUR",
"board": "RO"
}Ein stornierter idemKey ist verbraucht. Für eine neue Buchung einen neuen Key nehmen:
### buchen-nach-storno
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "BEISPIEL-0001"
}{"errorCode": "ERR_IDEM_KEY_RELEASED", "message": "idemKey gehoert zu einer stornierten Buchung — fuer einen neuen Verkauf einen neuen idemKey verwenden"}### stornieren-unbekannt
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{apiKey}}
{"idemKey": "GIBT-ES-NICHT"}{"errorCode": "ERR_BOOKING_NOT_FOUND", "message": "keine Buchung mit diesem idemKey"}7. Fehlerkatalog
Spalte „Aufrufer“: Anfrage korrigieren = nicht wiederholen, der Fehler liegt in der
Anfrage. Wiederholen = dieselbe Anfrage später noch einmal (bei Retry-After frühestens
nach so vielen Sekunden). Melden = beim Veranstalter/Betreiber melden, Wiederholen hilft
nicht. Ein Code kann an mehreren Endpunkten vorkommen; Status und Bedeutung bleiben gleich.
7.1 Zugang und Transport
- Code
ERR_UNAUTHORIZED- Status
- 401
- Bedeutung
- Key fehlt, unbekannt oder widerrufen
- Aufrufer
- Key prüfen, nicht wiederholen (Wiederholen wird verzögert, Abschnitt 8)
- Code
ERR_TENANT_SUSPENDED- Status
- 403
- Bedeutung
- Key gueltig, Veranstalter gesperrt
- Aufrufer
- Veranstalter fragen, nicht wiederholen
- Code
ERR_KEY_GROUP_INACTIVE- Status
- 403
- Bedeutung
- Kundengruppe des Keys deaktiviert
- Aufrufer
- Veranstalter fragen
- Code
ERR_MODE_MISMATCH- Status
- 403
- Bedeutung
X-TourAPI-Require-Modeverlangt einen anderen Modus als den des Keys (z. B. Live-Key in einer Test-Umgebung); nichts ausgeführt (Abschnitt 11.1)- Aufrufer
- Key tauschen, nicht wiederholen
- Code
ERR_SCENARIO_NOT_ALLOWED- Status
- 422
- Bedeutung
X-TourAPI-Sandbox-Scenariomit Live-Key, unbekanntem Szenario oder an einem Endpunkt, an dem es nicht wirkt (Abschnitt 11.3)- Aufrufer
- Anfrage korrigieren
- Code
ERR_METHOD_NOT_ALLOWED- Status
- 405
- Bedeutung
- falsche HTTP-Methode
- Aufrufer
- Anfrage korrigieren
- Code
ERR_BAD_REQUEST- Status
- 400
- Bedeutung
- Body kein JSON, zu groß (> 1 MiB),
reffehlt,priceCheck.tolerancePercentnegativ,priceCheck.currencylänger als 3 Zeichen (kürzer oder unbekannt:422 ERR_CURRENCY_NOT_AVAILABLE); Content-API:sincefehlt, Parameter mehrfach,langmit mehr als 5 oder doppelten Sprachen - Aufrufer
- Anfrage korrigieren
- Code
ERR_UNKNOWN_FIELD- Status
- 422
- Bedeutung
- unbekanntes Feld in
occupancy(Auskunft) bzw. irgendwo (/v1/search/open,/v1/search/open/dates,/v1/book,/v1/cancel, Body von/v1/export/edf/ack); unbekannter Query-Parameter an/v1/export/edf/*und/v1/limits - Aufrufer
- Anfrage korrigieren
- Code
ERR_OPEN_SEARCH_NOT_ALLOWED- Status
- 403
- Bedeutung
- der Key hat kein Recht für die offene Suche und die Termin-Matrix (4a.1; bei neuen Keys aus;
GET /v1/limitszeigt es) - Aufrufer
- Veranstalter fragen
- Code
ERR_DESTINATION_NOT_ALLOWED- Status
- 403
- Bedeutung
- offene Suche: das Ziel (
destinations[i]) bzw. das Hotel (hotels[i], Termin-Matrix:hotel) liegt außerhalb der erlaubten Ziele des Suchprofils - Aufrufer
- Ziel aus dem Suchprofil nehmen (
GET /v1/limits)
### fehler-ohne-key
POST {{baseUrl}}/v1/price
Content-Type: application/json
{}{"errorCode": "ERR_UNAUTHORIZED", "message": "fehlender/ungueltiger API-Key"}### fehler-key-widerrufen
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{gesperrterKey}}
{}{"errorCode": "ERR_UNAUTHORIZED", "message": "fehlender/ungueltiger API-Key"}### fehler-methode
GET {{baseUrl}}/v1/price
X-Api-Key: {{apiKey}}{"errorCode": "ERR_METHOD_NOT_ALLOWED", "message": "nur POST"}### fehler-tippfehler-belegung
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}, {"alter": 8}]}
}{"errorCode": "ERR_UNKNOWN_FIELD", "message": "unbekanntes Feld 'occupancy.travellers[1].alter' — abgelehnt (die Belegung bestimmt den Preis, hier wird nicht geraten)"}7.2 Anfrage (Felder und Grenzen)
- Code
ERR_BAD_DATE- Status
- 422
- Bedeutung
- Datum fehlt oder ist nicht
JJJJ-MM-TT(messagenennt das Feld) - Aufrufer
- Anfrage korrigieren
- Code
ERR_EMPTY_STAY- Status
- 422
- Bedeutung
checkOut≤checkIn- Aufrufer
- Anfrage korrigieren
- Code
ERR_STAY_TOO_LONG- Status
- 422
- Bedeutung
- mehr als 30 Nächte
- Aufrufer
- Anfrage korrigieren
- Code
ERR_STAY_IN_PAST- Status
- 422
- Bedeutung
- Anreise vor dem Stichtag
- Aufrufer
- Anfrage korrigieren
- Code
ERR_STAY_TOO_FAR- Status
- 422
- Bedeutung
- Anreise mehr als 732 Tage nach dem Stichtag
- Aufrufer
- Anfrage korrigieren
- Code
ERR_NO_TRAVELLERS- Status
- 422
- Bedeutung
- keine Reisenden
- Aufrufer
- Anfrage korrigieren
- Code
ERR_TOO_MANY_TRAVELLERS- Status
- 422
- Bedeutung
- mehr als 20 Reisende
- Aufrufer
- Anfrage korrigieren
- Code
ERR_INVALID_AGE- Status
- 422
- Bedeutung
- Alter negativ oder über 120
- Aufrufer
- Anfrage korrigieren
- Code
ERR_BOARD_MISSING- Status
- 422
- Bedeutung
boardfehlt (/v1/price,/v1/search,priceCheck)- Aufrufer
- Anfrage korrigieren
- Code
ERR_NOW_MISMATCH- Status
- 422
- Bedeutung
priceCheck.nowist nicht der Stichtag- Aufrufer
- Feld weglassen
- Code
ERR_VALIDATION- Status
- 422
- Bedeutung
- Text zu lang:
idemKey,reference(128),leadPaxName(255),metadata.correlationId(64);messagenennt Feld und Grenze - Aufrufer
- Anfrage korrigieren
- Code
ERR_QUANTITY_INVALID- Status
- 400
- Bedeutung
quantityaußerhalb 1–1.000.000- Aufrufer
- Anfrage korrigieren
- Code
ERR_INVALID_IDEM_KEY- Status
- 400
- Bedeutung
idemKeyfehlt (/v1/book,/v1/cancel)- Aufrufer
- Anfrage korrigieren
- Code
ERR_INVALID_BUCKET- Status
- 400
- Bedeutung
roomfehlt (/v1/book, mit und ohnepriceCheck)- Aufrufer
- Anfrage korrigieren
- Code
ERR_INVALID_STAY- Status
- 400
- Bedeutung
- Aufenthalt ungültig (Schutz im Verkauf; die API prüft vorher mit
ERR_BAD_DATE/ERR_EMPTY_STAY) - Aufrufer
- Anfrage korrigieren
- Code
ERR_BAD_PAGE_SIZE- Status
- 422
- Bedeutung
pageSizeaußerhalb 1–100 (/v1/search; offene Suche: 1 bis Suchprofil) bzw. 1–1000 (/v1/content/*)- Aufrufer
- Anfrage korrigieren
- Code
ERR_BAD_CURSOR- Status
- 422
- Bedeutung
- Cursor unlesbar, verändert oder abgelaufen (z. B. nach Server-Neustart; offene Suche: 15 Minuten nach Ausgabe); Content-API:
cursor/sinceunlesbar, verändert oder von einem anderen Key - Aufrufer
- ohne
cursorneu beginnen bzw. Verzeichnis neu holen
- Code
ERR_CURSOR_MISMATCH- Status
- 422
- Bedeutung
- Cursor gehört zu anderer Anfrage oder anderem Key (
/v1/search,/v1/search/open) - Aufrufer
- Anfrage korrigieren
- Code
ERR_UNKNOWN_DESTINATION- Status
- 422
- Bedeutung
/v1/searchbzw./v1/search/openohnecursor: zudestinationbzw.destinations[i]gibt es für diesen Key kein Hotel (Groß-/Kleinschreibung zählt)- Aufrufer
- Code aus
GET /v1/destinationsnehmen (4.5)
- Code
ERR_BAD_WINDOW- Status
- 422
- Bedeutung
- offene Suche und Termin-Matrix:
arrivalFrom/arrivalTofehlt oderarrivalToliegt vorarrivalFrom - Aufrufer
- Anfrage korrigieren
- Code
ERR_BAD_NIGHTS- Status
- 422
- Bedeutung
- offene Suche und Termin-Matrix:
nightsMin/nightsMaxfehlt, < 1 odernightsMin>nightsMax - Aufrufer
- Anfrage korrigieren
- Code
ERR_BAD_TARGET- Status
- 422
- Bedeutung
- offene Suche:
destinationsundhotelszugleich, leere Liste, leerer Code, oder keins von beiden, obwohl das Suchprofil nur einzelne Ziele erlaubt; Termin-Matrix:hotelfehlt - Aufrufer
- Anfrage korrigieren
- Code
ERR_BAD_SORT- Status
- 422
- Bedeutung
- offene Suche:
sortunbekannt (price,pricePerNight,hotel) - Aufrufer
- Anfrage korrigieren
- Code
ERR_BAD_FILTER- Status
- 422
- Bedeutung
- offene Suche und Termin-Matrix: leere
boards/boardTypes/rooms-Liste oder leerer Eintrag, unbekannte Verpflegungsart, Preisfilter negativ oderminTotalCents>maxTotalCents,currencykein Code aus drei Großbuchstaben; Filter nach Hotelstamm außerhalb der Form (category,regions,geo, 4a.2) – die Meldung nennt das Feld - Aufrufer
- Anfrage korrigieren
- Code
ERR_WINDOW_TOO_WIDE- Status
- 422
- Bedeutung
- offene Suche bzw. Termin-Matrix: Anreisefenster breiter als das Suchprofil erlaubt (
maxWindowDaysbzw.matrixMaxWindowDays,messagenennt die Grenze) - Aufrufer
- Fenster teilen oder verkleinern
- Code
ERR_NIGHTS_NOT_ALLOWED- Status
- 422
- Bedeutung
- offene Suche: Dauer außerhalb der erlaubten Dauern oder Spanne
nightsMax - nightsMin + 1zu breit; Termin-Matrix: Dauer außerhalb der erlaubten Dauern (messagenennt die Grenze) - Aufrufer
- Dauer anpassen
- Code
ERR_SEARCH_TOO_BROAD- Status
- 422
- Bedeutung
- offene Suche: zu viele
destinationsoderhotelsin der Anfrage oder zu viele Hotels im Suchraum; Termin-Matrix: mehr Zellen alsmatrixMaxCells(messagenennt die Grenze) - Aufrufer
- eingrenzen
- Code
ERR_CURRENCY_REQUIRED- Status
- 422
- Bedeutung
- offene Suche: die Hotels des Suchraums rechnen in mehreren Vertragswährungen,
currencyfehlt (messagenennt sie) - Aufrufer
currencysetzen
### fehler-aufenthalt-zu-lang
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D31}}",
"occupancy": {"travellers": [{"age": 40}]}
}{"errorCode": "ERR_STAY_TOO_LONG", "message": "checkOut: Aufenthalt laenger als 30 Naechte"}### fehler-anreise-vergangen
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"board": "RO",
"checkIn": "{{gestern}}",
"checkOut": "{{heute}}",
"occupancy": {"travellers": [{"age": 40}]}
}{"errorCode": "ERR_STAY_IN_PAST", "message": "checkIn: {{gestern}} liegt vor dem Stichtag {{heute}} (Serverdatum)"}Zu lange Texte werden vor dem Verkauf abgelehnt, nie gekürzt:
### fehler-text-zu-lang
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}{"errorCode": "ERR_VALIDATION", "message": "idemKey: zu lang (129 Zeichen, hoechstens 128)"}Ein verschriebenes Feld außerhalb der Belegung wird nicht abgelehnt, aber benannt – hier fehlt dadurch das Pflichtfeld, und die Antwort sagt beides:
### fehler-tippfehler-feld
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"board": "RO",
"checke": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}]}
}{"errorCode": "ERR_BAD_DATE", "message": "checkIn: fehlt (Pflichtfeld, JJJJ-MM-TT)",
"warnings": ["unbekanntes Feld 'checke' — ignoriert (Tippfehler?)"]}Hinweise in einer erfolgreichen Antwort:
### hinweise-now-und-waehrung
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "EZ",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"now": "{{gestern}}",
"currency": "USD",
"occupancy": {"travellers": [{"age": 40}]}
}{
"room": "EZ",
"currency": "EUR",
"rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
"totalCents": 12500,
"perTravellerCents": [12500],
"breakdown": [
{"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "70.00", "amountCents": 7000},
{"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "55.00", "amountCents": 5500}
],
"availability": {"configured": true, "available": true, "minFree": 5},
"warnings": [
"now: '{{gestern}}' ignoriert — Stichtag ist das Serverdatum {{heute}}",
"currency: angefragt 'USD', der Vertrag rechnet in 'EUR' (keine Umrechnung)"
]
}7.3 Bestand, Preis, Verkauf
- Code
ERR_HOTEL_NOT_FOUND- Status
- 404
- Bedeutung
- Hotel gibt es für diesen Key nicht (auch: fremder Veranstalter, nicht veröffentlicht, Sonderpreis der Kundengruppe nicht rechenbar – siehe 1.1)
- Aufrufer
- Anfrage korrigieren; bei einem sonst bekannten Hotel den Veranstalter informieren
- Code
ERR_ROOM_NOT_FOUND- Status
- 404
- Bedeutung
- Zimmer gibt es in diesem Hotel nicht (Termin-Matrix:
rooms[i]) - Aufrufer
- Anfrage korrigieren
- Code
ERR_BOOKING_NOT_FOUND- Status
- 404
- Bedeutung
- Buchung unbekannt oder für diesen Key nicht sichtbar
- Aufrufer
- Referenz/Key prüfen
- Code
ERR_REFERENCE_AMBIGUOUS- Status
- 409
- Bedeutung
- eigene
referencepasst zu mehreren Buchungen (/v1/booking);messagenennt dieTA-…-Referenzen (Test-Key:SB-…) - Aufrufer
- mit der TourAPI-Referenz lesen; eigene Referenzen eindeutig halten
- Code
ERR_TENANT_NOT_FOUND- Status
- 404
- Bedeutung
- Veranstalter nicht (mehr) aktiv, nur Verkauf/Storno
- Aufrufer
- Melden
- Code
ERR_BOARD_NOT_OFFERED- Status
- 422
- Bedeutung
- Verpflegung wird nicht angeboten (Code genau wie im Vertrag;
/v1/price,/v1/prices,priceCheck; offene Suche:boards[i]von keinem Hotel des Suchraums; Termin-Matrix:boards[i]nicht im Hotel oder keine Verpflegung passt zuboards/boardTypes); in der Suche Grund indiagnostics.reasonsbzw.coverage.reasons - Aufrufer
- Anfrage korrigieren
- Code
ERR_OCCUPANCY_NOT_ALLOWED- Status
- 422
- Bedeutung
- Belegung passt in kein Zimmer (bzw. nicht ins angefragte)
- Aufrufer
- andere Belegung/anderes Zimmer
- Code
ERR_STAY_LENGTH_NOT_ALLOWED- Status
- 422
- Bedeutung
- eine Verkaufsregel des Zimmers verlangt eine andere Aufenthaltsdauer (3.4); in der Suche Grund in
diagnostics.reasons - Aufrufer
- Dauer ändern
- Code
ERR_ARRIVAL_DAY_NOT_ALLOWED- Status
- 422
- Bedeutung
- eine Verkaufsregel verlangt einen anderen An-/Abreise-Wochentag (3.4)
- Aufrufer
- Reisetage verschieben
- Code
ERR_TRAVEL_DATES_NOT_ALLOWED- Status
- 422
- Bedeutung
- der Aufenthalt liegt außerhalb des Verkaufsfensters einer Regel (3.4)
- Aufrufer
- anderer Zeitraum
- Code
ERR_BOARD_NOT_ALLOWED- Status
- 422
- Bedeutung
- die Verpflegung wird für diesen Aufenthalt nicht verkauft (3.4)
- Aufrufer
- andere Verpflegung
- Code
ERR_LEAD_TIME_NOT_ALLOWED- Status
- 422
- Bedeutung
- die Anreise liegt innerhalb der Release-Frist einer Verkaufsregel, gezählt ab dem Stichtag (3.4)
- Aufrufer
- spätere Anreise
- Code
ERR_BOARD_NOT_AVAILABLE- Status
- 422
- Bedeutung
- die Verpflegung wird für diese Reisegruppe nicht verkauft, ihr Zuschlag verlangt eine andere Zusammensetzung (3.5); in
/v1/pricesEintrag inrooms[].errors[], in der Suche Grund indiagnostics.reasons - Aufrufer
- andere Verpflegung/Belegung
- Code
ERR_ROOM_RESTRICTION_INVALID- Status
- 422
- Bedeutung
- eine Verkaufsregel im Vertrag ist nicht auswertbar
- Aufrufer
- Melden
- Code
ERR_NO_SECTION- Status
- 422
- Bedeutung
- für eine Nacht gibt es keinen Preis (Saison nicht im Vertrag); rechnet ein anderes Zimmer, in
/v1/pricesEintrag inrooms[].errors[], bei/v1/priceohneroominwarnings, in der Suche indiagnostics.roomErrors - Aufrufer
- anderer Zeitraum
- Code
ERR_NO_PRICE- Status
- 422
- Bedeutung
- kein buchbares Zimmer für die Anfrage, ohne genaueren Grund
- Aufrufer
- anderer Zeitraum/andere Belegung
- Code
ERR_OCCUPANCY_NIGHT_UNCOVERED- Status
- 422
- Bedeutung
- Belegungsregeln des Vertrags decken eine Nacht nicht ab
- Aufrufer
- Melden
- Code
ERR_OCCUPANCY_INCONSISTENT_MCA- Status
- 422
- Bedeutung
- Mindestbelegung wechselt innerhalb des Aufenthalts (nicht unterstützt)
- Aufrufer
- kürzerer Zeitraum oder Melden
- Code
ERR_OCCUPANCY_INCONSISTENT_CHILDREN- Status
- 422
- Bedeutung
- ein Reisender ist innerhalb des Aufenthalts einmal Kind, einmal Erwachsener (das Kinderband des Zimmers wechselt mit der Saison; nicht unterstützt)
- Aufrufer
- kürzerer Zeitraum oder Melden
- Code
ERR_OCCUPANCY_INCONSISTENT_INFANTS- Status
- 422
- Bedeutung
- ein Säugling zählt innerhalb des Aufenthalts einmal zur Belegung, einmal nicht (die Belegungsregel des Zimmers wechselt mit der Saison), und ein Zu- oder Abschlag hängt an der Personenzahl; nicht eindeutig
- Aufrufer
- kürzerer Zeitraum oder Melden
- Code
ERR_CHILDREN_ORDER_MISSING- Status
- 422
- Bedeutung
- der Vertrag legt nicht fest, ob das älteste oder das jüngste Kind zuerst zählt, und für diese Kinder macht das einen Preisunterschied (Vertragsfehler)
- Aufrufer
- Melden
- Code
ERR_INVALID_AMOUNT- Status
- 422
- Bedeutung
- ein Betrag oder Prozentsatz im Vertrag ist nicht lesbar
- Aufrufer
- Melden
- Code
ERR_AMOUNT_OVERFLOW- Status
- 422
- Bedeutung
- der Preis überschreitet den darstellbaren Cent-Bereich (Vertragsfehler)
- Aufrufer
- Melden
- Code
ERR_CURRENCY_NOT_AVAILABLE- Status
- 422
- Bedeutung
priceCheck.currencypasst nicht zur Vertragswährung oder diese ist unbekannt; in der offenen Suche Grund incoverage.reasons(Hotel rechnet in anderer als der angefragten oder in unbekannter Währung); Termin-Matrix: dasselbe als 422- Aufrufer
- Anfrage korrigieren bzw. Melden
- Code
ERR_PRICE_DRIFT- Status
- 409
- Bedeutung
- aktueller Preis weicht vom
priceCheckab - Aufrufer
- neuen Preis anzeigen, mit neuem
expectedCentsbuchen
- Code
ERR_SOLD_OUT- Status
- 422
- Bedeutung
- Nacht ausgebucht
- Aufrufer
- nicht wiederholen
- Code
ERR_STOP_SALE- Status
- 422
- Bedeutung
- Verkaufsstopp
- Aufrufer
- nicht wiederholen
- Code
ERR_INVENTORY_CLOSED- Status
- 422
- Bedeutung
- Nacht geschlossen oder nur auf Anfrage
- Aufrufer
- nicht wiederholen
- Code
ERR_NO_INVENTORY- Status
- 422
- Bedeutung
- für mindestens eine Nacht ist keine Kapazität hinterlegt (Buchung und Suchgrund gleich)
- Aufrufer
- nicht wiederholen
- Code
ERR_NOT_AVAILABLE- Status
- –
- Bedeutung
- nur als Grund in der Suche: jede Nacht hat Kapazität, aber nicht jede ist offen (Buchung:
ERR_SOLD_OUT,ERR_STOP_SALE,ERR_INVENTORY_CLOSED) - Aufrufer
- –
- Code
ERR_OUTSIDE_PRICE_FILTER- Status
- –
- Bedeutung
- nur als Grund in der offenen Suche (jedes Angebot des Hotels) bzw. der Termin-Matrix (jedes Angebot der Zelle) liegt außerhalb von
minTotalCents/maxTotalCents - Aufrufer
- –
- Code
ERR_NO_CATEGORY- Status
- –
- Bedeutung
- nur als Grund (offene Suche; Termin-Matrix: jede Zelle des Hotels): Filter
category, das Hotel hat keine offizielle Kategorie im Hotelstamm - Aufrufer
- Veranstalter: Kategorie pflegen
- Code
ERR_NO_REGION- Status
- –
- Bedeutung
- nur als Grund (offene Suche; Termin-Matrix: jede Zelle des Hotels): Filter
regions, das Hotel hat keine Region im Hotelstamm - Aufrufer
- Veranstalter: Region pflegen
- Code
ERR_NO_GEO- Status
- –
- Bedeutung
- nur als Grund (offene Suche; Termin-Matrix: jede Zelle des Hotels): Filter
geo, das Hotel hat keine Koordinaten im Hotelstamm - Aufrufer
- Veranstalter: Koordinaten pflegen
- Code
ERR_GROUP_LIMIT- Status
- 422
- Bedeutung
- Zuteilung der Kundengruppe erschöpft oder für Zimmer und Nacht nicht vorhanden
- Aufrufer
- nicht wiederholen
- Code
ERR_IDEMPOTENCY_MISMATCH- Status
- 409
- Bedeutung
idemKeyschon mit anderen Buchungsdaten benutzt- Aufrufer
- Fehler im Aufrufer: eindeutige Keys vergeben
- Code
ERR_IDEM_KEY_RELEASED- Status
- 409
- Bedeutung
idemKeygehört zu einer stornierten Buchung- Aufrufer
- neuen
idemKeynehmen
- Code
ERR_SANDBOX_LIMIT- Status
- 422
- Bedeutung
- Test-Key: mehr als 5.000 offene Testbuchungen dieses Zugangs (Abschnitt 11.2)
- Aufrufer
- Testbuchungen stornieren
### fehler-verpflegung-nicht-angeboten
POST {{baseUrl}}/v1/prices
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"boards": ["AI"],
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}]}
}{"errorCode": "ERR_BOARD_NOT_OFFERED", "message": "boards: Verpflegung 'AI' wird nicht angeboten"}Dasselbe gilt für /v1/price – eine Verpflegung, die das Zimmer nicht anbietet, wird nie zum
Preis ohne Verpflegung gerechnet:
### fehler-verpflegung-preis
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"board": "AI",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}]}
}{"errorCode": "ERR_BOARD_NOT_OFFERED", "message": "board: Verpflegung 'AI' wird nicht angeboten"}7.4 Last und Betrieb
- Code
ERR_RATE_LIMITED- Status
- 429
- Bedeutung
- zu viele Anfragen dieses Keys (Abschnitt 8); offene Suche und Termin-Matrix: Takt des Suchprofils überschritten (beide zusammen)
- Aufrufer
- Wiederholen nach
Retry-After
- Code
ERR_SEARCH_BUSY- Status
- 429
- Bedeutung
- zu viele gleichzeitige Suchen des Veranstalters (offene Suche: auch des Keys laut Suchprofil)
- Aufrufer
- Wiederholen nach
Retry-After(1 s)
- Code
ERR_SEARCH_TIMEOUT- Status
- 503
- Bedeutung
- Suche hat die Zeitgrenze (10 s) überschritten
- Aufrufer
- eingrenzen (
destination/destinations, kleineres Fenster), dann wiederholen
- Code
ERR_PRICE_TIMEOUT- Status
- 503
- Bedeutung
/v1/priceohneroomoder/v1/priceshat die Zeitgrenze (2 s) überschritten- Aufrufer
- auf
/v1/pricemitroomausweichen (für/v1/pricesgilt die Frist immer, auch mitroomundboards), dann wiederholen
- Code
ERR_BOOKING_DISABLED- Status
- 503
- Bedeutung
- Verkauf/Storno auf diesem Knoten nicht eingeschaltet (Test-Key: Sandbox nicht eingeschaltet – ein Test-Key bucht nie live)
- Aufrufer
- Wiederholen, dauerhaft: Melden
- Code
ERR_BOOKING_BUSY- Status
- 503
- Bedeutung
- Buchung/Storno kam wegen gleichzeitiger Vorgänge am selben Hotel nicht durch, nichts gebucht bzw. storniert
- Aufrufer
- Wiederholen nach
Retry-After(1 s) mit demselbenidemKey
- Code
ERR_INTERNAL- Status
- 500
- Bedeutung
- interner Fehler, z. B. Datenbank nicht erreichbar (eine verletzte Invariante im Rechenkern kommt an den Auskunftsendpunkten als 422); offene Suche: Ergebnis nicht beweisbar (4a.4)
- Aufrufer
- Wiederholen mit Pause; bei
/v1/bookmit demselbenidemKey
- Code
ERR_INVENTORY_DRIFT- Status
- 500
- Bedeutung
- Kontingent-Invariante verletzt, nichts verkauft
- Aufrufer
- Melden
- Code
ERR_INVENTORY_STATUS_UNKNOWN- Status
- 500
- Bedeutung
- unbekannter Tagesstatus im Kontingent, nichts verkauft
- Aufrufer
- Melden
- Code
ERR_RELEASE_DRIFT- Status
- 500
- Bedeutung
- Storno wegen Kontingent-Invariante blockiert, nichts storniert
- Aufrufer
- Melden
7.5 Vertragsdaten (Fehler beim Veranstalter)
Diese Codes kommen aus dem Rechenkern, wenn der Vertrag des Hotels eine Regel enthält, die
TourAPI nicht (oder nicht so) rechnet. Sie sollten nach der Veröffentlichung nicht vorkommen,
weil der Vertrag vorher geprüft wird. Status immer 422. Aufrufer: Melden (mit Hotel,
Zeitraum und message); in der Suche erscheinen sie als Grund in diagnostics.reasons
(Hotel fällt weg) bzw. in diagnostics.roomErrors (einzelnes Zimmer ausgelassen). Bei
/v1/price ohne room wird ein betroffenes Zimmer ausgelassen und in warnings genannt,
solange ein anderes Zimmer rechnet; /v1/prices nennt die betroffenen Verpflegungen in
rooms[].errors[] und bepreist den Rest (Abschnitt 3.2). ERR_NEGATIVE_TRAVELLER_PRICE und
ERR_NEGATIVE_PERCENT_BASE hängen an Belegung und Verpflegung: Das Schreib-Gate warnt davor,
lässt den Vertrag aber zu; sie können darum auch nach der Veröffentlichung kommen. /v1/prices
lässt dann nur die betroffene Verpflegung aus (rooms[].errors[], Abschnitt 3.2).
- Code
ERR_NO_BASECHARGE- Bedeutung
- Grundpreis fehlt
- Code
ERR_SECTION_BAD_DATE,ERR_BOARD_BAD_DATE,ERR_OCCUPANCY_BAD_DATE- Bedeutung
- ungültiges Datum im Vertrag
- Code
ERR_OCCUPANCY_INCOMPLETE- Bedeutung
- Belegungsregel unvollständig
- Code
ERR_AMBIGUOUS_BOARDCHARGE- Bedeutung
- zwei Verpflegungszuschläge können dieselbe Person in derselben Nacht treffen; welcher gilt, ist nicht eindeutig (das Schreib-Gate lässt das nicht zu, nur Altbestand)
- Code
ERR_AMBIGUOUS_BASECHARGE,ERR_AMBIGUOUS_SECTION- Bedeutung
- zwei Grundpreise gleichen Typs in einer Saison bzw. zwei Saisons für denselben Tag; welcher Preis gilt, ist nicht eindeutig (das Schreib-Gate lässt das nicht zu, nur Altbestand)
- Code
ERR_AMBIGUOUS_FREENIGHT- Bedeutung
- zwei Freinacht-Angebote können für denselben Aufenthalt greifen; welche Nächte das zweite erlässt, ist nicht bestimmt (das Schreib-Gate lässt das nicht zu, nur Altbestand)
- Code
ERR_UNSUPPORTED_FREENIGHT,ERR_UNSUPPORTED_REDUCTION_MODE- Bedeutung
- Freinacht-Angebot in einer Form, die TourAPI nicht rechnet (z. B. fester Betrag statt Prozentsatz, Personen-Einschränkung, Nachtwahl „größer/kleiner als“)
- Code
ERR_NEGATIVE_TRAVELLER_PRICE- Bedeutung
- nach allen Zu- und Abschlägen würde ein Reisender weniger als 0 zahlen (z. B. fester Rabatt je Person auf ein Kind, das nichts kostet, oder gestapelte Rabatte über 100 %). Ein Reisender zahlt nie weniger als 0;
messagenennt Reisenden und Zu-/Abschlag. Prozent-Rabatte rechnen auf den Betrag, den der Reisende nach Kinder-/Personenermäßigung schuldet, und lösen den Fehler allein nicht aus
- Code
ERR_NEGATIVE_PERCENT_BASE- Bedeutung
- ein Kinder-/Personenabschlag (fester Betrag) ist größer als der Tagespreis bzw. die Verpflegung, auf die er wirkt; ein Prozent-Rabatt darauf würde zum Zuschlag, eine Freinacht (z. B. „7=6“) würde den Aufenthalt verteuern.
messagenennt Zu-/Abschlag bzw. Freinacht, Reisenden und Nacht
- Code
ERR_UNSUPPORTED_OCCUPANCY_PRICEBLOCK- Bedeutung
- Grundpreis je Belegung (aus einer fremden Lieferung, z. B. „genau 1 Person“ / „ab 2 Personen“) in einer Form, die TourAPI nicht rechnet — oder ein Säugling reist in einem solchen Zimmer (die liefernde Stelle verkauft es mit Säugling nicht)
- Code
ERR_UNSUPPORTED_GUESTCHARGE_OBJECT- Bedeutung
- Personenermäßigung (z. B. Kinderrabatt) auf einen Zimmerpreis (Objektpreis): es gibt keinen Preis je Person, auf den sie wirken könnte (das Schreib-Gate lässt das nicht zu, nur Altbestand)
- Code
ERR_UNSUPPORTED_COMBIGROUP- Bedeutung
- ein Angebot ist in Kombinationsgruppe 0 exklusiv; Gruppe 0 steht im EDF auch für alle Angebote ohne Gruppe, ein EDF-Empfänger rechnete anders (das Schreib-Gate lässt das nicht zu, nur Altbestand)
- Code
ERR_COMPATIBLE_WITH_INVALID- Bedeutung
- die Liste „kombinierbar nur mit Gruppen“ eines Angebots ist nicht eindeutig: Gruppe doppelt, außerhalb 0..2147483647 oder zusammen mit „nur eins je Gruppe“ (das Schreib-Gate lässt das nicht zu, nur Altbestand)
- Code
ERR_CALCMODE_MISSING,ERR_CALCMODE_UNSUPPORTED- Bedeutung
- Rechenart des Zimmers fehlt bzw. nicht unterstützt
- Code
ERR_BASE_BOARD_INVALID,ERR_BASE_BOARD_CHARGED- Bedeutung
- Basis-Board des Zimmers (Verpflegung im Grundpreis) ungültig bzw. mit eigenem Zuschlag (das Schreib-Gate lässt das nicht zu, nur Altbestand)
- Code
ERR_MINCHARGEDPERSONS_MISSING,ERR_INVALID_MIN_CHARGED_PERSONS- Bedeutung
- Mindestzahl zahlender Personen fehlt bzw. ungültig
- Code
ERR_INVALID_ENUM,ERR_INVALID_WEEKDAY_MASK- Bedeutung
- ungültiger Aufzählungswert bzw. Wochentagsmaske
- Code
ERR_MISSING_APPLIANCE_TYPE,ERR_MISSING_AMOUNT_OR_PERCENT,ERR_MISSING_BOARD_CODE,ERR_AMOUNT_PERCENT_CONFLICT,ERR_DUPLICATE_APPLIANCE_CODE,ERR_EXTRA_TYPE_INVALID- Bedeutung
- Zu- oder Abschlag unvollständig oder widersprüchlich
- Code
ERR_EXTRACALC_UNSUPPORTED,ERR_UNSUPPORTED_APPLIANCE_TYPE,ERR_UNSUPPORTED_APPLYTOBOARD,ERR_UNSUPPORTED_EXTRA_REF,ERR_UNSUPPORTED_GUESTCHARGE_LINK,ERR_UNSUPPORTED_GUESTCHARGE_TYPE,ERR_UNSUPPORTED_INVERT,ERR_UNSUPPORTED_MANDATORY,ERR_UNSUPPORTED_MCA_AGEWINDOW,ERR_UNSUPPORTED_MCA_RANGE,ERR_UNSUPPORTED_RESTRICTION,ERR_UNSUPPORTED_VARMCP_PERSTAY- Bedeutung
- Vertragsregel, die TourAPI nicht rechnet
7.6 EDF-Lieferung (/v1/export/edf/*, Abschnitt 10)
- Code
ERR_EXPORT_BAD_CURSOR- Status
- 400
- Bedeutung
epoch/sincefehlt oder unlesbar,untilkein gültiger Kettenschlüssel,max_byteskeine Zahl ≥ 65536, Parameter doppelt, Seite mitten in einem Stand ohneuntil, Folgeseite desfullohneepoch/since/until- Aufrufer
- Anfrage korrigieren bzw. Kette neu beginnen
- Code
ERR_EXPORT_NOT_ALLOWED- Status
- 403
- Bedeutung
- Key ohne Export-Recht
- Aufrufer
- Veranstalter fragen, nicht wiederholen
- Code
ERR_EXPORT_EPOCH- Status
- 409
- Bedeutung
epochpasst nicht (Feed neu aufgebaut) oder der Stand (since,until,seqder Quittung) liegt über dem aktuellen Lieferstand (größer als der letzte gelieferte Stand)- Aufrufer
fullabrufen
- Code
ERR_EXPORT_CURSOR_EXPIRED- Status
- 410
- Bedeutung
- Stand älter als die Aufbewahrung
- Aufrufer
fullabrufen
- Code
ERR_EXPORT_NOT_READY- Status
- 503
- Bedeutung
- Lieferung für diesen Key noch nicht gebaut, vorübergehend nicht aktuell (Lieferung hinkt mehr als 2 Minuten hinterher) bzw. auf dem Knoten nicht eingerichtet
- Aufrufer
- Wiederholen nach
Retry-After; dauerhaft: Melden
Außerdem an /v1/export/edf/*: 401/403 aus 7.1 (ERR_TENANT_SUSPENDED,
ERR_KEY_GROUP_INACTIVE), 422 ERR_UNKNOWN_FIELD (unbekannter Parameter bzw. Feld der Quittung)
und 429 ERR_RATE_LIMITED für den Takt (10.3; Retry-After bis 3600 s beim full – nicht
blind abwarten, sondern den nächsten Takt planen).
7.7 Hotelinhalte (/v1/content/*, Abschnitt 12)
- Code
ERR_CONTENT_NOT_ALLOWED- Status
- 403
- Bedeutung
- Inhalte für den Veranstalter nicht freigeschaltet (auch: Testumgebung auf dieser Installation nicht beliefert) oder Key ohne Inhalts-Recht
- Aufrufer
- Veranstalter fragen, nicht wiederholen
- Code
ERR_LANGUAGE_NOT_OFFERED- Status
- 422
- Bedeutung
langnennt eine Sprache, die keine Inhaltssprache des Veranstalters ist (bzw. am Katalog keine Beschriftungssprache); die angebotenen stehen inwarnings- Aufrufer
- Anfrage korrigieren
- Code
ERR_CONTENT_CURSOR_EXPIRED- Status
- 410
- Bedeutung
sinceliegt vor dem Aufbewahrungshorizont des Feeds (30 Tage)- Aufrufer
- Verzeichnis neu holen, mit dessen
feedTokenweiter
- Code
ERR_CONTENT_NOT_READY- Status
- 503
- Bedeutung
- Inhalte bzw. Bild-Adressen auf diesem Knoten nicht eingerichtet
- Aufrufer
- Wiederholen nach
Retry-After; dauerhaft: Melden
Außerdem an /v1/content/*: 401/403 aus 7.1, 404 ERR_HOTEL_NOT_FOUND (Hotel nicht im
Verzeichnis des Keys), 400 ERR_BAD_REQUEST, 422 ERR_BAD_PAGE_SIZE, ERR_BAD_CURSOR
und 429 ERR_RATE_LIMITED (Rate je Key, Abschnitt 8). Unbekannte
Query-Parameter werden ignoriert und in warnings genannt.
8. Fairness: Rate-Limit und Such-Gate
| Riegel | Grenze (Voreinstellung) | Antwort |
|---|---|---|
| Anfragen je API-Key | 200 je Sekunde, kurzzeitig bis 400 (Token-Bucket) | 429 ERR_RATE_LIMITED + Retry-After |
| gleichzeitige Suchen je Veranstalter | 8 (Rechenzeit: ein Viertel der Kerne, mind. 1) | 429 ERR_SEARCH_BUSY + Retry-After: 1 |
| Rechenzeit einer Suche | 10 s | 503 ERR_SEARCH_TIMEOUT |
Rechenzeit von /v1/price ohne room und /v1/prices | 2 s | 503 ERR_PRICE_TIMEOUT |
- Die Grenzen gelten je Server-Knoten. Sie sind ein Schutz gegen Schleifen und Lastspitzen, keine abrechenbare Quote. Der Betreiber kann sie ändern.
Retry-Afterist in ganzen Sekunden (mindestens 1). Vorher nicht wiederholen; danach mit derselben Anfrage (bei/v1/bookmit demselbenidemKey).- Wer vor Ablauf von
Retry-Aftererneut schickt und wieder abgewiesen wird, bekommt die429erst verzögert (bis zum Ende der angesagten Wartezeit, höchstens 1 s). Die Wartezeit gilt je API-Key: arbeiten mehrere Prozesse mit demselben Key parallel, trifft die Verzögerung jeden, der nach einer429weiterschickt, egal mit wie vielen Verbindungen. Die Verbindung bleibt offen. - Abgewiesene Keys (
401,403 ERR_TENANT_SUSPENDED,403 ERR_KEY_GROUP_INACTIVE) zählen je Absender-IP: nach 20 Abweisungen in kurzer Folge kommen nur noch 5 je Sekunde sofort, weitere erst verzögert (höchstens 1 s), egal mit wie vielen Verbindungen. Status und Antwort bleiben gleich. Anfragen mit gültigem Key sind davon nie betroffen. - Unbekannte Pfade (
404) und falsche Methoden (405 ERR_METHOD_NOT_ALLOWED) zählen in dieselbe Grenze je Absender-IP wie abgewiesene Keys. /v1/healthhat eine eigene Grenze je Absender-IP: 50 Aufrufe sofort, danach 10 je Sekunde sofort, weitere erst verzögert (höchstens 1 s). Die Antwort ist immer der aktuelle Zustand, nie eine Abweisung.- Abgewiesene Anfragen zählen nicht als Nutzung.
- Gekappte Suchseiten sind kein Fehler, siehe 4.3.
So sieht die Drosselung aus (die Beispiele laufen gegen eine Instanz mit 1 Anfrage je 10 s):
### drossel-erste-anfrage
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "EZ",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}]}
}### drossel-zweite-anfrage
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}
{
"hotel": "TEST-HOTEL-BASE",
"room": "EZ",
"board": "RO",
"checkIn": "{{D0}}",
"checkOut": "{{D2}}",
"occupancy": {"travellers": [{"age": 40}]}
}{"errorCode": "ERR_RATE_LIMITED", "message": "Anfrage-Rate dieses API-Keys ueberschritten"}Kopfzeile dazu: Retry-After: 10.
9. Idempotenz und Nebenläufigkeit
Buchung:
idemKeyist Pflicht und gehört dem Aufrufer. Er gilt je Veranstalter und Key-Kreis (Kundengruppe bzw. Basisvertrag): Zwei Kundengruppen können denselben Key benutzen, ohne sich zu stören.- Gleicher
idemKey, gleiche Buchungsdaten (Hotel, Zimmer, Aufenthalt, Menge) = dieselbe Buchung:200,alreadyBooked: true, gleiche Referenz.reference,metadata,leadPaxNameundpriceCheckeiner Wiederholung werden nicht verglichen und nicht übernommen – es gilt die erste Buchung. - Gleicher
idemKey, andere Buchungsdaten:409 ERR_IDEMPOTENCY_MISMATCH. - Stornierter
idemKey:409 ERR_IDEM_KEY_RELEASED. - Die Idempotenzprüfung kommt vor Stichtag, Grenzen und
priceCheck: Besteht die Buchung, bestätigt die Wiederholung sie auch nach Mitternacht (Anreise inzwischen Vergangenheit,priceCheck.nownicht mehr der Stichtag) oder nach einer Preisänderung. Nur ein neueridemKeydurchläuft diese Prüfungen. Form-Fehler der Anfrage (unbekannte Felder, fehlende Pflichtfelder, zu lange Texte) werden weiterhin vorher abgelehnt. - Läuft die erste Buchung eines
idemKeynoch, wartet eine Wiederholung auf deren Ausgang und bestätigt sie dann (200,alreadyBooked: true); scheitert die erste, läuft die Wiederholung als neue Buchung. Dauert das Warten zu lange:503 ERR_BOOKING_BUSYmitRetry-After– mit demselbenidemKeywiederholen. - Zwei gleichzeitige Buchungen desselben Zimmers können das Kontingent nie überbuchen: Jede Nacht wird atomar gezählt, und eine Buchung nimmt alle Nächte oder keine.
- Kommen Buchung oder Storno wegen gleichzeitiger Vorgänge am selben Hotel nicht durch:
503 ERR_BOOKING_BUSYmitRetry-After(Abschnitt 7.4). Es ist nichts gebucht bzw. storniert; nach der Wartezeit mit demselbenidemKeywiederholen. - Wird ein Hotel gelöscht, während eine Buchung dafür läuft, gilt die Reihenfolge: war die
Buchung zuerst, bleibt sie bestehen; sonst
404 ERR_HOTEL_NOT_FOUND, nichts gebucht. - Nach Zeitüberschreitung oder
5xxist unklar, ob gebucht wurde: mit demselbenidemKeywiederholen, nie mit einem neuen.
Storno:
- Adressiert über den
idemKeyder Buchung. Doppelstorno ist ein Erfolg (alreadyReleased: true). Gleichzeitige Stornos derselben Buchung geben das Kontingent genau einmal zurück.
10. EDF-Lieferung (Cache-Export)
Zweck: Ein Abnehmer hält die Preise und Verfügbarkeiten aller Hotels seines Keys als
EDF-Dateien im eigenen Cache und fragt vor der Buchung live nach (/v1/price, dann
/v1/book). Verbindlich ist immer /v1/book; der Cache ist ein Angebot, kein Bestand.
Den vollständigen Liefervertrag (kanonische Form des Manifests, Grenzen) gibt es auf Anfrage
beim Betreiber.
| Methode/Pfad | Antwort |
|---|---|
GET /v1/export/edf/full[?max_bytes=N] | 200 Zip mit dem Vollstand (bei großen Beständen die erste Seite) |
GET /v1/export/edf/full?epoch=E&since=S&until=K[&max_bytes=N] | 200 Zip mit der nächsten Seite des Vollstands |
GET /v1/export/edf/changes?epoch=E&since=S[&until=K][&max_bytes=N] | 200 Zip mit allen Änderungen nach Stand S; 204, wenn nichts neu ist |
POST /v1/export/edf/ack {"epoch": E, "seq": T} | 204; meldet „verarbeitet“ (nur für die Überwachung beim Veranstalter) |
- Zuschnitt nur über den Key: Der Key bestimmt, was geliefert wird – dieselben Hotels
und Preise wie
/v1/searchund/v1/pricefür diesen Key (Basisvertrag, Kundengruppe mit ihrem Preis, Kontingent-Gruppe nur Hotels mit Zuteilung). Es gibt keinen Parameter, der Veranstalter oder Gruppe wählt; ein unbekannter Parameter ist422 ERR_UNKNOWN_FIELD. - Export-Recht: je Key freizuschalten (Veranstalter-Admin, in der Konsole an der
Key-Zeile „Export erteilen“); ohne Recht
403 ERR_EXPORT_NOT_ALLOWED. - Nur Veröffentlichtes: Entwürfe ändern die Lieferung nie. Änderungen (Veröffentlichung, Stop-Sale, Buchung, Aktion, Zuteilung) stehen spätestens nach etwa einer Minute im Feed.
10.1 Paket
Ein Zip mit manifest.json als erstem Eintrag, dann je Hotel eine Preisdatei
(hotels/hotelonly/EDF----<tenant>-<hotel>.xml, EDF 5.1.6) und eine Allotment-Datei
(hotels/hotelonly/allotment/EDF----<tenant>-<hotel>.xml, HotelAllotmentRoot 1.012).
Identität aus dem Manifest bzw. BasicData, nie aus dem Dateinamen (Codes dürfen -
enthalten). Jede Datei hat ihre sha256-Prüfsumme und Länge im Manifest; ein Paket wird ganz
oder gar nicht angewendet.
Ein Eintrag in objects und einer in removed:
{"path": "hotels/hotelonly/EDF----TEST-TENANT-A-TEST-HOTEL-BASE.xml", "kind": "hotel", "hotel": "TEST-HOTEL-BASE", "seq": 3, "sha256": "9f2c…", "bytes": 2210, "source_rev": 4}{"kind": "hotel", "hotel": "TEST-HOTEL-ALTAKTION", "seq": 5, "reason": "withdrawn:variant_error"}- Tombstones: Zurückgezogene Hotels stehen unter
removedmit Grund (withdrawn:deleted,withdrawn:variant_error,withdrawn:not_exportable,withdrawn:no_currency,withdrawn:not_in_universe). Der Abnehmer löscht sie aus seinem Cache. Einfullhat immerremoved: []– er ersetzt den Cache ganz. - Pattern der Allotment-Datei: zwei Zeichen je Nacht (
PatternLength="2", wie die MTS-Lieferungen):00–99= freie Einheiten für diesen Key,**= mehr als 99 frei oder Freiverkauf,SS= Stop-Sale,RR= auf Anfrage; eine Nacht ohne Kapazität ist00. Fehlt eine Allotment-Datei, gilt das Hotel als nicht verfügbar. Aufbau je Zimmer:<Allotments RoomCode="DZ"><Allotment Start="JJJJ-MM-TT" End="JJJJ-MM-TT" Pattern="…"/>; die ersten zwei Zeichen vonPatterngehören zur NachtStart, jedes weitere Paar zur Folgenacht. - Pattern und
minFree:SS,RRund00ergeben ausdrücklichminFree0(nicht buchbar); nur**senktminFreenicht, und-1heißt: jede Nacht**. Das Minimum über die Nächte eines Aufenthalts istavailability.minFreevon/v1/pricefür denselben Key (Tabelle in Abschnitt 3.3) – beide kommen aus derselben Quelle je Nacht.
10.2 Vollstand, Änderungen, Quittung
Beim Start und nach 409/410 holt der Abnehmer den Vollstand. Aus dem Manifest merkt er
epoch und to_seq. Passt der Vollstand nicht in ein Paket (mehr als 10.000 Dateien, mehr als
1 GiB entpackt oder mehr als max_bytes), kommt er in Seiten, siehe unten „Seiten“:
### export-voll
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{apiKey}}{
"format": "tourapi-edf-feed/1",
"tenant": "TEST-TENANT-A",
"scope": "",
"epoch": "{{exportEpoch}}",
"type": "full",
"from_seq": 0,
"to_seq": "{{*}}",
"more": false,
"generated_at": "{{*}}",
"rules": {"edf": "5.1.6", "allotment": "1.012", "spec": ""},
"objects": "{{*}}",
"removed": []
}Kopfzeilen jeder 200/204-Antwort: X-Export-Epoch, X-Export-Seq (letzter
vollständiger Stand), X-Export-From (Stand, ab dem geliefert wurde), X-Export-More;
bei einem changes-Paket und bei einer full-Seite mit more: true zusätzlich
X-Export-Until (Kettenschlüssel, siehe unten). Endet eine Seite mitten in einem Stand (to_after
im Manifest), nennt X-Export-Seq den Stand davor – auf den ersten Seiten eines full also
0. Maßgeblich für since, Anwenden und Quittung sind to_seq/to_after aus dem Manifest. Ein Key einer
Kundengruppe bekommt den Stand seiner Gruppe (scope), mit deren Preisen:
### export-partner
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{rabattKey}}Danach fragt er im Takt nach Änderungen. since ist das to_seq des letzten angewendeten
Pakets; steht es schon auf dem aktuellen Stand, kommt 204 ohne Inhalt:
### export-nichts-neu
GET {{baseUrl}}/v1/export/edf/changes?epoch={{exportEpoch}}&since={{exportSeq}}
X-Api-Key: {{apiKey}}Seiten (max_bytes): full und changes sind Seitenketten. Eine Seite hat höchstens
10.000 Dateien und 1 GiB entpackt; mit max_bytes=N schneidet der Server zusätzlich Seiten von
höchstens N Bytes (mindestens eine Datei je Seite); N ist mindestens 65536 (64 KiB), kleiner
ist 400 ERR_EXPORT_BAD_CURSOR. Die erste Seite (full ohne Parameter bzw. changes ohne
until) legt das Ziel der Kette fest (den aktuellen Stand) und gibt es als
Kettenschlüssel in X-Export-Until aus (beim full nur, wenn more: true): eine
undurchsichtige, versiegelte Zeichenkette (u1.… bei changes, f1.… bei full), gebunden
an Key und epoch. Folgeseiten fragt der Abnehmer mit since=<to_seq> bzw.
since=<to_seq>:<to_after> (wenn das Manifest to_after trägt) und
until=<X-Export-Until> (Wert der Vorseite, URL-kodiert, unverändert), beim full zusätzlich mit
epoch=<epoch> (Wert der ersten Seite), bis more false ist. Jede Seite gibt den Schlüssel frisch
aus; er gilt 15 Minuten nach der letzten Seite, beim full nur für genau den Stand der
nächsten Seite. Ein selbst gewähltes until (Zahl), ein fremder oder abgelaufener Schlüssel ist
400 ERR_EXPORT_BAD_CURSOR – dann die Kette neu beginnen. Eine full-Seite trägt nie
removed; die erste beginnt bei from_seq: 0, jede weitere beim to der Vorseite. Erst am
Kettenende ist der Stand konsistent: angewendet (getauscht) wird dort, auch wenn der
Veranstalter zwischendurch weiter ändert – die Kette endet genau auf ihrem Ziel.
Abbruch und Wiederholen: Scheitert ein Paket, nachdem 200 schon gesendet ist (etwa weil
eine Datei beim Veranstalter inzwischen fehlt), bricht der Server die Verbindung ab. Der
Abnehmer sieht dann einen Transportfehler (unexpected EOF, Verbindung zurückgesetzt), nie
ein sauber beendetes, verkürztes Paket. Trotzdem muss er jedes Paket vor dem Anwenden
gegen sein Manifest prüfen: jede Datei aus objects vorhanden, Länge (bytes) und sha256
stimmen, keine Datei zu viel. Ein Streaming-Leser (z. B. Javas ZipInputStream) erkennt ein
Zip, das an einer Eintragsgrenze endet, nicht von selbst als unvollständig. Eine Folgeseite,
die kurz scheitert (Transportfehler, abgebrochenes oder unlesbares Paket, 5xx), fragt er mit
denselben epoch/since/until noch einmal an (der Schlüssel gilt 15 Minuten, 503 nach
Retry-After), statt die Kette neu zu beginnen – ein neuer Anfang kostet den Takt (full:
eine Stunde). Nur nach 400 beginnt er die Kette neu, nach 409/410 holt er full. Bricht
eine Kette ab, bleibt der alte Stand.
Nach dem Anwenden quittiert der Abnehmer den Stand. Die Quittung ist freiwillig und ändert nichts an der Lieferung; der Veranstalter sieht daran, welchen Stand der Abnehmer verarbeitet hat:
### export-quittung
POST {{baseUrl}}/v1/export/edf/ack
Content-Type: application/json
X-Api-Key: {{apiKey}}
{"epoch": "{{exportEpoch}}", "seq": {{exportSeq}}}10.3 Fehler und Takt
- Status
- 400
- Code
ERR_EXPORT_BAD_CURSOR- Wann
epoch/sincefehlt oder unlesbar,untilkein gültiger Kettenschlüssel (fremd, abgelaufen, Zahl, beimfullfür einen anderen Stand),max_byteskeine Zahl ≥ 65536, Seite mitten in einem Stand ohneuntil, Stand über dem Kettenziel, Folgeseite desfullohneepoch/since/until- Abnehmer tut
- Anfrage korrigieren bzw. Kette neu beginnen
- Status
- 403
- Code
ERR_EXPORT_NOT_ALLOWED- Wann
- Key ohne Export-Recht
- Abnehmer tut
- Veranstalter fragen
- Status
- 403
- Code
ERR_KEY_GROUP_INACTIVE- Wann
- Kundengruppe des Keys deaktiviert (nie still der Basisvertrag)
- Abnehmer tut
- Veranstalter fragen
- Status
- 409
- Code
ERR_EXPORT_EPOCH- Wann
epochpasst nicht (Feed neu aufgebaut) odersince/untilbzw.seqder Quittung über dem aktuellen Lieferstand (größer als der letzte gelieferte Stand; ein älteressinceist erlaubt und liefert alles danach)- Abnehmer tut
full
- Status
- 410
- Code
ERR_EXPORT_CURSOR_EXPIRED- Wann
- Stand älter als die Aufbewahrung (14 Tage)
- Abnehmer tut
full
- Status
- 429
- Code
ERR_RATE_LIMITED- Wann
- Takt überschritten, siehe unten
- Abnehmer tut
- nach
Retry-After
- Status
- 503
- Code
ERR_EXPORT_NOT_READY- Wann
- Lieferung für diesen Key noch nie gebaut (neuer Key, neue Gruppe), oder die Lieferung ist vorübergehend nicht aktuell (sie hinkt mehr als 2 Minuten hinter den Daten her)
- Abnehmer tut
- nach
Retry-After, alter Stand bleibt gültig
### export-stand-unlesbar
GET {{baseUrl}}/v1/export/edf/changes?epoch={{exportEpoch}}&since=gestern
X-Api-Key: {{apiKey}}### export-fremder-stand
GET {{baseUrl}}/v1/export/edf/changes?epoch=01J00000000000000000000000&since=1
X-Api-Key: {{partnerKey}}{"errorCode": "ERR_EXPORT_EPOCH", "message": "epoch veraltet (Feed neu aufgebaut) oder Stand neuer als der aktuelle Lieferstand — full abrufen"}### export-ohne-recht
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{poolKey}}{"errorCode": "ERR_EXPORT_NOT_ALLOWED", "message": "dieser API-Key hat kein Export-Recht (EDF-Lieferung) — der Veranstalter-Admin schaltet es frei"}Takt je Key: ein neuer full höchstens einmal je Stunde, eine neue changes-Kette
höchstens einmal je Minute. Folgeseiten einer Kette (full oder changes, mit dem
Kettenschlüssel) und Quittungen haben einen eigenen, großzügigen Takt (5 je Sekunde, 50 auf
einmal). Welcher Abruf eines Kettenanfangs den Takt verbraucht:
- Antwort
400 ERR_EXPORT_BAD_CURSOR,422 ERR_UNKNOWN_FIELD(ungültige Anfrage, vor jeder Arbeit geprüft)- verbraucht den Takt
- nein
- Antwort
503 ERR_EXPORT_NOT_READY(Lieferung noch nie gebaut oder vorübergehend nicht aktuell)- verbraucht den Takt
- nein – wer
Retry-Afterbefolgt, bekommt kein 429
- Antwort
200(auch ein abgebrochener Download),204(„nichts neu“),409 ERR_EXPORT_EPOCH- verbraucht den Takt
- ja
Die Takt-Prüfung kommt vor der Prüfung von epoch und Stand. Ein Abnehmer mit veralteter
epoch sieht innerhalb des Takts zuerst 429, erst nach dessen Retry-After das 409.
Beispielablauf changes (gemessen, Takt 1 min):
- Zeit
- 0 s
- Anfrage
changes?epoch=E&since=S&foo=1(Tippfehler)- Antwort
422 ERR_UNKNOWN_FIELD- Abnehmer tut
- korrigieren; Takt nicht verbraucht
- Zeit
- 0 s
- Anfrage
changes?epoch=E&since=S- Antwort
204- Abnehmer tut
- Takt verbraucht; nächste Kette frühestens in 60 s
- Zeit
- 0 s
- Anfrage
changes?epoch=E&since=S, der Feed wurde inzwischen neu aufgebaut (Everaltet)- Antwort
429 ERR_RATE_LIMITED,Retry-After: 60- Abnehmer tut
Retry-Afterabwarten
- Zeit
- 60 s
- Anfrage
- derselbe Abruf
- Antwort
409 ERR_EXPORT_EPOCH- Abnehmer tut
fullholen (eigener Takt, 1 je Stunde)
Darüber hinaus gilt das Rate-Limit des Keys (Abschnitt 8). Ein zweites full in derselben Stunde:
### export-zu-oft
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{apiKey}}{"errorCode": "ERR_RATE_LIMITED", "message": "full hoechstens einmal je 1 h je API-Key (danach changes)"}10.4 Anwenden und aus dem Cache rechnen (Referenz-Empfänger)
TourAPI hat einen Referenz-Empfänger, der genau diese Regeln umsetzt und bei jedem Build
gegen die echte API geprüft wird: das Werkzeug edf-empfaenger (auf Anfrage beim Betreiber)
(pull, apply, stand, rechne, reset; der Key kommt nur aus der Umgebung
EDF_EMPFAENGER_KEY). Was er aus der Lieferung rechnet, ist für jede Anfrage der Prüfmenge
centgenau /v1/price desselben Keys – Gesamtpreis, Preis je Reisendem, gewähltes Zimmer,
available und minFree, bei Ablehnung derselbe Code; das gilt auch für ungültige und
mehrfach ungültige Anfragen (welche Grenze zuerst greift). Referenz-Empfänger und
make e2e-export liegen im Quellbaum von TourAPI und gehören nicht zur Lieferung: Sie als
Kunde bekommen Key und Zugangspaket und setzen die Regeln dieses Abschnitts in Ihrem System
um – der Referenz-Empfänger ist der Beleg, dass sie aufgehen.
EDF_EMPFAENGER_KEY=… edf-empfaenger pull --dir <bestand> --api <baseUrl> [--max-bytes N] [--ohne-ack]
edf-empfaenger stand --dir <bestand>
edf-empfaenger rechne --dir <bestand> --anfragen anfragen.json [--heute JJJJ-MM-TT]anfragen.json ist eine Liste; je Anfrage hotel, room (optional, fehlt = günstigstes
verfügbares Zimmer), board, checkIn, checkOut und die Alter der Reisenden als ages
(nicht occupancy.travellers wie bei /v1/price), z. B.
[{"hotel": "TEST-HOTEL-BASE", "room": "DZ", "board": "RO", "checkIn": "2026-10-27",
"checkOut": "2026-10-29", "ages": [40, 38]}]. Die Antwort nennt je Anfrage anfrage (Index),
room, currency, totalCents, perTravellerCents, availability (available, minFree)
bzw. errorCode.
- Ganz oder gar nicht: Paket prüfen (sha256 und Länge je Datei), den neuen Stand
vollständig neben dem alten aufbauen, dann atomar umschalten; erst danach gelten
epochundto_seqals gespeichert. Eine Kette mit Seiten (fullwiechanges) wird erst am Kettenende (more: false) umgeschaltet; bricht sie ab, bleibt der alte Stand. Einefull-Kette beginnt immer mitfrom_seq: 0; einefull-Folgeseite schließt nur an die offene Kette an, nie an einen gespeicherten Stand – auch wenn ihrfrom_seqgleich dem gespeichertento_seqist (einfullträgt keineremoved, gelöschte Hotels blieben sonst stehen). - Nie rückwärts: Maßgeblich ist das Ziel der Kette, also
to_seqder letzten Seite (more: false), nicht das der ersten. Die erste Seite einerfull-Kette trägt oft ein kleineresto_seqals der gespeicherte Stand (etwa nach410: die ältesten Dateien kommen zuerst) und ist trotzdem kein Rückschritt. Einefull-Kette derselbenepoch, deren Ziel kleiner ist als der gespeicherte Stand, oder einfulleiner älterenepoch(dieepochist eine ULID, zeitlich sortiert) wird abgelehnt – ein verspätet zugestelltes altes Paket setzt den Cache nicht zurück.changesmüssen lückenlos an den Stand anschließen (from_seq= gespeichertesto_seq). Einfulleines anderen Mandanten oder Scopes (vertauschter Key) wird ebenfalls abgelehnt, als Scope-Fehler vor der Rückwärtsprüfung. - Abgelehntes
full: Der alte Stand bleibt, der Cache rechnet weiter darauf, holt aber nichts Neues mehr. Das ist gewollt laut – nie still zurückspringen. Liegt kein vertauschter Key vor und steht die Lieferung wirklich auf einer älterenepoch(etwa nach einem Restore auf einem Rechner, dessen Uhr nachgeht: die neueepoch-ULID ist dann kleiner) oder einem kleinerento_seq, setzen Sie den Stand bewusst zurück (Referenz-Empfänger:edf-empfaenger reset) und holen ein neuesfull. Im Zweifel vorher beim Betreiber nachfragen. - Lesen während des Abrufs: Ein Abruf darf dauern (große Kette,
429-Warten). Preise aus dem Cache laufen währenddessen weiter auf dem alten Stand und nach dem Umschalten auf dem neuen; den alten Stand erst löschen, wenn niemand mehr in ihm liest. Der Referenz-Empfänger erlaubt einen Schreiber (pull,apply,reset) und beliebig viele Leser (stand,rechne) je Bestand.429wartet er je Antwort höchstens--max-warten, je Abruf zusammen höchstens--max-warten-gesamt, dann bricht er ab (Stand unverändert). - Wiederholen statt neu beginnen: Eine Folgeseite mit kurzem Fehler holt der Referenz-Empfänger mit denselben Parametern bis zu dreimal (Regel in 10.2); den Kettenanfang wiederholt er nicht.
- Preis aus dem Cache: EDF 5.1.6 mit den deklarierten Regeln, dazu die Regeln der API: dieselben Anfrage-Grenzen (Anhang, gleiche Fehlercodes; Stichtag = heute in Europe/Berlin); mit Zimmer das günstigste Angebot dieses Zimmercodes, ohne Zimmer das günstigste verfügbare Zimmer, ist keines verfügbar das günstigste bepreisbare (wie Abschnitt 3.1); ein Zimmer, das die Verpflegung nicht anbietet, wird nicht bepreist.
- Deklarierte Regeln: Jede Hotel-Datei nennt in
SellingDatadie Kinderreihenfolge (ChildrenAgeOrder,Descending= ältestes Kind zuerst: wer ist „1. Kind“, welches Kind besetzt eine freie Vollzahler-Stelle),FullPayerDoesNotAffectBoardCharges/PersonType=C(ein Kind auf der Vollzahler-Stelle zahlt den vollen Grundpreis, die Verpflegung zum Kindersatz) undRounding Mode="Commercial" DecimalPlace="2" Scope="Person"(eine Rundung je Reisendem, Abschnitt 1.4). Wo das Schema Spielraum lässt, rechnet TourAPI so – der Cache muss es ebenso tun, sonst weicht er ab:MinCount/MaxCountzählen Kinder je Personenart ab dem ersten Kind, Erwachsene ab dem ersten nach den Vollzahlern; ein Säugling zählt in einer Personenbedingung („ab 3 Personen“) nur, wenn das Zimmer ihn zur Belegung zählt (Infants/@ApplyToOccupancyMin,MaxoderYes); ein fehlendesExtraMaxApplyheißt 1; mehrere Datumsfenster mitOperator="AND"gelten im Schnitt; Prozent-Zu- und -Abschläge neben einer Freinacht rechnen nur auf die nicht erlassenen Nächte; ein fester Betrag je Person (PS,PN) wirkt auch beim Objektpreis je Person; Zimmerposten (Objektpreis, unbesetzte Vollzahler-Stelle) gehören zum ältesten Reisenden und werden mit ihm gerundet. - Verfügbarkeit aus dem Pattern: Minimum über die Nächte
[checkIn, checkOut)des Zimmers wie in 10.1; eine Nacht, die die Datei nicht nennt, ist zu (nie „offen, weil nichts geliefert“). Das Feldconfiguredvon/v1/pricehat im Cache keine Entsprechung: ein Zimmer ohne Kontingent steht dort als00. - Vor der Buchung live: Der Cache kann einen Stand hinter der API liegen; eine im Cache
offene, inzwischen gesperrte oder ausgebuchte Nacht lehnt
/v1/bookab (ERR_STOP_SALE,ERR_SOLD_OUT).
11. Sandbox: Test-Keys und Testbuchungen
Ein Test-Key (tk_test_…) ist der Zwilling eines Live-Keys: derselbe Veranstalter, dieselbe
Kundengruppe, dieselben Rechte (Export-Recht, später Suchprofil) — zur Laufzeit vom Live-Key
übernommen. Er liest dieselben Daten wie der Live-Key (Hotels, Preise, Ziele,
Verfügbarkeit, EDF-Lieferung), aber Buchungen landen in der Sandbox: nie beim
Veranstalter, kein Kontingent, kein Export, keine Buchungsliste der Konsole. So lassen sich
Buchung, Buchungsinfo, Storno, Idempotenz und Wiederhol-Logik mit echten Angeboten testen.
- Den Test-Key stellt der Veranstalter in der Konsole zum Live-Key aus (Knopf „Test-Key“ an der Key-Zeile) und gibt ihn mit einem eigenen Zugangspaket weiter – oder Sie stellen ihn selbst im Abnehmer-Portal aus (7, 30 oder 90 Tage), wenn der Veranstalter das für Ihren Zugang erlaubt. Gültig 30 Tage (höchstens 90), höchstens 5 aktive Test-Keys je Zugang.
- Ist der Live-Key gesperrt oder der Test-Key abgelaufen:
401 ERR_UNAUTHORIZED. Kundengruppe deaktiviert:403 ERR_KEY_GROUP_INACTIVEwie live. Nach einer Rotation des Live-Keys arbeitet der Test-Key mit dem neuen Live-Key weiter; er endet nie später als sein Live-Key. - Maßgeblich für den Modus ist die Zuordnung beim Veranstalter, nicht der Präfix des Keys.
11.1 Kennzeichnung und Schutz vor Verwechslung
Jede Antwort auf einen angenommenen Key trägt den Kopf X-TourAPI-Mode: test bzw.
live (Kopfnamen wie in HTTP üblich ohne Groß-/Kleinschreibung vergleichen: der Server
schreibt X-Tourapi-Mode). Buchung, Buchungsinfo und Storno eines Test-Keys tragen zusätzlich das Feld
"sandbox": true, die Referenz beginnt mit SB- (live TA-). Lesen mit dem Test-Key
liefert dieselbe Antwort wie mit dem Live-Key:
### sandbox-preis
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{testKey}}
X-TourAPI-Require-Mode: test
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"board": "RO",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}Test-Umgebungen (CI, Agenten, Entwicklerrechner) senden den Kopf X-TourAPI-Require-Mode:
test. Kommt dann ein Live-Key, lehnt die API jede Anfrage ab und führt nichts aus – ein
versehentlich eingesetzter Live-Key kann so nicht buchen (die Prüfung liegt beim Server, nicht
im Client). X-TourAPI-Require-Mode: live verlangt umgekehrt einen Live-Key; ein anderer Wert
ist 400 ERR_BAD_REQUEST.
### sandbox-live-key-in-testumgebung
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
X-TourAPI-Require-Mode: test
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "BEISPIEL-0003"
}{"errorCode": "ERR_MODE_MISMATCH", "message": "X-TourAPI-Require-Mode: test verlangt, der API-Key ist ein live-Key — nichts ausgefuehrt"}11.2 Testbuchung, Buchungsinfo, Storno
/v1/book mit Test-Key durchläuft dieselben Prüfungen in derselben Reihenfolge wie live
(Felder, Wiederholung, Hotel, Stichtag und Grenzen, Verkaufsregeln, priceCheck,
Verfügbarkeit) und liefert dieselben Fehlercodes. Die Verfügbarkeit wird geprüft, aber nicht
verbraucht:
### sandbox-buchen
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{testKey}}
X-TourAPI-Require-Mode: test
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "BEISPIEL-0001",
"reference": "TEST-4711",
"leadPaxName": "Test Person",
"priceCheck": {
"board": "RO",
"currency": "EUR",
"occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
"expectedCents": 18000,
"tolerancePercent": 0
}
}{"booked": true, "alreadyBooked": false, "reference": "{{sandboxRef}}", "correlationId": "{{*}}", "sandbox": true}### sandbox-buchung-lesen
GET {{baseUrl}}/v1/booking?ref={{sandboxRef}}
X-Api-Key: {{testKey}}{"reference": "{{sandboxRef}}", "customerReference": "TEST-4711", "hotel": "TEST-HOTEL-BASE", "room": "DZ",
"checkIn": "{{D2}}", "checkOut": "{{D4}}", "quantity": 1, "status": "confirmed",
"bookedAt": "{{*}}", "updatedAt": "{{*}}", "totalCents": 18000, "currency": "EUR", "board": "RO", "sandbox": true}Test- und Live-Welt sind getrennt: Ein Test-Key sieht und storniert nur Testbuchungen, ein
Live-Key nur echte. Derselbe idemKey bucht in beiden Welten unabhängig voneinander.
### sandbox-live-key-sieht-testbuchung-nicht
GET {{baseUrl}}/v1/booking?ref={{sandboxRef}}
X-Api-Key: {{apiKey}}{"errorCode": "ERR_BOOKING_NOT_FOUND", "message": "keine Buchung zu '{{sandboxRef}}'"}### sandbox-stornieren
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{testKey}}
{"idemKey": "BEISPIEL-0001"}{"released": true, "alreadyReleased": false, "sandbox": true}- Testbuchungen werden 30 Tage nach Anlage gelöscht. Höchstens 5.000 offene (nicht
stornierte) Testbuchungen je Zugang, darüber
422 ERR_SANDBOX_LIMIT. - Die Aufrufe eines Test-Keys (Methode, Pfad, Status,
errorCode, Dauer, Anfrage und Antwort) protokolliert TourAPI 7 Tage lang, damit der Veranstalter bei der Fehlersuche helfen kann. Deshalb: in Testbuchungen Testnamen verwenden. - EDF-Lieferung mit Test-Key: Abrufe (
full,changes) und Quittungen (ack) stehen nur in diesem Protokoll der Test-Aufrufe, nie im Lieferprotokoll des Veranstalters – sie zählen nicht als Abholung oder Verarbeitung im Health der Lieferung.
11.3 Szenarien: Fehlerfälle erzwingen
Mit dem Kopf X-TourAPI-Sandbox-Scenario erzwingt ein Test-Key einen Fehlerfall – zum
Testen der Wiederhol-Logik (Abschnitt 8 und 9). Mit einem Live-Key, einem unbekannten Namen
oder an einem Endpunkt, an dem das Szenario nicht wirkt: 422 ERR_SCENARIO_NOT_ALLOWED.
- Szenario
booking_busy- Endpunkte
/v1/book,/v1/cancel- Wirkung
- erster Versuch je
idemKeyund Endpunkt (Buchung und Storno zählen getrennt):503 ERR_BOOKING_BUSYmitRetry-After: 1, nichts gebucht bzw. storniert; die Wiederholung mit demselbenidemKeyläuft durch
- Szenario
price_drift- Endpunkte
/v1/bookmitpriceCheck- Wirkung
- erster Versuch je
idemKey:409 ERR_PRICE_DRIFT; der Preis selbst ändert sich nicht (messagenennt aktuellen und erwarteten Preis, beide gleich); neuen Preis holen, erneut buchen
- Szenario
sold_out- Endpunkte
/v1/book- Wirkung
- immer
422 ERR_SOLD_OUT(nach allen anderen Prüfungen)
- Szenario
rate_limited- Endpunkte
- alle
- Wirkung
- erste Anfrage je Test-Key und Endpunkt:
429 ERR_RATE_LIMITEDmitRetry-After: 1
- Szenario
search_busy- Endpunkte
/v1/search- Wirkung
- erste Anfrage je Test-Key:
429 ERR_SEARCH_BUSYmitRetry-After: 1
- Szenario
price_timeout- Endpunkte
/v1/price,/v1/prices- Wirkung
- erste Anfrage je Test-Key und Endpunkt:
503 ERR_PRICE_TIMEOUT
„Erste Anfrage“ gilt 10 Minuten; jede Wiederholung in dieser Zeit läuft normal durch. Das Gedächtnis liegt je Knoten der API.
### sandbox-szenario-busy
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{testKey}}
X-TourAPI-Sandbox-Scenario: booking_busy
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "BEISPIEL-0002"
}{"errorCode": "ERR_BOOKING_BUSY", "message": "Buchung/Storno kam wegen gleichzeitiger Vorgaenge am selben Hotel nicht durch; nichts geaendert — mit demselben idemKey wiederholen (Sandbox-Szenario booking_busy)"}### sandbox-szenario-busy-wiederholt
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{testKey}}
X-TourAPI-Sandbox-Scenario: booking_busy
{
"hotel": "TEST-HOTEL-BASE",
"room": "DZ",
"checkIn": "{{D2}}",
"checkOut": "{{D4}}",
"quantity": 1,
"idemKey": "BEISPIEL-0002"
}{"booked": true, "alreadyBooked": false, "reference": "{{*}}", "correlationId": "{{*}}", "sandbox": true}### sandbox-szenario-live-key
GET {{baseUrl}}/v1/destinations
X-Api-Key: {{apiKey}}
X-TourAPI-Sandbox-Scenario: rate_limited{"errorCode": "ERR_SCENARIO_NOT_ALLOWED", "message": "X-TourAPI-Sandbox-Scenario: nur mit einem Test-Key (Sandbox)"}11.4 Grenzen der Test-Keys
Alle Test-Keys eines Zugangs teilen sich einen Topf: 20 Anfragen/s, Burst 40 (daneben
bleibt die Grenze des Live-Keys unberührt). Test-Keys eines Veranstalters belegen höchstens 2
gleichzeitige Suchen. Der Takt der EDF-Lieferung (full 1/h, neue changes-Kette 1/min)
gilt je Zugang, nicht je Test-Key.
11.5 Was die Sandbox nicht beweist
- Kontingent und Rennen um das letzte Zimmer: Testbuchungen verbrauchen nichts. Eine Testbuchung kann gelingen, wo live ein anderer schneller war.
- Abläufe beim Veranstalter nach der Buchung (Bestätigung, Änderung, Rechnung).
- Verhalten unter Live-Last (eigener, kleinerer Topf, siehe 11.4).
11.6 KI-Agenten: MCP-Server im Portal
Das Portal bietet unter /mcp einen MCP-Server (Model Context Protocol, Transport
„Streamable HTTP“, zustandslos, nur Werkzeuge). Ein KI-Agent wie Claude Code, Codex oder
Antigravity ruft damit die API über Werkzeuge auf, statt HTTP-Code zu schreiben. Jedes Werkzeug
ist genau ein /v1-Aufruf mit dem Key der MCP-Anfrage (X-Api-Key oder
Authorization: Bearer) und X-TourAPI-Require-Mode: test – Verhalten, Grenzen, Isolation und
Fehlercodes sind die der API. Nur Test-Keys: ein Live-Key bekommt ERR_MODE_MISMATCH (403),
nichts wird ausgeführt. Das Portal speichert keinen Key.
| Werkzeug | Aufruf | Argumente |
|---|---|---|
list_destinations | GET /v1/destinations | keine |
search_hotels | POST /v1/search | Anfrage-Körper; Seiten per cursor |
price_offer | POST /v1/price | Anfrage-Körper |
price_all_rooms | POST /v1/prices | Anfrage-Körper |
sandbox_book | POST /v1/book | Anfrage-Körper, idemKey Pflicht |
get_booking | GET /v1/booking | ref |
sandbox_cancel | POST /v1/cancel | Anfrage-Körper |
get_limits | GET /v1/limits | keine |
open_search | POST /v1/search/open | Anfrage-Körper |
open_search_dates | POST /v1/search/open/dates | Anfrage-Körper |
hotel_details | GET /v1/content/hotels/{code} | code, lang |
Werkzeuge mit Anfrage-Körper reichen ihre Argumente unverändert als JSON-Körper an die API
weiter (Schema = OpenAPI des Endpunkts). Die übrigen nehmen nur die genannten Argumente; ein
anderes ergibt ERR_UNKNOWN_FIELD, ein ungültiger Wert ERR_BAD_REQUEST – ohne API-Aufruf.
hotel_details nur mit Inhalts-Recht. tools/list fragt mit dem Key der Anfrage
GET /v1/limits und nennt hotel_details nur bei content.allowed: true (dieselbe Regel wie die
Content-API, 4c); ohne Key fehlt es. Wird es trotzdem aufgerufen, antwortet die API
ERR_CONTENT_NOT_ALLOWED (403). Weist die API den Key dabei ab (z. B. ERR_MODE_MISMATCH für
einen Live-Key) oder antwortet sie nicht, ist tools/list ein JSON-RPC-Fehler (-32000) mit
Fehlercode bzw. Grund in message – keine Werkzeugliste.
hotel_details und lang. Bietet der Veranstalter eine angefragte Inhaltssprache nicht an,
gibt das Werkzeug nicht ERR_LANGUAGE_NOT_OFFERED weiter: es liest die Inhaltssprachen
(GET /v1/content/catalog, languages) und fragt die angefragten Sprachen, soweit angeboten,
sonst eine Rückfallsprache (en, dann de, dann die erste angebotene). structuredContent
nennt das unter language (requested, delivered, offered, fallback: true). Die
Content-API selbst bleibt streng; bietet der Veranstalter keine Inhaltssprache an, bleibt ihr Fehler.
Ergebnis. structuredContent (derselbe JSON-Text steht in content) hat zwei Teile:
data ist die Antwort der API, in der jeder Text durch den Platzhalter [untrusted] ersetzt ist;
untrusted enthält diese Texte unter ihrem JSON-Pointer in der Antwort, bereinigt: Steuer-,
Bidi-, Null-Breite- und andere unsichtbare Zeichen entfernt, HTML als Text, Links durch
[link removed] ersetzt, höchstens 200 Zeichen (Meldungen 500, Beschreibungen 2000). Texte sind
die Freitexte Dritter (Hotel- und Zielnamen, Kette, Anschrift, Beschreibungen, Bildtitel und
-nachweise, Name des Leitgasts), die Meldungen der API (warnings, message) und jede
Zeichenkette, die nicht dem festen Muster ihres Feldes entspricht: in data bleiben nur Codes
(Buchstaben, Ziffern, _ . -, höchstens 64 Zeichen), Referenzen, Daten, Zeitpunkte, Währungen,
Fehlercodes und Cursor. Ein Ziel- oder Gruppen-Code mit Leerzeichen oder Link erscheint also nur
unter untrusted. Adress-Felder (url der Bild-Varianten) fehlen;
Bilder liefert die Content-API selbst. Texte unter untrusted sind für den Agenten Daten, keine
Anweisungen.
{"data": {"results": [{"hotel": "H1", "name": "[untrusted]", "room": "DZ", "fromTotalCents": 42000, "currency": "EUR"}]}, "untrusted": {"/results/0/name": "Haus Eins"}}Fehler kommen als Werkzeug-Ergebnis mit isError: true und structuredContent
{errorCode, status, message, retryAfter, hint}: errorCode und status wie die API
(Abschnitt 7), retryAfter = Retry-After in Sekunden, hint = Spalte „Aufrufer“ des
Fehlerkatalogs (englisch). message ist bereinigt wie ein Text unter untrusted. Fehlt der Key,
steht X-Api-Key oder Authorization mehr als einmal in der Anfrage oder nennen beide
verschiedene Keys: ERR_UNAUTHORIZED ohne API-Aufruf. Eigene Codes des MCP-Servers:
ERR_UPSTREAM_UNAVAILABLE (502, API nicht erreichbar oder Zeitüberschreitung – nach retryAfter
wiederholen, sandbox_book mit demselben idemKey) und ERR_RESPONSE_TOO_LARGE (502, Antwort
über 4 MiB – Anfrage eingrenzen).
Grenzen. Nur POST (kein SSE-Strom: GET ergibt 405), Content-Type: application/json,
eine JSON-RPC-Nachricht je Anfrage (keine Batches); Körper höchstens 64 KiB, Tiefe 16, 4096
JSON-Elemente – sonst 413 bzw. 400, bevor etwas ausgewertet wird. Je Key 10 Werkzeugaufrufe/s
(darüber ERR_RATE_LIMITED mit retryAfter), daneben gilt der Topf der Test-Keys (11.4). Eine
Anfrage mit fremdem Origin bekommt 403 (Schutz gegen Webseiten im Browser); keine Cookies, kein
CORS. Protokoll-Fassungen 2025-03-26, 2025-06-18 und 2025-11-25.
Einrichtung. Den Test-Key als Umgebungsvariable TOURAPI_API_KEY setzen (dieselbe wie im eigenen
Code, eine Variable genügt); in die Konfiguration gehört nur der Verweis darauf. Claude Code, Datei .mcp.json im Projekt:
{"mcpServers": {"tourapi": {"type": "http", "url": "https://portal.example.de/mcp", "headers": {"X-Api-Key": "${TOURAPI_API_KEY}"}}}}Codex (der Key geht als Bearer-Token aus der Umgebungsvariable):
codex mcp add tourapi --url https://portal.example.de/mcp --bearer-token-env-var TOURAPI_API_KEYAntigravity setzt in headers keine Umgebungsvariablen ein; die Brücke mcp-remote liest den Key
aus der Umgebung (Eintrag in mcp_config.json, Fassung der Brücke fest, damit kein ungeprüfter
Stand den Key bekommt):
{"mcpServers": {"tourapi": {"command": "npx", "args": ["-y", "mcp-remote@0.14.3", "https://portal.example.de/mcp", "--header", "X-Api-Key:${TOURAPI_API_KEY}"]}}}Die Seite „KI-Agenten“ im Portal zeigt dieselben Einträge mit der Adresse Ihres Portals.
12. Hotelinhalte: /v1/content/*
Die Content-API liefert die Hotelinhalte des Veranstalters für Webseiten und Kataloge der Abnehmer: Stamm (Objektart, Kette, Adresse), Lage (Koordinaten), Kategorien, Fakten, Texte, Bilder und Ausstattung. Sie ist ein eigener Weg neben Preis und Buchung: Inhalte ändern keinen Preis und keine Verfügbarkeit.
| Route | Zweck |
|---|---|
GET /v1/content/hotels | Verzeichnis der Hotels des Keys, aufsteigend nach code, seitenweise |
GET /v1/content/hotels/{code} | Inhalt eines Hotels (ETag, If-None-Match → 304) |
GET /v1/content/changes?since=… | Änderungen seit einem Stand, in der Reihenfolge, in der sie gespeichert wurden |
GET /v1/content/catalog | Kataloge mit Beschriftungen (Objektarten, Kategorieskalen, Text- und Bildtypen, Ausstattung) |
12.1 Zugang und Umfang
- Die Content-API antwortet nur, wenn beides gilt: der Betreiber hat Inhalte für den
Veranstalter freigeschaltet, und der Key trägt das Inhalts-Recht (vergibt der
Veranstalter-Admin unter API-Zugang, Voreinstellung aus). Sonst
403 ERR_CONTENT_NOT_ALLOWED. Ein Test-Key hat das Recht seines Live-Keys und liest dieselben Inhalte. - Einen Veranstalter, den der Betreiber als Testumgebung führt, beliefert nur die dafür
eingerichtete Installation. Überall sonst gilt er als nicht freigeschaltet: dieselbe Antwort
403 ERR_CONTENT_NOT_ALLOWED. - Welche Hotels ein Key sieht, bestimmt der Key wie bei
/v1/destinations(1.1): ohne Kundengruppe und mit Preisgruppe alle Hotels des Veranstalters, mit Kontingent-Gruppe nur Hotels mit Zuteilung – unabhängig von der Verfügbarkeit. Jedes andere Hotel ist404 ERR_HOTEL_NOT_FOUND. - Ausgeliefert werden nur sichtbare Texte in den Inhaltssprachen des Veranstalters und sichtbare, fertig verarbeitete Bilder. Kontaktdaten des Hotels (Telefon, Mail, Web) gibt es in der Content-API nicht, auch nicht als leeres Feld.
- Bilder liegen unter öffentlichen, nicht erratbaren Adressen
(
<base>/m/<key>/<size>.jpg, ohne Key abrufbar, 1 Tag cachebar). Neben jedem Bild stehtcredit; beiattributionRequired: truemuss die Nennung neben dem Bild erscheinen. - Keys gehören nicht in Browser-Code (1.1): die Webseite liest die Content-API auf dem Server, nur die Bild-Adressen gehen an den Browser.
12.2 Verzeichnis
### inhalt-verzeichnis
GET {{baseUrl}}/v1/content/hotels?pageSize=2
X-Api-Key: {{apiKey}}{
"hotels": [
{"code": "TEST-HOTEL-ALTAKTION", "name": "TEST Altaktion Teneriffa", "destination": "TFS", "contentVersion": 0, "websiteReady": false, "missing": ["general_text", "geo", "category", "images", "amenities"]},
{
"code": "TEST-HOTEL-BASE",
"name": "TEST Basis Palma",
"destination": "PMI",
"contentVersion": "{{*}}",
"updatedAt": "{{*}}",
"category": {"kind": "official", "scheme": "stars", "value": 4, "superior": true},
"geo": {"lat": 39.565, "lon": 2.627, "precision": "address"},
"websiteReady": false,
"missing": ["images", "amenities"]
}
],
"nextCursor": "{{*}}",
"feedToken": "{{*}}",
"scopeHash": "{{*}}"
}pageSize1–1000 (Standard 500). Die nächste Seite holt man mitcursor=<nextCursor>; ohnenextCursorist das Verzeichnis vollständig.contentVersionzählt jede Änderung der Auslieferung eines Hotels;0= noch kein Inhalt (dann fehltupdatedAt).categoryist die offizielle Landeskategorie,geodie Lage – beide nur, wenn gepflegt.websiteReadyundmissingwie im Inhalt eines Hotels (12.3).feedTokenist der Stand, ab dem/v1/content/changesweitermacht. Alle Seiten eines Verzeichnis-Durchlaufs tragen denselben Stand (den der ersten Seite).scopeHashändert sich, sobald sich die Hotelmenge des Keys ändert (neues oder gelöschtes Hotel, Zuteilung kommt oder geht). Das steht nicht im Feed: dann das Verzeichnis neu holen.cursor,feedTokenundnextsind undurchsichtig und an den Key gebunden: mit einem anderen Key, verändert oder nach einem Schlüsselwechsel des Servers422 ERR_BAD_CURSOR.
### inhalt-verzeichnis-weiter
GET {{baseUrl}}/v1/content/hotels?pageSize=2&cursor={{inhaltCursor}}
X-Api-Key: {{apiKey}}{
"hotels": [
{"code": "TEST-HOTEL-CLOSED", "name": "TEST Geschlossen Mahon", "destination": "MAH", "contentVersion": 0, "websiteReady": false, "missing": ["general_text", "geo", "category", "images", "amenities"]},
{
"code": "TEST-HOTEL-DISCOUNT",
"name": "TEST Rabatt Palma",
"destination": "PMI",
"contentVersion": "{{*}}",
"updatedAt": "{{*}}",
"category": {"kind": "official", "scheme": "stars", "value": 4},
"geo": {"lat": 39.5696, "lon": 2.6502, "precision": "locality"},
"websiteReady": false,
"missing": ["general_text", "images", "amenities"]
}
],
"nextCursor": "{{*}}",
"feedToken": "{{*}}",
"scopeHash": "{{*}}"
}12.3 Inhalt eines Hotels
### inhalt-hotel
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE?lang=de,tr
X-Api-Key: {{apiKey}}{
"code": "TEST-HOTEL-BASE",
"name": "TEST Basis Palma",
"destination": "PMI",
"contentVersion": "{{*}}",
"updatedAt": "{{*}}",
"accommodationType": "HOTEL",
"categories": [{"kind": "official", "scheme": "stars", "value": 4, "superior": true}],
"address": {"street": "Passeig Marítim 12", "postalCode": "07014", "city": "Palma", "region": "Mallorca", "country": "ES"},
"geo": {"lat": 39.565, "lon": 2.627, "precision": "address"},
"facts": {"rooms": 120, "checkInFrom": "14:00", "checkOutUntil": "11:00"},
"texts": [
{"type": "GENERAL", "lang": "de", "html": "<p>TEST-Hotel am Strand mit <b>Pool</b> und Garten.</p>\n<p>TEST-Lage: ruhige Bucht.</p>", "updatedAt": "{{*}}"},
{"type": "GENERAL", "lang": "de", "fallbackFrom": "tr", "html": "<p>TEST-Hotel am Strand mit <b>Pool</b> und Garten.</p>\n<p>TEST-Lage: ruhige Bucht.</p>", "updatedAt": "{{*}}"}
],
"media": [],
"amenities": [
{"code": "WIFI_ROOM", "available": true, "charge": "included"},
{"code": "SPA", "available": false},
{"code": "DIST_AIRPORT", "available": true, "distanceM": 12000, "ref": "PMI"}
],
"websiteReady": false,
"missing": ["images", "amenities"]
}- Sprachen: ohne
langkommen alle Texte in allen Inhaltssprachen des Veranstalters. Mitlang(1–5 Sprachen, ISO 639-1 klein, kommagetrennt) kommt je Texttyp genau ein Eintrag je angefragter Sprache. Fehlt der Text in dieser Sprache, springt die Standardsprache des Veranstalters ein, dann Englisch; der Eintrag trägt dannfallbackFrommit der angefragten Sprache (hier: kein Text auf Türkisch, es kommt Deutsch). Gibt es auch dort keinen, fehlt der Eintrag. Titel (titles) und Alternativtexte (alts) der Bilder folgen derselben Regel. Eine Sprache, die der Veranstalter nicht anbietet:422 ERR_LANGUAGE_NOT_OFFERED(die aktiven Sprachen stehen inwarnings). htmlenthält nurp,br,b,strong,i,em,ul,ol,liohne Attribute.machineTranslated: truekennzeichnet eine maschinelle Übersetzung.categories:kindofficial(Landeskategorie) oderoperator(Einstufung des Veranstalters),schemestarsoderkeys,value1–5 in halben Schritten; „4 Superior“ istvalue: 4, superior: true, nie 4,5.geo.precision:address,street,localityoderunknown.mediain Anzeigereihenfolge,order: 1ist das Hauptbild. Je Bildvariantsmit den erzeugten Breiten (w320bisw2048, nie breiter als das Original), jeweils mit Breite, Höhe, Bytes undurl;focus(x, y in Prozent) ist der wichtigste Bildpunkt für eigene Zuschnitte.idist stabil, solange das Bild im Hotel bleibt.amenities: Ausstattung mit Code aus dem Katalog (12.5).available: falseheißt ausdrücklich nicht vorhanden; ein Merkmal, das fehlt, ist unbekannt. Je nach Merkmal mitcount,distanceM,areaM2,ref(Flughafen-Code zur Entfernung) und bei kostenpflichtig möglichen Merkmalencharge(included,extra,unknown).name,destinationundgiataCodekommen aus dem Vertrag des Hotels. Ändert sich einer davon, steht das Hotel im Feed (12.4,reason: contract), und Detail wie Verzeichnis liefern ab dann den neuen Wert.websiteReadyundmissing: Reifegrad „webseitenbereit“ – ob der Veranstalter genug Inhalt für eine Webseite gepflegt hat. Eine Kennzahl, sie ändert weder Verkauf noch Auslieferung.missingnennt die fehlenden Kriterien in fester Reihenfolge, leer = webseitenbereit:general_text(allgemeiner Text in der Standardsprache des Veranstalters),geo(Lage),category(Kategorie oder Objektart),images(mindestens 5 ausgelieferte Bilder, das Hauptbild darunter),amenities(mindestens 10 gepflegte Merkmale, auch ausdrücklich „nicht vorhanden“). Die Bewertung ändert sich nur mit dem Inhalt (neuecontentVersion); die Konsole des Veranstalters zeigt dieselbe. Unbekannte Werte inmissingignorieren (es können Kriterien dazukommen). Im Verzeichnis stehen beide Felder ebenfalls.
Jede Antwort trägt einen ETag. Er ändert sich mit dem Inhalt, der Sprachwahl und der
Darstellung. Mit If-None-Match kommt 304 ohne Körper, solange sich nichts geändert hat:
### inhalt-hotel-unveraendert
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE?lang=de,tr
X-Api-Key: {{apiKey}}
If-None-Match: {{inhaltEtag}}Ohne lang alle Inhaltssprachen (hier en als maschinelle Übersetzung):
### inhalt-hotel-alle-sprachen
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE
X-Api-Key: {{apiKey}}{
"code": "TEST-HOTEL-BASE",
"name": "TEST Basis Palma",
"destination": "PMI",
"contentVersion": "{{*}}",
"updatedAt": "{{*}}",
"accommodationType": "HOTEL",
"categories": "{{*}}",
"address": "{{*}}",
"geo": "{{*}}",
"facts": "{{*}}",
"texts": [
{"type": "GENERAL", "lang": "de", "html": "{{*}}", "updatedAt": "{{*}}"},
{"type": "GENERAL", "lang": "en", "html": "<p>TEST hotel on the beach with a <b>pool</b> and garden.</p>\n<p>TEST location: quiet bay.</p>", "machineTranslated": true, "updatedAt": "{{*}}"}
],
"media": [],
"amenities": "{{*}}",
"websiteReady": false,
"missing": ["images", "amenities"]
}12.4 Änderungen
### inhalt-aenderungen
GET {{baseUrl}}/v1/content/changes?since={{feedToken}}
X-Api-Key: {{apiKey}}{"changes": [], "next": "{{*}}", "more": false, "scopeHash": "{{*}}"}sinceist derfeedTokendes Verzeichnisses odernextder vorigen Antwort (Pflicht, sonst400 ERR_BAD_REQUEST).pageSize1–1000 (Standard 500).- Je Eintrag
code,change(upsert= Inhalt geändert, neu holen;removed= Hotel gelöscht), beiupsertdie aktuellecontentVersion, und optionalreason(languages= die Inhaltssprachen des Veranstalters haben sich geändert,media_ready= ein Bild ist fertig verarbeitet,contract=name,destinationodergiataCodehat sich mit dem Vertrag geändert,deletedbeiremoved). Unbekannte Werte ignorieren. - In der Reihenfolge, in der die Änderungen gespeichert wurden, lückenlos; ein Hotel steht je
Seite höchstens einmal (mit seinem letzten Stand).
more: true= sofort mitnextweiterlesen. - Nur Hotels des Keys. Eine Änderung der Hotelmenge steht nicht im Feed, sondern im
scopeHash. - Der Feed reicht 30 Tage zurück. Ein älterer Stand:
410 ERR_CONTENT_CURSOR_EXPIRED– dann das Verzeichnis neu holen und mit dessenfeedTokenweitermachen.
12.5 Katalog
### inhalt-katalog
GET {{baseUrl}}/v1/content/catalog?lang=de
X-Api-Key: {{apiKey}}{
"version": "2026.1",
"languages": ["de", "en", "tr"],
"defaultLanguage": "de",
"labelLanguages": ["de"],
"accommodationTypes": "{{*}}",
"categorySchemes": [{"code": "sterne", "sort": 1, "labels": {"de": "Sterne"}}, {"code": "schluessel", "sort": 2, "labels": {"de": "Schlüssel"}}],
"textTypes": "{{*}}",
"mediaTypes": "{{*}}",
"amenityGroups": "{{*}}",
"amenities": "{{*}}"
}languagessind die Inhaltssprachen des Veranstalters,defaultLanguageseine Standardsprache.langwählt die Beschriftungssprachen (de,en,tr; ohnelangalle).- Je Ausstattungsmerkmal
group,valueType(flag,anzahl,meter,flaeche_m2,meter_mit_bezug),unitundchargeable. Ein Code wird nie umgedeutet;locked: trueheißt entfallen (bleibt lesbar).versionändert sich mit jedem neuen Katalog; der Katalog trägt einenETag.
12.6 Abgleich für Abnehmer
- Erstbefüllung: Verzeichnis seitenweise holen,
feedTokenundscopeHashmerken, je Hotel den Inhalt holen und denETagspeichern. - Laufend (etwa alle paar Minuten):
changes?since=<token>(beim ersten AbruffeedToken, danach das zuletzt gemerktenext), geänderte Hotels neu holen,removedlöschen,nextmerken. Ändert sichscopeHash: sofort Schritt 3. - Täglich und bei
410: Verzeichnis neu holen und mit dem eigenen Bestand abgleichen (fehlende Hotels löschen, neue holen,contentVersionvergleichen). - Inhalte immer mit
If-None-Matchholen.
Zugangs- und Umfangsfehler:
### inhalt-ohne-recht
GET {{baseUrl}}/v1/content/hotels
X-Api-Key: {{poolKey}}{"errorCode": "ERR_CONTENT_NOT_ALLOWED", "message": "dieser API-Key hat kein Inhalts-Recht — der Veranstalter-Admin schaltet es unter API-Zugang frei"}### inhalt-nicht-im-verzeichnis
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-DISCOUNT
X-Api-Key: {{partnerKey}}{"errorCode": "ERR_HOTEL_NOT_FOUND", "message": "Hotel 'TEST-HOTEL-DISCOUNT' nicht im Verzeichnis dieses API-Keys"}### inhalt-sprache-nicht-angeboten
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE?lang=fr
X-Api-Key: {{apiKey}}{"errorCode": "ERR_LANGUAGE_NOT_OFFERED", "message": "lang: 'fr' ist keine Inhaltssprache dieses Veranstalters (ISO 639-1, klein)", "warnings": ["aktive Inhaltssprachen: de, en, tr (Standard de)"]}### inhalt-stand-fremder-key
GET {{baseUrl}}/v1/content/changes?since={{feedToken}}
X-Api-Key: {{partnerKey}}{"errorCode": "ERR_BAD_CURSOR", "message": "since: unlesbar, von einem anderen API-Key oder nicht von diesem Server — den Wert der Vorseite unveraendert zurueckgeben, sonst neu beginnen"}13. Geplant und Grenzen
Geplant: Innerhalb von /v1 kommen nur additive Änderungen hinzu (Abschnitt 1.9); jede
Änderung steht mit Datum im Changelog. Derzeit ist keine Änderung angekündigt, die eine
bestehende Anbindung anpassen müsste.
Grenzen: Die Zahlenwerte (Größen, Takte, Zeitgrenzen) stehen im Anhang „Grenzen auf einen Blick“, was die Sandbox nicht abbildet in 11.5.
Anhang: Grenzen auf einen Blick
- Was
- Body je Anfrage
- Wert
- 1 MiB
- Was
- Nächte je Aufenthalt
- Wert
- 1–30
- Was
- Anreise
- Wert
- Stichtag bis Stichtag + 732 Tage
- Was
- Reisende je Anfrage, Alter
- Wert
- 1–20, 0–120
- Was
- Zimmer je Buchung (
quantity) - Wert
- 1–1.000.000
- Was
idemKey,reference- Wert
- 128 Zeichen (länger:
422 ERR_VALIDATION)
- Was
leadPaxName- Wert
- 255 Zeichen (länger:
422 ERR_VALIDATION)
- Was
metadata.correlationId- Wert
- 64 Zeichen (länger:
422 ERR_VALIDATION)
- Was
- Rate je Key
- Wert
- 200/s, Burst 400
- Was
- Test-Keys je Zugang
- Wert
- höchstens 5, gültig bis 90 Tage; zusammen 20/s, Burst 40; 2 gleichzeitige Suchen je Veranstalter
- Was
- Testbuchungen
- Wert
- 5.000 offene je Zugang, gelöscht 30 Tage nach Anlage; Protokoll der Test-Aufrufe 7 Tage
- Was
- Such-Zeitgrenze
- Wert
- 10 s
- Was
- Suchseite
- Wert
- Standard 50, höchstens 100 Hotels
- Was
- Offene Suche
- Wert
- je Key laut Suchprofil (Fenster, Dauern, Ziele, Seite, Zeitbudget, Takt; Termin-Matrix: Fenster und Zellen), abrufbar mit
GET /v1/limits; Seite ohne Angabe 20; Cursor 15 min
- Was
- Verbindung
- Wert
- 15 s lesen, 15 s schreiben
- Was
- EDF-Lieferung je Key
- Wert
full1/h, neuechanges-Kette 1/min, Folgeseiten/Quittungen 5/s (Burst 50); Paket-Antworten bis 10 min schreiben
- Was
- Content-API
- Wert
- Verzeichnis- und Feed-Seite 1–1000 (Standard 500),
lang1–5 Sprachen, Feed 30 Tage