Öffentliche API-Doku
Anmelden
API-Doku

TourAPI – API-Handbuch v1

Käufer-API v1: Ablauf, Regeln, Fehlerkatalog und Beispiele – jedes Beispiel läuft bei jedem Build als Test.

Inhalt

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ürzelBedeutungEndpunkt
BAVerfügbarkeit und Preis anfragen, suchen/v1/search, /v1/price, /v1/prices
BAoffene Suche ohne festen Termin (Recht je Key)/v1/search/open (Abschnitt 4a)
BATermin-Matrix eines Hotels (Recht je Key)/v1/search/open/dates (Abschnitt 4b)
–wirksame Grenzen des eigenen Keys/v1/limits (Abschnitt 4c)
Bbuchen/v1/book
–Buchungsinfo lesen/v1/booking
Sstornieren/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, Szenarienalle 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.
  • 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 Grund ERR_HOTEL_NOT_FOUND in diagnostics.reasons, auch /v1/book lehnt mit 404 ab) – 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 Kopf X-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 von v1 ä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. GET sind /v1/booking, /v1/destinations, /v1/health und die EDF-Lieferung (/v1/export/edf/full, /changes), alle übrigen sind POST; 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/…) ist 404 ohne JSON-Körper (text/plain, kein errorCode); alle anderen Fehler haben die Form aus 1.6.

1.3 Datum, Aufenthalt, Stichtag

  • Datumsfelder sind Kalendertage JJJJ-MM-TT ohne Uhrzeit und Zone.
  • Ein Aufenthalt ist halb offen: checkIn ist die erste Nacht, checkOut der Abreisetag. checkIn=2026-10-26, checkOut=2026-10-28 sind zwei Nächte.
  • Stichtag ist das Serverdatum in der Zone Europe/Berlin. Frühbucher- und Zeitraumregeln rechnen immer damit. Das Anfragefeld now ist nur noch geduldet: leer lassen. Schickt man ein anderes Datum, rechnet die Auskunft trotzdem mit dem Serverdatum und sagt es in warnings; /v1/book lehnt ein abweichendes priceCheck.now ab (ERR_NOW_MISMATCH).
  • Grenzen je Anfrage (greifen vor jeder Rechnung):
GrenzeWertCode (422)
Nächte je Aufenthalt1–30ERR_EMPTY_STAY, ERR_STAY_TOO_LONG
Anreise frühestensheute (Stichtag)ERR_STAY_IN_PAST
Anreise spätestensStichtag + 732 TageERR_STAY_TOO_FAR
Reisende je Anfrage1–20ERR_NO_TRAVELLERS, ERR_TOO_MANY_TRAVELLERS
Alter0–120ERR_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 (rounding in 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 ist perTravellerCents[i], totalCents ist deren Summe. Ein Posten wie „−15 % auf 89,90“ ist −13,485 und bleibt es bis zur Summe. Den exakten Betrag je Posten zeigt breakdown[].amountExact (Abschnitt 3.1).
  • Die Währung ist die Vertragswährung des Hotels (currency, ISO 4217). TourAPI rechnet nicht um. currency in der Anfrage ist ein Wunsch: weicht er ab, kommt die Antwort in Vertragswährung mit Hinweis in warnings. Ist am Vertrag keine Währung hinterlegt, bleibt currency leer, ebenfalls mit Hinweis – es wird nicht „EUR“ geraten.
  • Bei /v1/book mit priceCheck ist 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: 200 mit dem Ergebnis. Fehler: 4xx/5xx mit
    {"errorCode": "ERR_…", "message": "…", "warnings": ["…"]}

    errorCode ist stabil und für Programme gedacht, message ist 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, ignoriertes now. Bitte loggen. Viele Integrationsfehler stehen genau dort.
  • Rechenzeit: Jede Antwort eines Endpunkts – auch Fehlerantworten und /v1/health – trägt den Kopf Server-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 in warnings genannt. Innerhalb von occupancy wird es abgelehnt (422 ERR_UNKNOWN_FIELD), weil ein Tippfehler dort den Preis ändert. /v1/book und /v1/cancel lehnen 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 % auf TEST-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
epoch und to_seq aus dem Manifest des Beispiels export-voll
Platzhalter
{{ohnePreisRef}}, {{zweiteRef}}
Bedeutung
Buchungsreferenzen aus den Beispielen buchen-ohne-preispruefung bzw. 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
feedToken und nextCursor aus dem Beispiel inhalt-verzeichnis
Platzhalter
{{inhaltEtag}}
Bedeutung
ETag aus dem Beispiel inhalt-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 kommenKommt nie innerhalb /v1 (das wäre /v2)
neue optionale Anfragefelderneue Pflichtfelder in der Anfrage
neue AntwortfelderFelder entfernen oder umbenennen
neue EndpunkteTyp oder Bedeutung eines Feldes ändern
neue ERR_*-Codes, jeweils mit dokumentiertem Umgang im FehlerkatalogBedeutung 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).
  • message und warnings sind 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ültige destination-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, mit idemKey und priceCheck (dem Preis von eben).
  • /v1/booking: liest die Buchung über unsere oder die eigene Referenz.
  • /v1/cancel: storniert über denselben idemKey.

Regeln, die man kennen muss:

  1. Suche und Preis sind Auskunft, verbindlich ist erst /v1/book. Zwischen Preis und Buchung kann der Veranstalter Preise oder Kontingent ändern. Mit priceCheck lehnt /v1/book einen geänderten Preis ab (409 ERR_PRICE_DRIFT) statt still zum neuen zu buchen.
  2. Jede Buchung hat einen eigenen idemKey (vom Aufrufer vergeben, z. B. die eigene Vorgangsnummer). Wiederholungen mit demselben Key buchen nie doppelt (Abschnitt 9).
  3. 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 in warnings mit Zimmer und Code genannt; rechnet kein Zimmer, kommt der Fehler als 422. Mit room kommt der Fehler dieses Zimmers immer als 422.
Feld
board
Pflicht
ja
Bedeutung
Verpflegungs-Code, genau wie im Vertrag (RO, nicht ro). Fehlt: ERR_BOARD_MISSING; bietet das Zimmer (ohne room: 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 room in 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 in perTravellerCents), night (Index ab 0, -1 = je Aufenthalt), amountExact (exakter Betrag als Dezimalzahl, z. B. "-13.485"), amountCents, bei Extras applianceCode, bei Extras einer Extra-Familie (mehrere Extras mit demselben applianceCode) zusätzlich variant (die Variante; erst beide zusammen nennen das Extra), bei den Posten einer Freinacht zusätzlich freeNight: 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. code nennt das Extra, bei einer Extra-Familie zusammen mit variant. amountCents ist die exakte Summe des Extras, einmal gerundet; bei inTotal=true kann sie von der Summe seiner breakdown-Zeilen um mehrere Cent abweichen – maßgeblich ist breakdown.
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 room auch 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:

NachtBeitrag zu minFreeAllotment-Datei (Abschnitt 10)
1 bis 99 Einheiten freidie Anzahl01–99
mehr als 99 frei oder Freiverkaufsenkt minFree nicht**
Stop-Sale0SS
auf Anfrage0RR
ausgebucht, geschlossen, ohne Kapazität, Kundengruppe ohne Zuteilung für Zimmer und Nacht000

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 Zimmer available=false (minFree 0), es bleibt aber configured=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:

CodeDie Regel verlangt …Aufrufer
ERR_STAY_LENGTH_NOT_ALLOWEDeine andere Aufenthaltsdauer (Mindest- oder Höchstnächte)Dauer ändern
ERR_ARRIVAL_DAY_NOT_ALLOWEDeinen anderen An- oder Abreise-WochentagReisetage verschieben
ERR_TRAVEL_DATES_NOT_ALLOWEDReisedaten in einem bestimmten Zeitraum (Verkaufsfenster)anderer Zeitraum
ERR_BOARD_NOT_ALLOWEDeine andere Verpflegung für diesen Aufenthaltandere Verpflegung
ERR_LEAD_TIME_NOT_ALLOWEDmehr Vorlauf: die Anreise liegt innerhalb der Release-Fristspä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: ohne room fä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 vor ERR_OCCUPANCY_NOT_ALLOWED eines 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 in diagnostics.reasons.
  • /v1/book: mit priceCheck gilt die Regel für die angefragte Verpflegung; ohne priceCheck (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: ohne room fä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 in rooms[].errors[] mit errorCode (3.2); der Rest bleibt bepreist.
  • /v1/search: das Hotel fällt weg, der Code steht in diagnostics.reasons.
  • /v1/book mit priceCheck: 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.

FeldPflichtBedeutung
destinationneinZielgebiet- oder Flughafen-Code des Hotels (unten); leer = alle Hotels
board, checkIn, checkOut, occupancy, currency, nowwie /v1/price
includeUnavailableneintrue: nicht buchbare Hotels kommen gekennzeichnet mit (Standard false)
pageSize, cursorneinSeiten, Abschnitt 4.3

Antwort:

Feld
results[]
Bedeutung
Treffer der Seite, nach fromTotalCents aufsteigend, 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=false das 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 mit includeUnavailable): 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/book lehnt denselben Aufenthalt mit ERR_NO_INVENTORY ab.
  • 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 pageSize Hotels (Standard 50, höchstens 100; außerhalb 1–100: 422 ERR_BAD_PAGE_SIZE) in Hotel-Code-Reihenfolge.
  • Gibt es weitere Hotels, steht nextCursor in der Antwort. Die nächste Seite: dieselbe Anfrage plus "cursor": "<nextCursor>". Auf der letzten Seite fehlt nextCursor.
  • Die Preis-Sortierung gilt je Seite. Wer eine Gesamtliste nach Preis braucht, blättert durch und sortiert selbst. diagnostics gilt 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 ohne cursor neu beginnen. pageSize und currency dü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, und nextCursor führt weiter. Das ist kein Fehler: einfach weiterblättern.
  • Schickt man weder pageSize noch cursor und gibt es mehr Hotels, steht zusätzlich ein Hinweis in warnings – 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.

FeldBedeutung
destinations[].codeCode, genau so in destination zu übergeben
destinations[].nameKlartext 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.totalCents und best.perTravellerCents sind bit-gleich zu /v1/price mit 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); mit boards zusammen: 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): scheme stars oder keys (Pflicht), min/max Stufe 1 bis 5 in Halbschritten als Zahl (z. B. 3.5, gleichwertig 3.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) und radiusKm (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
nextCursor der 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, immer available: 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/rooms entsprechend. 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/search dann 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/price fü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, oder ERR_OUTSIDE_PRICE_FILTER, wenn sein Preis außerhalb des Filters liegt; dazu ERR_CURRENCY_NOT_AVAILABLE (Hotel rechnet in anderer oder unbekannter Währung), ERR_HOTEL_NOT_FOUND (kein Angebot der Kundengruppe) und ERR_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: nextCursor führt zur nächsten Seite: dieselbe Anfrage plus "cursor": "<nextCursor>"; pageSize darf 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 fehlt nextCursor; eine Seite kann dann auch leer sein. Bei sort=price und pricePerNight rechnet 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 ohne cursor neu beginnen. Ein Cursor von /v1/search gilt hier nicht.
  • Stand: stand wechselt, 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, mit coverage.standChanged: true und einem Hinweis in warnings. Dann (wie auch bei Buchungen anderer zwischen zwei Seiten) kann ein Hotel doppelt erscheinen oder fehlen: nach hotel deduplizieren. Die Preise jeder Seite gelten für ihren Stand; beim Buchen sichert priceCheck den 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 und nextCursor fü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/search die 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_LIMITED bzw. ERR_SEARCH_BUSY, mit Retry-After); eine mit 429 ERR_SEARCH_BUSY abgewiesene Suche verbraucht keinen Takt. Rechnet eine Suche länger als 10 s, endet sie mit 503 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_INTERNAL statt 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/price mit demselben Hotel, Zimmer, Verpflegung, An- und Abreise und derselben Belegung.
  • Passt zur offenen Suche: Bei gleichem Filter und gleichem stand ist best aus 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). Mit perBoard ist best die 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[].hotel aus 4a)
Feld
arrivalFrom, arrivalTo
Pflicht
ja
Bedeutung
Anreisefenster wie 4a.2; höchstens matrixMaxWindowDays Tage (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 boards muss 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 (Standard false: 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 (cells leer); fehlt ihm der Wert, ist jede Zelle none mit ERR_NO_CATEGORY, ERR_NO_REGION bzw. 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 (bei perBoard dann Verpflegung); jede Zelle mit checkIn, 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, immer available: 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 Hotelstamm ERR_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 Zelle unchecked), 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 Zugang 403 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). Fehlt room: 400 ERR_INVALID_BUCKET, mit und ohne priceCheck
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/booking zurü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 priceCheck ab
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 priceCheck wird 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 priceChecktotalCents = geprüfter Preis, currency = Vertragswährung, board = Verpflegung aus dem priceCheck
ohne priceChecktotalCents: null, currency: "", board: "" (leere Texte, kein Preis erfasst)
ohne referencecustomerReference fehlt
ohne metadatametadata fehlt
mit Key ohne Kundengruppegroup 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-Mode verlangt 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-Scenario mit 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), ref fehlt, priceCheck.tolerancePercent negativ, priceCheck.currency länger als 3 Zeichen (kürzer oder unbekannt: 422 ERR_CURRENCY_NOT_AVAILABLE); Content-API: since fehlt, Parameter mehrfach, lang mit 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/limits zeigt 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 (message nennt 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
board fehlt (/v1/price, /v1/search, priceCheck)
Aufrufer
Anfrage korrigieren
Code
ERR_NOW_MISMATCH
Status
422
Bedeutung
priceCheck.now ist nicht der Stichtag
Aufrufer
Feld weglassen
Code
ERR_VALIDATION
Status
422
Bedeutung
Text zu lang: idemKey, reference (128), leadPaxName (255), metadata.correlationId (64); message nennt Feld und Grenze
Aufrufer
Anfrage korrigieren
Code
ERR_QUANTITY_INVALID
Status
400
Bedeutung
quantity außerhalb 1–1.000.000
Aufrufer
Anfrage korrigieren
Code
ERR_INVALID_IDEM_KEY
Status
400
Bedeutung
idemKey fehlt (/v1/book, /v1/cancel)
Aufrufer
Anfrage korrigieren
Code
ERR_INVALID_BUCKET
Status
400
Bedeutung
room fehlt (/v1/book, mit und ohne priceCheck)
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
pageSize auß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/since unlesbar, verändert oder von einem anderen Key
Aufrufer
ohne cursor neu 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/search bzw. /v1/search/open ohne cursor: zu destination bzw. destinations[i] gibt es für diesen Key kein Hotel (Groß-/Kleinschreibung zählt)
Aufrufer
Code aus GET /v1/destinations nehmen (4.5)
Code
ERR_BAD_WINDOW
Status
422
Bedeutung
offene Suche und Termin-Matrix: arrivalFrom/arrivalTo fehlt oder arrivalTo liegt vor arrivalFrom
Aufrufer
Anfrage korrigieren
Code
ERR_BAD_NIGHTS
Status
422
Bedeutung
offene Suche und Termin-Matrix: nightsMin/nightsMax fehlt, < 1 oder nightsMin > nightsMax
Aufrufer
Anfrage korrigieren
Code
ERR_BAD_TARGET
Status
422
Bedeutung
offene Suche: destinations und hotels zugleich, leere Liste, leerer Code, oder keins von beiden, obwohl das Suchprofil nur einzelne Ziele erlaubt; Termin-Matrix: hotel fehlt
Aufrufer
Anfrage korrigieren
Code
ERR_BAD_SORT
Status
422
Bedeutung
offene Suche: sort unbekannt (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 oder minTotalCents > maxTotalCents, currency kein 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 (maxWindowDays bzw. matrixMaxWindowDays, message nennt 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 + 1 zu breit; Termin-Matrix: Dauer außerhalb der erlaubten Dauern (message nennt die Grenze)
Aufrufer
Dauer anpassen
Code
ERR_SEARCH_TOO_BROAD
Status
422
Bedeutung
offene Suche: zu viele destinations oder hotels in der Anfrage oder zu viele Hotels im Suchraum; Termin-Matrix: mehr Zellen als matrixMaxCells (message nennt die Grenze)
Aufrufer
eingrenzen
Code
ERR_CURRENCY_REQUIRED
Status
422
Bedeutung
offene Suche: die Hotels des Suchraums rechnen in mehreren Vertragswährungen, currency fehlt (message nennt sie)
Aufrufer
currency setzen
### 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 reference passt zu mehreren Buchungen (/v1/booking); message nennt die TA-…-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 zu boards/boardTypes); in der Suche Grund in diagnostics.reasons bzw. 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/prices Eintrag in rooms[].errors[], in der Suche Grund in diagnostics.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/prices Eintrag in rooms[].errors[], bei /v1/price ohne room in warnings, in der Suche in diagnostics.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.currency passt nicht zur Vertragswährung oder diese ist unbekannt; in der offenen Suche Grund in coverage.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 priceCheck ab
Aufrufer
neuen Preis anzeigen, mit neuem expectedCents buchen
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
idemKey schon mit anderen Buchungsdaten benutzt
Aufrufer
Fehler im Aufrufer: eindeutige Keys vergeben
Code
ERR_IDEM_KEY_RELEASED
Status
409
Bedeutung
idemKey gehört zu einer stornierten Buchung
Aufrufer
neuen idemKey nehmen
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/price ohne room oder /v1/prices hat die Zeitgrenze (2 s) überschritten
Aufrufer
auf /v1/price mit room ausweichen (für /v1/prices gilt die Frist immer, auch mit room und boards), 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 demselben idemKey
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/book mit demselben idemKey
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; message nennt 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. message nennt 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/since fehlt oder unlesbar, until kein gültiger Kettenschlüssel, max_bytes keine Zahl ≥ 65536, Parameter doppelt, Seite mitten in einem Stand ohne until, Folgeseite des full ohne epoch/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
epoch passt nicht (Feed neu aufgebaut) oder der Stand (since, until, seq der Quittung) liegt über dem aktuellen Lieferstand (größer als der letzte gelieferte Stand)
Aufrufer
full abrufen
Code
ERR_EXPORT_CURSOR_EXPIRED
Status
410
Bedeutung
Stand älter als die Aufbewahrung
Aufrufer
full abrufen
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
lang nennt eine Sprache, die keine Inhaltssprache des Veranstalters ist (bzw. am Katalog keine Beschriftungssprache); die angebotenen stehen in warnings
Aufrufer
Anfrage korrigieren
Code
ERR_CONTENT_CURSOR_EXPIRED
Status
410
Bedeutung
since liegt vor dem Aufbewahrungshorizont des Feeds (30 Tage)
Aufrufer
Verzeichnis neu holen, mit dessen feedToken weiter
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

RiegelGrenze (Voreinstellung)Antwort
Anfragen je API-Key200 je Sekunde, kurzzeitig bis 400 (Token-Bucket)429 ERR_RATE_LIMITED + Retry-After
gleichzeitige Suchen je Veranstalter8 (Rechenzeit: ein Viertel der Kerne, mind. 1)429 ERR_SEARCH_BUSY + Retry-After: 1
Rechenzeit einer Suche10 s503 ERR_SEARCH_TIMEOUT
Rechenzeit von /v1/price ohne room und /v1/prices2 s503 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-After ist in ganzen Sekunden (mindestens 1). Vorher nicht wiederholen; danach mit derselben Anfrage (bei /v1/book mit demselben idemKey).
  • Wer vor Ablauf von Retry-After erneut schickt und wieder abgewiesen wird, bekommt die 429 erst 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 einer 429 weiterschickt, 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/health hat 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:

  • idemKey ist 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, leadPaxName und priceCheck einer 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.now nicht mehr der Stichtag) oder nach einer Preisänderung. Nur ein neuer idemKey durchlä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 idemKey noch, 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_BUSY mit Retry-After – mit demselben idemKey wiederholen.
  • 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_BUSY mit Retry-After (Abschnitt 7.4). Es ist nichts gebucht bzw. storniert; nach der Wartezeit mit demselben idemKey wiederholen.
  • 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 5xx ist unklar, ob gebucht wurde: mit demselben idemKey wiederholen, nie mit einem neuen.

Storno:

  • Adressiert über den idemKey der 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/PfadAntwort
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/search und /v1/price fü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 ist 422 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 removed mit Grund (withdrawn:deleted, withdrawn:variant_error, withdrawn:not_exportable, withdrawn:no_currency, withdrawn:not_in_universe). Der Abnehmer löscht sie aus seinem Cache. Ein full hat immer removed: [] – 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 ist 00. 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 von Pattern gehören zur Nacht Start, jedes weitere Paar zur Folgenacht.
  • Pattern und minFree: SS, RR und 00 ergeben ausdrücklich minFree 0 (nicht buchbar); nur ** senkt minFree nicht, und -1 heißt: jede Nacht **. Das Minimum über die Nächte eines Aufenthalts ist availability.minFree von /v1/price fü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/since fehlt oder unlesbar, until kein gültiger Kettenschlüssel (fremd, abgelaufen, Zahl, beim full für einen anderen Stand), max_bytes keine Zahl ≥ 65536, Seite mitten in einem Stand ohne until, Stand über dem Kettenziel, Folgeseite des full ohne epoch/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
epoch passt nicht (Feed neu aufgebaut) oder since/until bzw. seq der Quittung über dem aktuellen Lieferstand (größer als der letzte gelieferte Stand; ein älteres since ist 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-After befolgt, 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 (E veraltet)
Antwort
429 ERR_RATE_LIMITED, Retry-After: 60
Abnehmer tut
Retry-After abwarten
Zeit
60 s
Anfrage
derselbe Abruf
Antwort
409 ERR_EXPORT_EPOCH
Abnehmer tut
full holen (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 epoch und to_seq als gespeichert. Eine Kette mit Seiten (full wie changes) wird erst am Kettenende (more: false) umgeschaltet; bricht sie ab, bleibt der alte Stand. Eine full-Kette beginnt immer mit from_seq: 0; eine full-Folgeseite schließt nur an die offene Kette an, nie an einen gespeicherten Stand – auch wenn ihr from_seq gleich dem gespeicherten to_seq ist (ein full trägt keine removed, gelöschte Hotels blieben sonst stehen).
  • Nie rückwärts: Maßgeblich ist das Ziel der Kette, also to_seq der letzten Seite (more: false), nicht das der ersten. Die erste Seite einer full-Kette trägt oft ein kleineres to_seq als der gespeicherte Stand (etwa nach 410: die ältesten Dateien kommen zuerst) und ist trotzdem kein Rückschritt. Eine full-Kette derselben epoch, deren Ziel kleiner ist als der gespeicherte Stand, oder ein full einer älteren epoch (die epoch ist eine ULID, zeitlich sortiert) wird abgelehnt – ein verspätet zugestelltes altes Paket setzt den Cache nicht zurück. changes müssen lückenlos an den Stand anschließen (from_seq = gespeichertes to_seq). Ein full eines 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 älteren epoch (etwa nach einem Restore auf einem Rechner, dessen Uhr nachgeht: die neue epoch-ULID ist dann kleiner) oder einem kleineren to_seq, setzen Sie den Stand bewusst zurück (Referenz-Empfänger: edf-empfaenger reset) und holen ein neues full. 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. 429 wartet 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 SellingData die 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) und Rounding 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/MaxCount zä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/@ApplyToOccupancy Min, Max oder Yes); ein fehlendes ExtraMaxApply heißt 1; mehrere Datumsfenster mit Operator="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 Feld configured von /v1/price hat im Cache keine Entsprechung: ein Zimmer ohne Kontingent steht dort als 00.
  • Vor der Buchung live: Der Cache kann einen Stand hinter der API liegen; eine im Cache offene, inzwischen gesperrte oder ausgebuchte Nacht lehnt /v1/book ab (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_INACTIVE wie 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 idemKey und Endpunkt (Buchung und Storno zählen getrennt): 503 ERR_BOOKING_BUSY mit Retry-After: 1, nichts gebucht bzw. storniert; die Wiederholung mit demselben idemKey läuft durch
Szenario
price_drift
Endpunkte
/v1/book mit priceCheck
Wirkung
erster Versuch je idemKey: 409 ERR_PRICE_DRIFT; der Preis selbst ändert sich nicht (message nennt 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_LIMITED mit Retry-After: 1
Szenario
search_busy
Endpunkte
/v1/search
Wirkung
erste Anfrage je Test-Key: 429 ERR_SEARCH_BUSY mit Retry-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.

WerkzeugAufrufArgumente
list_destinationsGET /v1/destinationskeine
search_hotelsPOST /v1/searchAnfrage-Körper; Seiten per cursor
price_offerPOST /v1/priceAnfrage-Körper
price_all_roomsPOST /v1/pricesAnfrage-Körper
sandbox_bookPOST /v1/bookAnfrage-Körper, idemKey Pflicht
get_bookingGET /v1/bookingref
sandbox_cancelPOST /v1/cancelAnfrage-Körper
get_limitsGET /v1/limitskeine
open_searchPOST /v1/search/openAnfrage-Körper
open_search_datesPOST /v1/search/open/datesAnfrage-Körper
hotel_detailsGET /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_KEY

Antigravity 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.

RouteZweck
GET /v1/content/hotelsVerzeichnis 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/catalogKataloge 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 ist 404 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 steht credit; bei attributionRequired: true muss 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": "{{*}}"
}
  • pageSize 1–1000 (Standard 500). Die nächste Seite holt man mit cursor=<nextCursor>; ohne nextCursor ist das Verzeichnis vollständig.
  • contentVersion zählt jede Änderung der Auslieferung eines Hotels; 0 = noch kein Inhalt (dann fehlt updatedAt). category ist die offizielle Landeskategorie, geo die Lage – beide nur, wenn gepflegt. websiteReady und missing wie im Inhalt eines Hotels (12.3).
  • feedToken ist der Stand, ab dem /v1/content/changes weitermacht. 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, feedToken und next sind undurchsichtig und an den Key gebunden: mit einem anderen Key, verändert oder nach einem Schlüsselwechsel des Servers 422 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 lang kommen alle Texte in allen Inhaltssprachen des Veranstalters. Mit lang (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 dann fallbackFrom mit 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 in warnings).
  • html enthält nur p, br, b, strong, i, em, ul, ol, li ohne Attribute. machineTranslated: true kennzeichnet eine maschinelle Übersetzung.
  • categories: kind official (Landeskategorie) oder operator (Einstufung des Veranstalters), scheme stars oder keys, value 1–5 in halben Schritten; „4 Superior“ ist value: 4, superior: true, nie 4,5.
  • geo.precision: address, street, locality oder unknown.
  • media in Anzeigereihenfolge, order: 1 ist das Hauptbild. Je Bild variants mit den erzeugten Breiten (w320 bis w2048, nie breiter als das Original), jeweils mit Breite, Höhe, Bytes und url; focus (x, y in Prozent) ist der wichtigste Bildpunkt für eigene Zuschnitte. id ist stabil, solange das Bild im Hotel bleibt.
  • amenities: Ausstattung mit Code aus dem Katalog (12.5). available: false heißt ausdrücklich nicht vorhanden; ein Merkmal, das fehlt, ist unbekannt. Je nach Merkmal mit count, distanceM, areaM2, ref (Flughafen-Code zur Entfernung) und bei kostenpflichtig möglichen Merkmalen charge (included, extra, unknown).
  • name, destination und giataCode kommen 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.
  • websiteReady und missing: Reifegrad „webseitenbereit“ – ob der Veranstalter genug Inhalt für eine Webseite gepflegt hat. Eine Kennzahl, sie ändert weder Verkauf noch Auslieferung. missing nennt 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 (neue contentVersion); die Konsole des Veranstalters zeigt dieselbe. Unbekannte Werte in missing ignorieren (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": "{{*}}"}
  • since ist der feedToken des Verzeichnisses oder next der vorigen Antwort (Pflicht, sonst 400 ERR_BAD_REQUEST). pageSize 1–1000 (Standard 500).
  • Je Eintrag code, change (upsert = Inhalt geändert, neu holen; removed = Hotel gelöscht), bei upsert die aktuelle contentVersion, und optional reason (languages = die Inhaltssprachen des Veranstalters haben sich geändert, media_ready = ein Bild ist fertig verarbeitet, contract = name, destination oder giataCode hat sich mit dem Vertrag geändert, deleted bei removed). 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 mit next weiterlesen.
  • 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 dessen feedToken weitermachen.

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": "{{*}}"
}
  • languages sind die Inhaltssprachen des Veranstalters, defaultLanguage seine Standardsprache. lang wählt die Beschriftungssprachen (de, en, tr; ohne lang alle).
  • Je Ausstattungsmerkmal group, valueType (flag, anzahl, meter, flaeche_m2, meter_mit_bezug), unit und chargeable. Ein Code wird nie umgedeutet; locked: true heißt entfallen (bleibt lesbar). version ändert sich mit jedem neuen Katalog; der Katalog trägt einen ETag.

12.6 Abgleich für Abnehmer

  1. Erstbefüllung: Verzeichnis seitenweise holen, feedToken und scopeHash merken, je Hotel den Inhalt holen und den ETag speichern.
  2. Laufend (etwa alle paar Minuten): changes?since=<token> (beim ersten Abruf feedToken, danach das zuletzt gemerkte next), geänderte Hotels neu holen, removed löschen, next merken. Ändert sich scopeHash: sofort Schritt 3.
  3. Täglich und bei 410: Verzeichnis neu holen und mit dem eigenen Bestand abgleichen (fehlende Hotels löschen, neue holen, contentVersion vergleichen).
  4. Inhalte immer mit If-None-Match holen.

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
full 1/h, neue changes-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), lang 1–5 Sprachen, Feed 30 Tage