Public API docs
Sign in
API docs

TourAPI – API Handbook v1

Buyer API v1: flow, rules, error catalogue and examples – every example runs as a test on every build.

Contents

Translation as of 2026-10-09. The German version is authoritative.

These docs can be read without signing in and contain no credentials. For tools and AI agents: llms.txt · llms-full.txt · handbuch.en.md · openapi.yaml

Search the handbook

For developers connecting a travel portal or a tour operator system to TourAPI. As of: 2026-09-30, API version v1; changes are listed in the changelog. Machine-readable: openapi.yaml (OpenAPI 3.1). For AI agents (Claude Code, Codex, Antigravity): a summary of the flow, formats and retry rules in the portal at /doku/agenten.md, filled in with your own access after signing in (page “AI agents”, download as AGENTS.md).

Every example in this handbook is a test. The requests are in beispiele/ (in the format of the JetBrains and VS Code REST clients) and run on every build against a fresh TourAPI instance with test data; the responses shown are compared with the actual ones.

CodeMeaningEndpoint
BAQuery availability and price, search/v1/search, /v1/price, /v1/prices
BAopen search without fixed dates (permission per key)/v1/search/open (section 4a)
BAdate matrix of a hotel (permission per key)/v1/search/open/dates (section 4b)
–effective limits of your own key/v1/limits (section 4c)
BBook/v1/book
–Read booking info/v1/booking
SCancel/v1/cancel
–Node health/v1/health (section 1.8)
–EDF delivery (cache export)/v1/export/edf/full, /changes, /ack (section 10)
–Sandbox: test keys, test bookings, scenariosall endpoints with a test key (section 11)
–Hotel content (master data, location, texts, images, amenities)/v1/content/hotels, /changes, /catalog (section 12)

Console/agents: not part of the Buyer API (POST /changes of the tour operator console, login via session, no API key; description and limits available from the operator on request).

1. Getting started

1.1 Access

  • Every request carries the API key in the X-Api-Key header. There is no session and no token.
  • The key is issued by the tour operator (access package). A key belongs to exactly one tour operator (tenant) and optionally to one customer group. The customer group determines special prices and, as an allotment group, the allotment booked against. A key without a group works on the base contract. The tour operator decides which kind a customer group is:
    • Allotment group (sales partner with an allotment): sees only the hotels for which it has an allocation, and books against that allocation. Other hotels do not exist for the key (404 ERR_HOTEL_NOT_FOUND, not in search or /v1/destinations). If it currently has no allocation, it sees no hotel.
    • Price group: sees all hotels of the tour operator and books from the general inventory like a key without a group – only at its own price.
  • If the special price of a customer group (with or without allocation) cannot be calculated for a hotel (faulty price promotion at the tour operator), the hotel does not exist for this group (404 ERR_HOTEL_NOT_FOUND, in search reason ERR_HOTEL_NOT_FOUND in diagnostics.reasons, /v1/book also rejects with 404) – never silently the base price.
  • Tenant, customer group and prices come only from the key, never from a parameter. Hotels of other tour operators do not exist for the key (404).
  • Missing, unknown or revoked key: 401 ERR_UNAUTHORIZED. Valid key of a suspended tour operator: 403 ERR_TENANT_SUSPENDED. Key of a deactivated customer group: 403 ERR_KEY_GROUP_INACTIVE (never silently the base contract).
  • Keys do not belong in browser code or logs. If you lose a key, have the tour operator revoke it – or revoke it yourself in the partner portal (if the tour operator has enabled it).
  • Keys in the partner portal: there you see the keys of your access, pick up live keys released by the tour operator, rotate live keys and revoke your own keys. The portal shows the raw value exactly once. The API rejects a revoked key within a few seconds (401).
  • Rotation: the new live key has the same rights (customer group, export, content, search profile). The previous one stays valid for a period set by the tour operator (default 24 hours, 1 hour to 7 days) and is rejected to the second afterwards (401 ERR_UNAUTHORIZED). Switch all systems to the new key within that period. You can rotate the new key again only once the previous one has expired – so no more than two keys are valid at the same time.
  • For development there are test keys (tk_test_…): same data as the live key, bookings only in the sandbox (section 11). Every response states the mode in the X-TourAPI-Mode header.

1.2 Base URL, transport, version

  • The base URL is in the access package; in the examples {{baseUrl}}.
  • All paths start with /v1. What may change within v1 is governed by the compatibility promise (section 1.9). Please ignore unknown response fields.
  • Requests: JSON in UTF-8, Content-Type: application/json, at most 1 MiB body. GET endpoints are /v1/booking, /v1/destinations, /v1/health and the EDF delivery (/v1/export/edf/full, /changes); all others are POST. Wrong method: 405 ERR_METHOD_NOT_ALLOWED.
  • Timestamps in responses (bookedAt, updatedAt) are RFC 3339 in UTC, e.g. 2026-09-26T08:15:03Z.
  • The server closes a connection after 15 s of reading or 15 s of writing. Exception: packages of the EDF delivery (/v1/export/edf/full, /changes) may write for up to 10 min (section 10).
  • An unknown path (e.g. a typo in /v1/…) returns 404 without a JSON body (text/plain, no errorCode); all other errors have the form from 1.6.

1.3 Date, stay, reference date

  • Date fields are calendar days JJJJ-MM-TT without time or zone.
  • A stay is half-open: checkIn is the first night, checkOut the departure day. checkIn=2026-10-26, checkOut=2026-10-28 are two nights.
  • The reference date is the server date in the Europe/Berlin zone. Early booking and date-range rules always use it. The request field now is only tolerated: leave it empty. If you send a different date, the query still uses the server date and says so in warnings; /v1/book rejects a differing priceCheck.now (ERR_NOW_MISMATCH).
  • Limits per request (applied before any calculation):
LimitValueCode (422)
Nights per stay1–30ERR_EMPTY_STAY, ERR_STAY_TOO_LONG
Earliest arrivaltoday (reference date)ERR_STAY_IN_PAST
Latest arrivalreference date + 732 daysERR_STAY_TOO_FAR
Travellers per request1–20ERR_NO_TRAVELLERS, ERR_TOO_MANY_TRAVELLERS
Age0–120ERR_INVALID_AGE

1.4 Money

  • All amounts are whole cents (…Cents, integer). 18000 = 180.00.
  • Rounding: commercial rounding to 2 decimal places, once per traveller (rounding in every price response: {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"} – the same rule the hotel's EDF declares). A traveller's line items are summed exactly (unrounded); only the sum is rounded: that is perTravellerCents[i], totalCents is their sum. A line item such as "−15 % on 89.90" is −13.485 and stays so until the sum. The exact amount per line item is shown in breakdown[].amountExact (section 3.1).
  • The currency is the hotel's contract currency (currency, ISO 4217). TourAPI does not convert. currency in the request is a preference: if it differs, the response comes in the contract currency with a note in warnings. If no currency is set on the contract, currency stays empty, also with a note – "EUR" is not guessed.
  • With /v1/book and priceCheck, by contrast, a currency mismatch is an error (ERR_CURRENCY_NOT_AVAILABLE): amounts without a common unit are not compared.

1.5 Occupancy

occupancy.travellers is the list of travellers of one room, each with their age on the arrival day. name and type may be sent but have no effect on the price. Child prices, full payers and minimum occupancy follow from the contract:

  • Whether someone is a child or an adult is decided by the room's child age band (e.g. 2–11 years: a 14-year-old pays the adult price), not by a fixed limit.
  • A room can require a number of full payers (e.g. 2 in a double room). If fewer adults travel, a child takes the free full-payer slot and pays the full base price (board at the child rate); child reductions apply only to children beyond that.
  • Tiers such as "1st child / 2nd child" count the children in the order the hotel defines (oldest or youngest first). The same order determines which child takes a free full-payer slot.
  • Anyone younger than the child age band is an infant: no base price for per-person pricing, never on a full-payer slot; board or a separate infant price only if the contract specifies them. For "from 3 persons" offers an infant counts only if the room counts infants towards occupancy.
  • No traveller pays less than 0. Percentage discounts apply to what the traveller owes after their child or person reduction (a free child stays free; an early booking −20 % on a −50 % child price hits half).

1.6 Responses, errors, warnings

  • Success: 200 with the result. Errors: 4xx/5xx with
    {"errorCode": "ERR_…", "message": "…", "warnings": ["…"]}

    errorCode is stable and intended for programs; message is a German text for humans and may change. Full list: section 7.

  • warnings (optional, also in success responses): what the API noticed about the request without rejecting it – ignored fields, differing currency, ignored now. Please log them. Many integration errors show up exactly there.
  • Processing time: Every response of an endpoint – including error responses and /v1/health – carries the header Server-Timing: tourapi;dur=<ms> (W3C Server Timing), e.g. tourapi;dur=12.4: milliseconds from the request reaching the endpoint until the response starts, including reading the request body and waiting at the rate limit and search gate (section 8), excluding the transfer of the response. The difference to the time you measure yourself is network and transfer. The value is information, not a commitment; if you do not need it, ignore the header.
  • Unknown fields: In query requests (/v1/price, /v1/prices, /v1/search) an unknown field is ignored and named in warnings. Within occupancy it is rejected (422 ERR_UNKNOWN_FIELD), because a typo there changes the price. /v1/book and /v1/cancel reject any unknown field.

1.7 The test world of the examples

The examples run against the operator's test world (golden seed): tour operator TEST-TENANT-A, hotels TEST-HOTEL-…, rooms DZ/EZ, board RO/BB/HB. Placeholders:

Placeholder
{{baseUrl}}
Meaning
Base URL
Placeholder
{{apiKey}}
Meaning
Key without a customer group (base contract).
Placeholder
{{rabattKey}}
Meaning
Key of the customer group TEST-PARTNER-DISCOUNT (−20 % on TEST-HOTEL-DISCOUNT, price group)
Placeholder
{{partnerKey}}
Meaning
Key of the allotment group TEST-PARTNER-BASE
Placeholder
{{poolKey}}
Meaning
Key of the allotment group TEST-PARTNER-POOL, without export permission
Placeholder
{{gesperrterKey}}
Meaning
revoked key
Placeholder
{{exportEpoch}}, {{exportSeq}}
Meaning
epoch and to_seq from the manifest of the export-voll example
Placeholder
{{ohnePreisRef}}, {{zweiteRef}}
Meaning
Booking references from the examples buchen-ohne-preispruefung and buchen-gleiche-kundenreferenz respectively
Placeholder
{{D0}}, {{D2}}, …
Meaning
Arrival day D0 of the test world plus n days. D0 = day of the first test data build + 30 days. D0 is only a fixed calendar day of the test data, not the reference date from 1.3 (that is always the server date)
Placeholder
{{heute}}, {{gestern}}
Meaning
Server date (= reference date), previous day
Placeholder
{{buchungsRef}}
Meaning
Booking reference from the buchen example
Placeholder
{{feedToken}}, {{inhaltCursor}}
Meaning
feedToken and nextCursor from the inhalt-verzeichnis example
Placeholder
{{inhaltEtag}}
Meaning
ETag from the inhalt-hotel example
Placeholder
{{*}}
Meaning
(responses only) any value, e.g. a timestamp

Test world prices (DZ, 2 adults, room only): first night 100.00, each further night 80.00 per room; breakfast +20.00, half board +35.00 per person and night. Exception TEST-HOTEL-KIND (destination ACE): price per person and night (DZ 89.90 / 79.90), children from 2 to 11 years −15 %.

1.8 Health: GET /v1/health

For load balancers and monitoring, without a key. 200 {"status": "ok"} means: the node has loaded the inventory, and its last sync with the database is fresh (default: younger than 60 s). Otherwise 503 with status degraded (sync too old, reason: "sync_stale") or down (inventory not loaded, reason: "view_not_loaded"), plus detail as text. The response contains no tour operator or inventory data. More than 10 calls per second from one sender IP are answered with a delay (at most 1 s, section 8).

If the node reads via a read replica, the response also states its lag in seconds (replica_lag_s). Above 5 s the status stays ok, and warnings then contains "replica_lag". 503 degraded is returned when sync age plus lag exceed the limit (reason: "replica_lag") or the lag cannot be measured (reason: "replica_lag_unknown").

Every response (including 503) also states the process uptime in seconds (uptime_s) and the build state (version, empty for a build without a state). Once the node has measured the database, db is present: "ok" with db_latency_ms (duration of one round trip, measured in the sync cycle, not on the health call) or "error" (last measurement failed). "error" alone does not flip the status — the node answers from its loaded inventory; with ok, "db_error" then appears in warnings, and if the sync stops, 503 degraded (sync_stale) follows after the limit.

"capacity_overlap" in warnings (status stays ok) means: the inventory check in the sync cycle found capacity periods that overlap per room type — an operator finding (inventory changed bypassing the write path), not an error in the request.

### gesundheit
GET {{baseUrl}}/v1/health
{
  "status": "ok",
  "db": "ok",
  "db_latency_ms": "{{*}}",
  "uptime_s": "{{*}}",
  "version": "{{*}}"
}

1.9 Compatibility promise for /v1

Within /v1 the API changes only additively:

May be added within /v1Never within /v1 (that would be /v2)
new optional request fieldsnew required request fields
new response fieldsremoving or renaming fields
new endpointschanging the type or meaning of a field
new ERR_* codes, each with documented handling in the error cataloguechanging the meaning of an existing ERR_* code

What the buyer has to do in return:

  • Ignore unknown response fields, do not reject them.
  • Handle unknown ERR_* codes by HTTP status (4xx: do not retry blindly, 5xx/429: retry with backoff, section 7).
  • message and warnings are texts for humans and not part of the promise.

Fixed within /v1: timestamps are RFC 3339 in UTC (Z), amounts are whole cents (1.4), text lengths are checked before writing (422 with field name, never 500, appendix), /v1/health needs no key (1.8).

1.10 Signing in to the partner portal

The portal (documentation, your own accounts and keys) is not part of the API; the API needs only the key. Signing in to the portal works like this:

  • The tour operator creates the portal account. You receive a user name and a one-time password (valid for 30 days).
  • First sign-in: user name and one-time password, then set up the second factor (authenticator app per RFC 6238: scan the QR code or type the key, enter the six-digit code), then choose your own password. Without a valid one-time password the second factor cannot be set up.
  • Every later sign-in: password and current code. A code is valid only once.
  • Wrong codes: after 5 the sign-in ends. 10 failed attempts within 7 days lock the account, even for the correct code. Only the tour operator can unlock it.
  • Locked or phone lost: contact the tour operator. “Lift lockout” releases the account with the existing second factor. “Reset 2nd factor” issues a new one-time password; you then set up the second factor and your password again, the old password no longer works.
  • Portal and tour operator console are separate: portal credentials do not work in the console.

2. Integration flow

/v1/destinations
  -> /v1/search
  -> /v1/price | /v1/prices
  -> /v1/book        idemKey, priceCheck
  -> /v1/booking
  -> /v1/cancel      idemKey
  • /v1/destinations: valid destination codes of the key.
  • /v1/search: bookable hotels with from-price and availability.
  • /v1/price: price information for hotel, room, board and occupancy (binding only with /v1/book); /v1/prices: all boards (and rooms) at once.
  • /v1/book: books, with idemKey and priceCheck (the price just quoted).
  • /v1/booking: reads the booking via our reference or your own.
  • /v1/cancel: cancels via the same idemKey.

Rules you need to know:

  1. Search and price are informational; only /v1/book is binding. Between price and booking the tour operator can change prices or allotment. With priceCheck, /v1/book rejects a changed price (409 ERR_PRICE_DRIFT) instead of silently booking at the new one.
  2. Every booking has its own idemKey (assigned by the caller, e.g. your own transaction number). Retries with the same key never book twice (section 9).
  3. Do not retry errors blindly. Which errors are worth a retry is stated in the error catalogue (column "Caller").

3. Price: /v1/price and /v1/prices (BA)

3.1 POST /v1/price – one price

Prices exactly one room of a hotel for one stay, one board and one occupancy.

Field
hotel
Required
yes
Meaning
Hotel code
Field
room
Required
no
Meaning
Room code. If omitted, the API takes the cheapest available room that allows the occupancy and offers the board; if none is available, the cheapest of those (then available=false, 3.3). A room whose contract data the calculation core rejects (7.5, e.g. ERR_INVALID_AMOUNT, ERR_NO_SECTION) is skipped and named in warnings with room and code; if no room calculates, the error comes as 422. With room, that room's error always comes as 422.
Field
board
Required
yes
Meaning
Board code, exactly as in the contract (RO, not ro). Missing: ERR_BOARD_MISSING; if the room (without room: no room) does not offer it: 422 ERR_BOARD_NOT_OFFERED
Field
checkIn, checkOut
Required
yes
Meaning
Stay (section 1.3)
Field
occupancy.travellers[]
Required
yes
Meaning
Travellers with age
Field
currency
Required
no
Meaning
Preferred currency, note only (1.4)
Field
now
Required
no
Meaning
tolerated, ignored (1.3)

Response:

Field
room
Meaning
the priced room (without room in the request: the cheapest available); pass it like this to /v1/book
Field
currency
Meaning
Contract currency, empty = not set on the contract
Field
totalCents
Meaning
Total price of the room for the stay
Field
rounding
Meaning
Rounding rule of the response (section 1.4)
Field
perTravellerCents[]
Meaning
Price per traveller, ordered by age descending (oldest first, same age in request order): the exact sum of their line items, rounded once. Sum = totalCents. With room prices (per-unit price) the first traveller carries the room line items; board is shown with the respective traveller.
Field
breakdown[]
Meaning
Line items: chargeType (BaseCharge, PhantomBaseCharge, GuestCharge, BoardCharge, Extra), code, traveller (index in perTravellerCents), night (index from 0, -1 = per stay), amountExact (exact amount as a decimal, e.g. "-13.485"), amountCents, for extras applianceCode, for extras of an extra family (several extras with the same applianceCode) additionally variant (the variant; only both together identify the extra), for the line items of a free night additionally freeNight: true (this night is waived in full or in part, e.g. "7=6": the seventh night carries one line item per traveller that cancels the base price and – depending on the offer – board). Order: night, within it traveller, within that calculation step (base price, person discount/surcharge, board including its person discount, extras); per-stay line items last.
Field
separateExtras[]
Meaning
only if present: separately listed extras. inTotal=true: mandatory extra, included in the total price. inTotal=false: optional extra, not in the total price. code identifies the extra, for an extra family together with variant. amountCents is the exact sum of the extra, rounded once; with inTotal=true it can differ from the sum of its breakdown rows by several cents – breakdown is authoritative.
Field
availability
Meaning
configured (allotment set up), available (every night open), minFree (smallest free count across the nights, exact up to 99; -1 = more than 99 free every night or free sale; 0 = at least one night not bookable)
Field
warnings[]
Meaning
Warnings (1.6); without room also one per skipped room with a contract error: 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}
}

Without room the API looks for the cheapest matching room – for one person, here the EZ; room in the response names it.

### 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}
}

Rooms that are sold out, blocked, have no allotment set up or – with a customer group's key – are not allocated are skipped by the selection without room, even if they are cheaper: in the hotel below the EZ (70 EUR) has no allotment and is not bookable, so the API takes the more expensive, bookable 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}
}

Price per person with room prices. Many contracts price the room, not the person (per-unit price). Then all room line items – base price, extras per room or per transaction – are in the first traveller's pot in perTravellerCents, and the others carry 0 (in the preis-einzeln example: [18000, 0]). Board is always a price per person and night, even with a per-unit price: it is shown with the respective traveller (with breakfast [22000, 4000], see preis-alle-verpflegungen); a single traveller pays the full room price but only one board. A fixed discount per person (e.g. early booking −20 € per person) reduces the room price with a per-unit price and is therefore also in the first pot. This is intentional and will stay so (EDF 5.1.6: "object-based additional services are attributed to the 1st person"; each pot is rounded once, the sum is always exactly totalCents). The first pot belongs to the oldest traveller – perTravellerCents and breakdown[].traveller are ordered by age descending, not by request order. Person-related line items (e.g. child prices) are shown with the respective traveller. perTravellerCents is therefore not meant for a "per person" display: divide totalCents by the number of travellers and round it yourself.

The price depends on the key: the same request with a customer group's key returns its special price (here −20 %). TEST-PARTNER-DISCOUNT is a price group: it books from the general inventory, and availability is the same as without a group (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}
}

Per-person price with child reduction: −15 % on 89.90 is −13.485 – a fraction of a cent. The line item stays exact (amountExact); the child's sum is rounded once: 89.90 − 13.485 + 79.90 − 11.985 = 144.33 → perTravellerCents[2] = 14433. A traveller's amountCents are distributed so that they add up exactly to their price: each row is the traveller's rounded running sum up to this row minus that up to the previous row (8990, −1348, 7990, −1199 = 14433). Rounding line items individually and adding them (−13.49, −11.99) gives 144.32 – that is not the price.

### 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}
}

Free nights ("7=6"). The line item that waives a night is an Extra with the offer's applianceCode and freeNight: true; it is included in the total price and appears in the waived night per traveller directly after that traveller's base price. In the test world (TEST-HOTEL-FREINACHT, DZ, room only, price per person: 100.00 first night, 80.00 each further night, checkIn = {{D0}}) 2 adults pay 1000.00 instead of 1160.00 for 7 nights: night 6 (the seventh) carries per traveller {"chargeType": "Extra", "code": "PN", "night": 6, "amountExact": "-80.00", "applianceCode": "FSO1", "freeNight": true}. With half board the same line item also waives the board (-115.00). 14 nights give two free nights (nights 12 and 13), 6 nights none.

3.2 POST /v1/prices – all boards at once

Like /v1/price, but instead of board optionally boards[] (if omitted: all boards of the room) and room optional (if omitted: all rooms). Rooms that do not allow the occupancy are dropped; if none allows it, the reason comes as an error (ERR_OCCUPANCY_NOT_ALLOWED). A requested board that no room offers: 422 ERR_BOARD_NOT_OFFERED. The board field does not filter here: if you send it, you still get all boards and a note in warnings (example below); filtering works only with boards[].

Response: currency, rounding, rooms[] with room and boards[] (per board board, globalType (board type as in the EDF export and the open search: AO for RO, otherwise the tour operator's mapping, otherwise the code itself if it is a board type, otherwise XX = not assignable), totalCents, perTravellerCents, separateExtras, availability), per room in ascending price order; warnings. No breakdown.

If a room's board does not calculate for this occupancy because the contract is faulty there (ERR_NEGATIVE_TRAVELLER_PRICE, ERR_NEGATIVE_PERCENT_BASE, section 7.5) or because it is not sold for this travel party (ERR_BOARD_NOT_AVAILABLE, 3.5), only this combination drops out: it appears without a price in rooms[].errors[] (board, globalType, errorCode, message) and in warnings; the rest of the matrix stays priced, and the room's boards[] may then be empty. The same applies to any other contract error of a room (section 7.5, e.g. ERR_NO_SECTION: no season in the contract for a night): the affected boards appear with the code in rooms[].errors[] and in warnings, all other rooms stay priced, with the same prices as /v1/price with room and board. If not a single combination calculates, the code comes as an error (422). Errors in the request itself still reject it as a whole.

Time limit: /v1/prices and /v1/price without room calculate several rooms or boards; if this exceeds 2 s (in normal operation it takes a few milliseconds), the response is 503 ERR_PRICE_TIMEOUT – never half a matrix or a "cheapest" room from a subset of the rooms. Remedy: specify room or boards. The deadline always applies to /v1/prices, even with room and a single board; only /v1/price with room has no deadline of its own and is therefore the safe way out. The 503 carries no Retry-After: narrow down first, then retry. If the caller closes the connection, the calculation is aborted and there is no response.

### 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}}
      ]
    }
  ]
}

With board instead of boards[] you get the same matrix as without it, plus the warning 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 Availability in price responses

availability is always set and refers to the priced room across all nights. A price with available=false is price information, not an offer: the booking fails (section 5.3). What is counted is exactly what /v1/book sells against: free capacity per night after bookings and daily status, and with an allotment group's key additionally its allocation (what the group has already booked is deducted; without an allocation for room and night nothing is free). A price group counts like a key without a group (1.1). minFree is the smallest of these across the nights.

Per night TourAPI counts exactly up to 99. What each night contributes to minFree:

NightContribution to minFreeAllotment file (section 10)
1 to 99 units freethe count01–99
more than 99 free or free saledoes not lower minFree**
Stop sale0SS
On request0RR
sold out, closed, no capacity, customer group without allocation for room and night000

minFree is -1 only if every night has more than 99 free or is in free sale; a single night with stop sale, on request or 0 makes minFree 0 (available=false). This is the same representation as in the EDF allotment of the cache delivery, so that cache and API show the same number.

  • configured=true: TourAPI knows an allotment for this room. That says nothing yet about each individual night: a night for which no capacity is entered makes the room available=false (minFree 0), but it stays configured=true.
  • configured=false: no allotment at all is set up for this room; booking is not possible.

If only individual nights lack capacity, search and /v1/book give the same reason: ERR_NO_INVENTORY (search 4.2, booking 5.3). What counts for "bookable" is available, not configured.

availability is a snapshot, not a commitment; only /v1/book checks bindingly. A booking or a cancellation is reflected by the server node that executed it in the next request (minFree drops or rises). Other nodes, bookings from other channels and contract changes catch up after a few seconds – as does the own node in the rare case that it could not reload the new state immediately (the booking itself is still valid).

3.4 Sales rules of the room: minimum stay, arrival days, sales window, release

A hotel contract can define which stays the hotel accepts, for example "high season at least 7 nights, arrival on Saturdays only", "half board only for arrival until 31.10." or "release 14 days" (book at the latest 15 days before arrival). Such rules do not change any price; they decide whether a room is offered for the requested stay. A stay excluded by a rule gets no price:

CodeThe rule requires …Caller
ERR_STAY_LENGTH_NOT_ALLOWEDa different length of stay (minimum or maximum nights)change the length
ERR_ARRIVAL_DAY_NOT_ALLOWEDa different arrival or departure weekdayshift the travel dates
ERR_TRAVEL_DATES_NOT_ALLOWEDtravel dates within a specific period (sales window)different period
ERR_BOARD_NOT_ALLOWEDa different board for this staydifferent board
ERR_LEAD_TIME_NOT_ALLOWEDmore lead time: the arrival lies within the release periodlater arrival

message names room, rule and requirement, such as "Zimmer DZ, Verkaufsregel 1: wenn Anreise 2026-07-01 bis 2026-08-31, verlangt mindestens 7 Nächte". If a stay violates several parts of a rule, this order applies: length, weekday, period, board, release.

Release (lead time). A rule with a release of n days requires more than n calendar days between the reference date (server date, 1.3) and arrival: with release 3 and reference date 10.05., arrival on 13.05. is still closed, from 14.05. it can be booked; with release 0 only arrival on the reference date itself is closed. The release can be tied to a period (e.g. "from September 14 days"): it then applies as soon as one night of the stay lies in that period, measured up to arrival. If the rule refers to departure (ApplyTo Departure), the release is measured up to departure: more than n calendar days are then required between reference date and departure. message names the reference date, arrival and the lead time. A stay closed today cannot open tomorrow; retrying does not help.

  • /v1/price: without room, a room whose rule excludes the stay drops out of the selection; if no room accepts it, the reason comes as 422. It takes precedence over another room's ERR_OCCUPANCY_NOT_ALLOWED because it says more precisely what to change.
  • /v1/prices: rules can apply per board; only the affected board is dropped. If nothing remains, the reason comes as 422.
  • /v1/search: the hotel is dropped, the code appears in diagnostics.reasons.
  • /v1/book: with priceCheck the rule applies to the requested board; without priceCheck (board unknown) the booking is sold as soon as one board of the room accepts the stay.

ERR_ROOM_RESTRICTION_INVALID (422) means: a rule in the contract cannot be evaluated. This is a contract error – report it.

3.5 Board only for certain travel parties

A board surcharge can be tied to the composition of the travel party, e.g. "half board surcharge applies to parties with at least 2 adults". If the requested occupancy does not match, this board is not bookable for this occupancy: 422 ERR_BOARD_NOT_AVAILABLE. TourAPI never calculates a price for a board without the board. message names board, traveller, night and the required party. This is not a contract error; a different board or occupancy may calculate (caller: different board or occupancy).

  • /v1/price: without room, the room drops out of the selection for this board (as with a sales rule, 3.4); if no room calculates, the reason comes as 422.
  • /v1/prices: only this board of the room drops out, as an entry in rooms[].errors[] with errorCode (3.2); the rest stays priced.
  • /v1/search: the hotel is dropped, the code appears in diagnostics.reasons.
  • /v1/book with priceCheck: 422, nothing is booked.

4. Search: /v1/search (BA)

4.1 Request and response

Finds the tour operator's bookable hotels for one stay, one board and one occupancy, each hotel with the cheapest available room (like /v1/price without room): a cheaper sold-out room does not drop the hotel from the list as long as another room is bookable.

FieldRequiredMeaning
destinationnoDestination or airport code of the hotel (below); empty = all hotels
board, checkIn, checkOut, occupancy, currency, nowas /v1/price
includeUnavailablenotrue: non-bookable hotels are included, flagged (default false)
pageSize, cursornoPages, section 4.3

Response:

Field
results[]
Meaning
Hits of the page, ascending by fromTotalCents, ties by hotel code
Field
results[].hotel, name, room
Meaning
Hotel and the room the price refers to
Field
results[].fromTotalCents, currency
Meaning
From-price (cheapest available room; with bookable=false the cheapest overall) in the hotel's contract currency
Field
results[].availability
Meaning
as with /v1/price
Field
results[].bookable
Meaning
true = all nights available
Field
results[].reason, priceInformational
Meaning
only with bookable=false (only with includeUnavailable): reason and "price information only" flag
Field
diagnostics
Meaning
per page: considered = hotels checked = returned + skipped; reasons[] = why hotels were dropped, per reason with count; timeBudgetExhausted = page cut short due to the time budget (4.3); roomErrors[] = rooms of the checked hotels that were skipped due to a contract error (7.5), per code with count – even if the hotel matches with another room
Field
nextCursor
Meaning
set as long as further hotels remain to be checked (4.3)
Field
warnings[]
Meaning
Warnings; with mixed contract currencies a collective warning

The search does not name the hotel codes of dropped hotels, only reasons and counts. An empty hit list therefore does not mean "no inventory": diagnostics.reasons tells whether e.g. the season does not match (ERR_NO_SECTION), everything is sold out (ERR_NOT_AVAILABLE), no allotment is set up for a night (ERR_NO_INVENTORY) or no room of the hotel offers the requested board (ERR_BOARD_NOT_OFFERED).

Where the codes for destination come from: Every hotel has a destination code (e.g. PMI) in the tour operator's contract and optionally airport codes; a hotel matches if destination is exactly equal to one of them (case-sensitive). Destination codes consist only of A-Z a-z 0-9 . _ - (1 to 64 characters, no free text); airport codes are IATA codes of three capital letters. The valid codes for the key are returned by GET /v1/destinations (4.5). A code for which there is no hotel for this key is an error: 422 ERR_UNKNOWN_DESTINATION (example suche-unbekanntes-ziel), never a silently empty list. The check is done on the first page (without cursor); if the destination disappears while paging, the search ends with an empty final page (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}
}

The same code in lower case is a different, unknown destination for the API:

### 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 Non-bookable hotels

By default the search returns only bookable hits and counts the rest in diagnostics. With includeUnavailable=true, non-bookable hotels are returned with bookable=false, reason and priceInformational=true. The reasons mean the same as with /v1/book:

  • ERR_NO_INVENTORY: for at least one night no allotment at all is set up – none at all for the room (configured=false) or just not for this night. /v1/book rejects the same stay with ERR_NO_INVENTORY.
  • ERR_NOT_AVAILABLE: every night has an allotment, but not every night is open (sold out, stop sale, closed, on request, customer group allocation exhausted or not present). Which case exactly is named by /v1/book (ERR_SOLD_OUT, ERR_STOP_SALE, ERR_INVENTORY_CLOSED, ERR_GROUP_LIMIT, section 5.3).

In the example TEST-HOTEL-SOLD has no capacity for the night, TEST-HOTEL-STOP is on 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 Pages and cursor

  • A request checks one page of at most pageSize hotels (default 50, at most 100; outside 1–100: 422 ERR_BAD_PAGE_SIZE) in hotel code order.
  • If there are more hotels, nextCursor is in the response. The next page: the same request plus "cursor": "<nextCursor>". On the last page nextCursor is absent.
  • Price sorting applies per page. If you need a complete list by price, page through and sort yourself. diagnostics applies per page.
  • The cursor is opaque and bound to request and key. Different request (destination, period, occupancy, board, includeUnavailable) or different key: 422 ERR_CURSOR_MISMATCH. Unreadable or expired cursor (e.g. after a server restart): 422 ERR_BAD_CURSOR – then start over without cursor. pageSize and currency may change between pages. The length of the cursor is fixed and says nothing about the hotel.
  • Truncated page: Under load a page does not start another hotel after 60 ms. It is then shorter than pageSize, diagnostics.timeBudgetExhausted=true, and nextCursor leads on. This is not an error: just keep paging.
  • If you send neither pageSize nor cursor and there are more hotels, there is also a note in warnings – the list is then only the first page.
### 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 Search load and time limits

Search is the most expensive request. Per tour operator up to 8 searches run concurrently; they share their computing time (one quarter of the server's cores per tour operator), so that a noisy tour operator does not slow down the others. If this quota is full, the response is immediately 429 ERR_SEARCH_BUSY with Retry-After: 1. A search that computes for longer than 10 s aborts with 503 ERR_SEARCH_TIMEOUT – never with a silently shortened list (a page truncated under load, by contrast, always has nextCursor, 4.3). Remedy: set destination or use a smaller pageSize. If the caller closes the connection, the search aborts its calculation and no longer responds.

4.5 GET /v1/destinations – valid destination codes

Returns all codes that destination in this key's search can filter on: destination and airport codes of the tour operator's hotels, sorted ascending, each code once. Only its own – a key never sees destinations of another tour operator, and an allotment group's key sees only the destinations of its hotels with an allocation (1.1; without an allocation the list is empty). No parameters; the list changes when the tour operator adds or removes hotels – for an allotment group's key also when the tour operator changes its allocations, and for any customer group's key when the tour operator switches the group's kind.

FieldMeaning
destinations[].codeCode, to be passed exactly like this in destination
destinations[].namePlain text for display; absent if TourAPI does not know the code
warnings[]Warnings, e.g. about parameters sent along (they are ignored)
### 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. Open search: POST /v1/search/open (BA)

4a.1 What for

“Where is it cheapest between 1 and 30 October for 5 to 7 nights?” – without a hotel code and without fixed dates. The open search returns the best offer per hotel across all arrival days of the window, all lengths of the range, all rooms and boards, sorted globally across all pages. Two promises:

  • The price is exact: best.totalCents and best.perTravellerCents are bit-identical to /v1/price with the same hotel, room, board, arrival and departure and the same occupancy. No from-prices from a cache, no sampling.
  • The order is proven: no hotel that was not shown has a better best offer than the last result. Under load only the page gets shorter (4a.4), never the price less exact or the order guessed.

Flow of a website: open search (list) → “dates & prices” of a hotel with the date matrix (4b, same window) → chosen offer in detail with /v1/price (hotel, room, board, checkIn, checkOut from best or the cell, the same occupancy: the same price including its breakdown; other boards of these dates one board at a time with /v1/price without room) → /v1/book with priceCheck.expectedCents = the price shown. There is no offer token: the booking checks the price anyway.

Permission: The open search is enabled per key (off for new keys). The tour operator admin sets the search profile in the key (window, lengths, destinations, page size, time budget, rate); at most what the operator grants the tour operator applies. Without permission: 403 ERR_OPEN_SEARCH_NOT_ALLOWED. The API checks the profile's limits per field and names them in the error message (4a.5); GET /v1/limits returns them machine-readably (4c).

4a.2 Request

Field
destinations
Required
no
Meaning
Destination codes as in GET /v1/destinations (4.5), union; at most as many as the profile allows
Field
hotels
Required
no
Meaning
Hotel codes of the key; not together with destinations. Without either: the key's whole inventory – only if the profile allows all destinations
Field
arrivalFrom, arrivalTo
Required
yes
Meaning
Arrival window, both days inclusive; arrivalFrom ≥ reference date, arrivalTo ≤ reference date + 732
Field
nightsMin, nightsMax
Required
yes
Meaning
Length from–to (1–30 and within the profile); every length in between counts
Field
occupancy
Required
yes
Meaning
one room, as for /v1/price
Field
boards
Required
no
Meaning
Board codes exactly as in the contract; without = all
Field
boardTypes
Required
no
Meaning
Board type as in the EDF export (AO, BB, HB, HB+, FB, FB+, SC, AI, AI+, XX); together with boards: both must match
Field
minTotalCents, maxTotalCents
Required
no
Meaning
Filter on the total price, limits inclusive
Field
currency
Required
conditional
Meaning
Filter on the contract currency, no conversion. Required if the hotels of the search price in several currencies (422 ERR_CURRENCY_REQUIRED)
Field
category
Required
no
Meaning
official category (national classification from the hotel master data): scheme stars or keys (required), min/max level 1 to 5 in half steps as a number (e.g. 3.5, equivalently 3.50), limits included (without: 1 or 5). "4 Superior" counts as 4
Field
regions
Required
no
Meaning
regions from the hotel master data (address), one of them; compared exactly as maintained (case matters); at most 50
Field
geo
Required
no
Meaning
radius: lat, lon (decimal degrees as a number, at most 6 decimal places) and radiusKm (0.001 to 500, at most 3 decimal places). Distance on the sphere (haversine, earth radius 6371 km), rounded to whole metres; the edge counts as inside. Correct across the date line and at the poles
Field
sort
Required
no
Meaning
price (default: total price), pricePerNight (price per night, compared exactly as a fraction, without rounding), hotel (hotel code)
Field
pageSize
Required
no
Meaning
Results per page, 1 up to the profile; if omitted 20 (or fewer if the profile allows fewer)
Field
cursor
Required
no
Meaning
nextCursor of the previous page, unchanged (4a.4)

Unlike the other query endpoints, the open search rejects every unknown field (422 ERR_UNKNOWN_FIELD) – otherwise a typo in a filter would silently change the result list. There is no now; the server's reference date applies (1.3).

Filters on hotel master data (category, regions, geo; combined with AND) apply before the calculation: a hotel outside does not belong to the search scope – it counts neither in hotelsInScope nor against the hotels per request of the search profile (ERR_SEARCH_TOO_BROAD), so a radius across the whole inventory works too. If a hotel lacks the value a filter needs (no official category, no region, no coordinates) and no existing value excludes it, it counts in the search scope with the reason ERR_NO_CATEGORY, ERR_NO_REGION or ERR_NO_GEO (first missing value in this order) – gaps in the hotel master data stay visible. The tour operator maintains the values in the hotel master data (console: hotel → Content → Master data & location); a change takes effect after the next sync (seconds) and changes stand (4a.4).

4a.3 Response

Field
results[].hotel
Meaning
Hotel code; order by the criterion of sort, ties by hotel code
Field
results[].best
Meaning
best offer: checkIn, checkOut, nights, room, board, boardType (board type as in the EDF export), currency, totalCents, perTravellerCents, availability (as for /v1/price, always available: true)
Field
results[].alternatives
Meaning
Pre-check without price: dates = dates (arrival × length) with at least one offer according to availability, sales rules, season and occupancy, boards/boardTypes/rooms accordingly. An upper bound: the price calculation may still exclude some of them. Never read it as a statement about prices or results
Field
coverage.complete
Meaning
true: page full or list finished. false: the time budget shortened the page (4a.4)
Field
coverage.timeBudgetExhausted
Meaning
Page shortened because of the time budget (= complete: false)
Field
coverage.standChanged
Meaning
the data has changed since the previous page (4a.4)
Field
coverage.hotelsInScope
Meaning
Hotels in the search scope (destinations or hotels, inventory of the key, filters on hotel master data; hotels without the filtered value count too, 4a.2)
Field
coverage.hotelsFeasible
Meaning
of those, with at least one date according to the pre-check – upper bound of the number of results (“up to N hotels”), not a result count
Field
coverage.hotelsPriced
Meaning
hotels calculated exactly on this page (also depends on how many hotels the server calculates in parallel; therefore left open in the examples)
Field
coverage.undecided
Meaning
Hotels whose position was still open when the time budget ran out (0 when complete)
Field
coverage.reasons[]
Meaning
Hotels without a single date, per reason – for the whole search scope, the same on every page. If no room offers a matching board: ERR_BOARD_NOT_OFFERED. As with /v1/search, availability comes next: if no room has even one free date, it is the reason (ERR_NO_INVENTORY, ERR_NOT_AVAILABLE). Otherwise what /v1/price answers for the first free date (room, board, arrival, length in this order) applies, e.g. ERR_OCCUPANCY_NOT_ALLOWED, ERR_NO_SECTION, ERR_STAY_LENGTH_NOT_ALLOWED, or ERR_OUTSIDE_PRICE_FILTER if its price lies outside the filter; in addition ERR_CURRENCY_NOT_AVAILABLE (hotel prices in a different or unknown currency), ERR_HOTEL_NOT_FOUND (no offer for the customer group) and ERR_NO_CATEGORY, ERR_NO_REGION, ERR_NO_GEO (the hotel lacks the value of a filter on hotel master data, 4a.2)
Field
coverage.priceReasons[]
Meaning
Hotels that dropped out only during the exact calculation on this page, per reason (e.g. ERR_OUTSIDE_PRICE_FILTER)
Field
coverage.roomErrors[]
Meaning
Rooms with a contract error (7.5), per code – never silently skipped
Field
stand
Meaning
Identifier of the data state of this page (opaque)
Field
nextCursor
Meaning
set as long as further results may follow
Field
warnings[]
Meaning
Notes, e.g. about a change of the data state

Best offer of a hotel: the minimum of the criterion (total price or price per night) over all dates × rooms × boards of the filter, bookable offers only. On a tie the earlier arrival wins, then the shorter length, then room and board in the order of the contract. With sort=hotel the criterion is the total price.

### 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": "{{*}}"
}

The next page is the same request with 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": "{{*}}"
}

Across the whole inventory, by price per night, only breakfast or half board. Hotels without allocation in the window appear as a reason 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": "{{*}}"
}

With filters on hotel master data across Palma and Menorca: at least 3.5 stars, region Mallorca, 25 km around Palma. The 3-star hotel in Alcúdia drops out, the two hotels on Menorca without maintained master data appear as a reason:

### 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 Pages, cursor, data state, time budget

  • Pages: nextCursor leads to the next page: the same request plus "cursor": "<nextCursor>"; pageSize may change. The next page continues after the last result (criterion, then hotel code) – all pages together are one sorted list. On the last page nextCursor is missing; that page may also be empty. With sort=price and pricePerNight a next page does not recompute the hotels of earlier pages: even deep into the list a page costs about as much as the first one.
  • Cursor: opaque, bound to request and key, valid for 15 minutes. Different request (any field except pageSize) or different key: 422 ERR_CURSOR_MISMATCH. Unreadable, expired or after a server restart without a fixed cursor key: 422 ERR_BAD_CURSOR – then start over without cursor. A cursor from /v1/search is not valid here.
  • Data state: stand changes when the tour operator publishes contracts, promotions or allocations or changes the region, coordinates or official category of a hotel in the hotel master data, and at midnight (reference date), but not on bookings or other content (texts, images). If the data state has changed since the previous page, the next page continues from the same position in the new state, with coverage.standChanged: true and a note in warnings. Then (as also with bookings by others between two pages) a hotel may appear twice or be missing: deduplicate by hotel. The prices of each page apply to its data state; when booking, priceCheck secures the price.
  • Time budget: Each page has a time budget (search profile, e.g. 150 ms), counted from the arrival of the request. Once it is used up and the page already has a result, it calculates no further hotels; results whose position is already fixed are still included. Then: coverage.complete: false, timeBudgetExhausted: true, undecided > 0 and nextCursor for the rest. The results shown are still exact and in proven order – just keep paging. A page never ends without a result as long as there is one. Shortened pages are not an error. They occur mainly with very large searches (a long arrival window with a wide range of stay lengths in the largest destination) while several searches of the same tour operator are running at the same time. If you need full pages, query smaller windows or ranges, or search one after another instead of in parallel.
  • Load: The open search shares the search slots and computing time of the tour operator with /v1/search (4.4), but occupies at most half of the search slots (4 of 8; all keys of the tour operator and both endpoints together) – the rest stays free for /v1/search. In addition the rate and concurrent searches of the search profile apply per key (429 ERR_RATE_LIMITED or ERR_SEARCH_BUSY, with Retry-After); a search rejected with 429 ERR_SEARCH_BUSY does not consume rate. If a search calculates for longer than 10 s, it ends with 503 ERR_SEARCH_TIMEOUT, never with a partial page.
  • Not provable: If the search cannot prove price, order or availability for a request (an error in the server, not a request error), it answers 500 ERR_INTERNAL instead of a possibly wrong page.

4a.5 Errors of the open search

The checks run in this order; the first finding is reported: key → permission → body and unknown fields → field format → limits of the API (1.3) → limits of the search profile → destinations, hotels, board, currency → cursor → rate and search slots → calculation. A rejected request occupies no search slot and does not count as a search. The codes are in the error catalogue (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)"}

The customer group's key may only search in one destination:

### 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)"}

A filter with a wrong value names the field:

### 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. Date matrix: POST /v1/search/open/dates (BA)

4b.1 What for

“Dates & prices” of a hotel: for one hotel and the same window as the open search, per date (arrival × length) the cheapest bookable offer or the reason why there is none. Permission and limits come from the same search profile as for the open search (4a.1); the profile's rate and concurrent searches apply to both endpoints together.

  • The price is exact: every cell with an offer is bit-identical to /v1/price with the same hotel, room, board, arrival and departure and the same occupancy.
  • Matches the open search: with the same filter and the same stand, best from 4a is the first cell with the smallest criterion (total price or price per night) – the same tie-break rule (earlier arrival, shorter length, room and board in contract order). With perBoard, best is the cell of its board at the first date with the smallest criterion.

4b.2 Request

Field
hotel
Required
yes
Meaning
Hotel code of the key (e.g. results[].hotel from 4a)
Field
arrivalFrom, arrivalTo
Required
yes
Meaning
Arrival window as in 4a.2; at most matrixMaxWindowDays days (search profile)
Field
nightsMin, nightsMax
Required
yes
Meaning
Length from–to as in 4a.2 (allowed lengths of the profile)
Field
occupancy
Required
yes
Meaning
one room, as for /v1/price
Field
boards, boardTypes
Required
no
Meaning
as in 4a.2; every code in boards must be offered by the hotel (422 ERR_BOARD_NOT_OFFERED)
Field
rooms
Required
no
Meaning
Room codes of the hotel; without = all (unknown: 404 ERR_ROOM_NOT_FOUND)
Field
minTotalCents, maxTotalCents
Required
no
Meaning
Filter on the total price as in 4a.2
Field
currency
Required
no
Meaning
Contract currency of the hotel; if the hotel prices in another currency or without a valid one: 422 ERR_CURRENCY_NOT_AVAILABLE
Field
perBoard
Required
no
Meaning
true: per date one cell per board (default false: one cell per date)
Field
category, regions, geo
Required
no
Meaning
filters on hotel master data as in 4a.2, same effect: if the hotel lies outside, it is not part of the search space and the matrix has no cell (cells empty); if it lacks the value, every cell is none with ERR_NO_CATEGORY, ERR_NO_REGION or ERR_NO_GEO

Cells = arrival days × lengths (× boards with perBoard), at most matrixMaxCells (search profile, otherwise 422 ERR_SEARCH_TOO_BROAD). There is no cursor. Every unknown field: 422 ERR_UNKNOWN_FIELD. The order of checks is that of the open search (4a.5), without cursor.

4b.3 Response

Field
hotel, currency
Meaning
Hotel and contract currency
Field
stand
Meaning
Data state as in 4a (same stand = same data)
Field
boards
Meaning
only with perBoard: the boards per date in contract order (first occurrence across the rooms, RO first, as in /v1/prices), in the order of the cells
Field
cells[]
Meaning
dense in the order checkIn, nights (with perBoard then board); every cell with checkIn, checkOut, nights, status. Empty only if the hotel lies outside the filters on hotel master data
Field
cells[].status = "offer"
Meaning
Offer: room, board, boardType, totalCents, perTravellerCents, availability (as for /v1/price, always available: true) – the cheapest room/board of the date, on a tie in contract order
Field
cells[].status = "none"
Meaning
no offer, reason in reason: the codes of /v1/price, the order of the open search (4a) – availability first (ERR_NO_INVENTORY, ERR_NOT_AVAILABLE), then sales rules, occupancy, season, price filter (ERR_OUTSIDE_PRICE_FILTER) or a contract error; with filters on hotel master data ERR_NO_CATEGORY, ERR_NO_REGION, ERR_NO_GEO (the hotel lacks the value, every cell)
Field
cells[].status = "unchecked"
Meaning
not checked because the time budget ran out – never read as “no offer”
Field
coverage
Meaning
complete (no cell unchecked), timeBudgetExhausted, cells = offers + none + unchecked, roomErrors[] (rooms with a contract error per code, 7.5)
Field
warnings[]
Meaning
Notes, e.g. on unchecked cells

The cells run in blocks in the order of the matrix; once the profile's time budget is used up, the remaining cells stay unchecked (the first block is always checked). Then narrow the window or the lengths and ask again. If the matrix computes longer than 10 s: 503 ERR_SEARCH_TIMEOUT; not provable: 500 ERR_INTERNAL (as in 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": []}
}

Per board, breakfast and half board only:

### 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 arrival days × 14 lengths × 3 boards are more cells than the profile allows:

### 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. Limits of the key: GET /v1/limits

The effective limits of your own key, so that a website sets up date picker, length selection and destinations instead of probing limits via 422. No parameters (any: 422 ERR_UNKNOWN_FIELD), does not count as a search.

Field
rate
Meaning
Rate of all requests of the key (4.4): perSecond, burst, scope (key = per key and node; testCircle = shared pot of the test keys, 11)
Field
export.allowed
Meaning
Permission for the EDF delivery (10)
Field
content.allowed
Meaning
Access to the content API (/v1/content/*): the operator has enabled content for the tour operator and the key carries the content right – the same rule as there; without access 403 ERR_CONTENT_NOT_ALLOWED
Field
openSearch.allowed
Meaning
Permission for the open search and the date matrix; without permission only this field is present
Field
openSearch.maxWindowDays, nightsMin, nightsMax, maxNightsSpan
Meaning
Arrival window, allowed lengths and length range per request (4a)
Field
openSearch.maxDestinations, maxHotels, maxCandidates, maxPageSize
Meaning
Destinations and hotels per request, hotels in the search scope, results per page (4a)
Field
openSearch.timeBudgetMs, rate, burst, concurrency
Meaning
Time budget per page or matrix, rate and concurrent searches (both endpoints together)
Field
openSearch.matrixMaxWindowDays, matrixMaxCells
Meaning
Window and cells of the date matrix (4b)
Field
openSearch.allDestinations, destinations
Meaning
all destinations allowed, otherwise the allowed destination codes – only those with hotels in the key's inventory (as GET /v1/destinations)

The values are exactly those that /v1/search/open and /v1/search/open/dates apply: the smallest of what the operator grants the tour operator and the key's profile. Which level sets a limit is not part of the response. Changes to the profile take effect after a few seconds.

### 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}
}

A key without permission for the open search:

### 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. Booking (B) and booking info

5.1 POST /v1/book

Field
hotel, room
Required
yes
Meaning
Hotel and room code (booking is board-neutral; the board is in priceCheck). If room is missing: 400 ERR_INVALID_BUCKET, with and without priceCheck
Field
checkIn, checkOut
Required
yes
Meaning
Stay (section 1.3)
Field
quantity
Required
yes
Meaning
Number of rooms, 1–1,000,000 (400 ERR_QUANTITY_INVALID)
Field
idemKey
Required
yes
Meaning
your own unique key for the transaction (section 9), at most 128 characters; if missing: 400 ERR_INVALID_IDEM_KEY
Field
reference
Required
no
Meaning
your own booking reference, at most 128 characters; lets you read the booking later
Field
leadPaxName
Required
no
Meaning
Name of the lead traveller, at most 255 characters
Field
metadata
Required
no
Meaning
free-form JSON object, stored and returned by /v1/booking, not evaluated. metadata.correlationId (up to 64 characters) is adopted as the correlation ID.
Field
priceCheck
Required
no, recommended
Meaning
Price check before the sale, see below

Texts that are too long (idemKey, reference, leadPaxName, metadata.correlationId) are rejected by the API before any sale with 422 ERR_VALIDATION; message names field, length and limit. Nothing is ever truncated.

priceCheck: board, occupancy.travellers[], expectedCents (the price the customer saw), tolerancePercent (allowed deviation in %, ≥ 0), optionally currency and now. TourAPI recalculates the price like /v1/price; if it deviates by more than the tolerance: 409 ERR_PRICE_DRIFT with the current price in message, nothing booked. With priceCheck, TourAPI stores the checked price on the booking (totalCents, currency, board in /v1/booking). Without priceCheck the booking is a pure allotment sale without a price (totalCents: null). Even then /v1/book only sells hotels that /v1/price knows for the key: without a valid contract there is no hotel (404 ERR_HOTEL_NOT_FOUND), even if an allotment is set up.

Response: booked, alreadyBooked (true = retry, nothing newly sold), reference (our booking reference TA-…), correlationId. correlationId is the metadata.correlationId sent along; if absent, TourAPI assigns a UUID and stores it on the record. A retry (alreadyBooked=true) always returns the stored correlationId of the first call – even if it sends none or a different one.

### 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 Retrying is safe

The same request with the same idemKey again – e.g. after a network error – does not sell again but confirms the existing booking (alreadyBooked: true, same reference).

### 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"}

The same idemKey with different booking data (hotel, room, stay, quantity) is an error in the caller:

### 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 Why a booking fails

If the sale fails on a night, message names the first affected night. Nothing is booked (all nights or none).

Code
ERR_SOLD_OUT
Status
422
Meaning
Night sold out
Code
ERR_STOP_SALE
Status
422
Meaning
Tour operator has stopped sales
Code
ERR_INVENTORY_CLOSED
Status
422
Meaning
Night closed or on request only
Code
ERR_NO_INVENTORY
Status
422
Meaning
no capacity set up for a night
Code
ERR_GROUP_LIMIT
Status
422
Meaning
the customer group's allocation is exhausted (or not present for room and night)
Code
ERR_HOTEL_NOT_FOUND
Status
404
Meaning
Hotel does not exist for this key (also: no valid contract, with and without priceCheck; customer group without an offer, 1.1)
Code
ERR_PRICE_DRIFT
Status
409
Meaning
Price deviates from the priceCheck
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
Meaning
a sales rule of the room excludes the stay (3.4); nothing is booked
Code
ERR_BOARD_NOT_AVAILABLE
Status
422
Meaning
the board of the priceCheck is not sold for this travel party (3.5); nothing is booked
### 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"}

A key with a customer group with an allocation books against its group's allocation, even in nights on free sale. Here TEST-PARTNER-BASE has 5 allocated for the night:

### 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"}

A price group books from the general inventory like a key without a group:

### 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=… – booking info

ref is our reference (TA-…) or your own reference from the booking; our reference wins. If your own reference matches several visible bookings (including cancelled ones): 409 ERR_REFERENCE_AMBIGUOUS, message names the matching TA-… references (newest first, at most 10) – then read with our reference from the booking response. Response: reference, customerReference, hotel, room, group (only for customer group bookings; group code from A-Z a-z 0-9 . _ -, 1 to 64 characters), checkIn, checkOut, quantity, status (confirmed | released), bookedAt, updatedAt (last change, for a cancellation the cancellation time; both RFC 3339 in UTC, e.g. 2026-09-26T08:15:03Z), metadata, totalCents, currency, board. The API does not return travellers' personal data.

Booked …then in the booking info
with priceChecktotalCents = checked price, currency = contract currency, board = board from the priceCheck
without priceChecktotalCents: null, currency: "", board: "" (empty strings, no price recorded)
without referencecustomerReference absent
without metadatametadata absent
with a key without a customer groupgroup absent

Visibility: a key with a customer group sees only its group's bookings. A key on the base contract sees all bookings of the tour operator, including those of the customer groups – but it can only cancel its own (section 6). Unknown or not visible: 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"
}

Via your own reference:

### 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"
}

A booking without priceCheck, without reference and without metadata – the response carries a server-assigned correlationId, the booking info no price:

### 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": ""
}

The same own reference on a second booking makes it ambiguous:

### 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. Cancellation (S): POST /v1/cancel

Only field: the booking's idemKey (if missing: 400 ERR_INVALID_IDEM_KEY; any other field: 422 ERR_UNKNOWN_FIELD). The allotment of all nights is returned, the status becomes released. Only what was booked with the same key scope can be cancelled: a key with a customer group cancels its group's bookings, a key on the base contract the base contract's bookings. Anything else is 404 ERR_BOOKING_NOT_FOUND. The API does not calculate cancellation fees. Every cancellation is logged at the tour operator with the time and the identifier (key_id) of the cancelling key and shown in its booking view; a retry (alreadyReleased) does not create a second entry.

Response: released (true), alreadyReleased (true = was already cancelled).

### stornieren
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{apiKey}}

{"idemKey": "BEISPIEL-0001"}
{"released": true, "alreadyReleased": false}

Retrying is safe:

### stornieren-wiederholt
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{apiKey}}

{"idemKey": "BEISPIEL-0001"}
{"released": true, "alreadyReleased": true}

The booking stays readable, with 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"
}

A cancelled idemKey is used up. For a new booking use a new key:

### 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. Error catalogue

Column "Caller": Fix the request = do not retry, the error is in the request. Retry = send the same request again later (with Retry-After no earlier than after that many seconds). Report = report to the tour operator/operator; retrying does not help. A code can occur at several endpoints; status and meaning stay the same.

7.1 Access and transport

Code
ERR_UNAUTHORIZED
Status
401
Meaning
Key missing, unknown or revoked
Caller
Check the key, do not retry (retries are delayed, section 8)
Code
ERR_TENANT_SUSPENDED
Status
403
Meaning
Key valid, tour operator suspended
Caller
Ask the tour operator, do not retry
Code
ERR_KEY_GROUP_INACTIVE
Status
403
Meaning
Key's customer group deactivated
Caller
Ask the tour operator
Code
ERR_MODE_MISMATCH
Status
403
Meaning
X-TourAPI-Require-Mode requires a different mode than the key's (e.g. live key in a test environment); nothing executed (section 11.1)
Caller
Swap the key, do not retry
Code
ERR_SCENARIO_NOT_ALLOWED
Status
422
Meaning
X-TourAPI-Sandbox-Scenario with a live key, unknown scenario or at an endpoint where it has no effect (section 11.3)
Caller
Fix the request
Code
ERR_METHOD_NOT_ALLOWED
Status
405
Meaning
wrong HTTP method
Caller
Fix the request
Code
ERR_BAD_REQUEST
Status
400
Meaning
Body not JSON, too large (> 1 MiB), ref missing, priceCheck.tolerancePercent negative, priceCheck.currency longer than 3 characters (shorter or unknown: 422 ERR_CURRENCY_NOT_AVAILABLE); Content API: since missing, duplicate parameter, lang with more than 5 or duplicate languages
Caller
Fix the request
Code
ERR_UNKNOWN_FIELD
Status
422
Meaning
unknown field in occupancy (queries) or anywhere (/v1/search/open, /v1/search/open/dates, /v1/book, /v1/cancel, body of /v1/export/edf/ack); unknown query parameter on /v1/export/edf/* and /v1/limits
Caller
Fix the request
Code
ERR_OPEN_SEARCH_NOT_ALLOWED
Status
403
Meaning
the key has no permission for the open search and the date matrix (4a.1; off for new keys; GET /v1/limits shows it)
Caller
Ask the tour operator
Code
ERR_DESTINATION_NOT_ALLOWED
Status
403
Meaning
open search: the destination (destinations[i]) or the hotel (hotels[i], date matrix: hotel) lies outside the allowed destinations of the search profile
Caller
Take a destination from the search profile (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 Request (fields and limits)

Code
ERR_BAD_DATE
Status
422
Meaning
Date missing or not JJJJ-MM-TT (message names the field)
Caller
Fix the request
Code
ERR_EMPTY_STAY
Status
422
Meaning
checkOut ≤ checkIn
Caller
Fix the request
Code
ERR_STAY_TOO_LONG
Status
422
Meaning
more than 30 nights
Caller
Fix the request
Code
ERR_STAY_IN_PAST
Status
422
Meaning
Arrival before the reference date
Caller
Fix the request
Code
ERR_STAY_TOO_FAR
Status
422
Meaning
Arrival more than 732 days after the reference date
Caller
Fix the request
Code
ERR_NO_TRAVELLERS
Status
422
Meaning
no travellers
Caller
Fix the request
Code
ERR_TOO_MANY_TRAVELLERS
Status
422
Meaning
more than 20 travellers
Caller
Fix the request
Code
ERR_INVALID_AGE
Status
422
Meaning
Age negative or above 120
Caller
Fix the request
Code
ERR_BOARD_MISSING
Status
422
Meaning
board missing (/v1/price, /v1/search, priceCheck)
Caller
Fix the request
Code
ERR_NOW_MISMATCH
Status
422
Meaning
priceCheck.now is not the reference date
Caller
Omit the field
Code
ERR_VALIDATION
Status
422
Meaning
Text too long: idemKey, reference (128), leadPaxName (255), metadata.correlationId (64); message names field and limit
Caller
Fix the request
Code
ERR_QUANTITY_INVALID
Status
400
Meaning
quantity outside 1–1,000,000
Caller
Fix the request
Code
ERR_INVALID_IDEM_KEY
Status
400
Meaning
idemKey missing (/v1/book, /v1/cancel)
Caller
Fix the request
Code
ERR_INVALID_BUCKET
Status
400
Meaning
room missing (/v1/book, with and without priceCheck)
Caller
Fix the request
Code
ERR_INVALID_STAY
Status
400
Meaning
Stay invalid (safeguard in the sale; the API checks beforehand with ERR_BAD_DATE/ERR_EMPTY_STAY)
Caller
Fix the request
Code
ERR_BAD_PAGE_SIZE
Status
422
Meaning
pageSize outside 1–100 (/v1/search; open search: 1 up to the search profile) or 1–1000 (/v1/content/*)
Caller
Fix the request
Code
ERR_BAD_CURSOR
Status
422
Meaning
Cursor unreadable, modified or expired (e.g. after a server restart; open search: 15 minutes after issue); Content API: cursor/since unreadable, modified or from another key
Caller
start over without cursor or fetch the directory again
Code
ERR_CURSOR_MISMATCH
Status
422
Meaning
Cursor belongs to a different request or a different key (/v1/search, /v1/search/open)
Caller
Fix the request
Code
ERR_UNKNOWN_DESTINATION
Status
422
Meaning
/v1/search or /v1/search/open without cursor: there is no hotel for destination or destinations[i] for this key (case-sensitive)
Caller
Take a code from GET /v1/destinations (4.5)
Code
ERR_BAD_WINDOW
Status
422
Meaning
open search and date matrix: arrivalFrom/arrivalTo missing or arrivalTo before arrivalFrom
Caller
Fix the request
Code
ERR_BAD_NIGHTS
Status
422
Meaning
open search and date matrix: nightsMin/nightsMax missing, < 1 or nightsMin > nightsMax
Caller
Fix the request
Code
ERR_BAD_TARGET
Status
422
Meaning
open search: destinations and hotels together, empty list, empty code, or neither although the search profile only allows individual destinations; date matrix: hotel missing
Caller
Fix the request
Code
ERR_BAD_SORT
Status
422
Meaning
open search: sort unknown (price, pricePerNight, hotel)
Caller
Fix the request
Code
ERR_BAD_FILTER
Status
422
Meaning
open search and date matrix: empty boards/boardTypes/rooms list or empty entry, unknown board type, price filter negative or minTotalCents > maxTotalCents, currency not a code of three capital letters; filter on hotel master data outside its format (category, regions, geo, 4a.2) – the message names the field
Caller
Fix the request
Code
ERR_WINDOW_TOO_WIDE
Status
422
Meaning
open search or date matrix: arrival window wider than the search profile allows (maxWindowDays or matrixMaxWindowDays, message names the limit)
Caller
split or narrow the window
Code
ERR_NIGHTS_NOT_ALLOWED
Status
422
Meaning
open search: length outside the allowed lengths or range nightsMax - nightsMin + 1 too wide; date matrix: length outside the allowed lengths (message names the limit)
Caller
adjust the length
Code
ERR_SEARCH_TOO_BROAD
Status
422
Meaning
open search: too many destinations or hotels in the request or too many hotels in the search scope; date matrix: more cells than matrixMaxCells (message names the limit)
Caller
narrow down
Code
ERR_CURRENCY_REQUIRED
Status
422
Meaning
open search: the hotels of the search scope price in several contract currencies, currency is missing (message names them)
Caller
set currency
### 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)"}

Texts that are too long are rejected before the sale, never truncated:

### 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)"}

A misspelled field outside the occupancy is not rejected but named – here the required field is missing as a result, and the response says both:

### 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?)"]}

Warnings in a successful response:

### 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 Inventory, price, sale

Code
ERR_HOTEL_NOT_FOUND
Status
404
Meaning
Hotel does not exist for this key (also: another tour operator, not published, customer group special price cannot be calculated – see 1.1)
Caller
Fix the request; for an otherwise known hotel inform the tour operator
Code
ERR_ROOM_NOT_FOUND
Status
404
Meaning
Room does not exist in this hotel (date matrix: rooms[i])
Caller
Fix the request
Code
ERR_BOOKING_NOT_FOUND
Status
404
Meaning
Booking unknown or not visible for this key
Caller
Check reference/key
Code
ERR_REFERENCE_AMBIGUOUS
Status
409
Meaning
your own reference matches several bookings (/v1/booking); message names the TA-… references (test key: SB-…)
Caller
read with the TourAPI reference; keep your own references unique
Code
ERR_TENANT_NOT_FOUND
Status
404
Meaning
Tour operator not (or no longer) active, sale/cancellation only
Caller
Report
Code
ERR_BOARD_NOT_OFFERED
Status
422
Meaning
Board is not offered (code exactly as in the contract; /v1/price, /v1/prices, priceCheck; open search: boards[i] offered by no hotel of the search scope; date matrix: boards[i] not in the hotel or no board matches boards/boardTypes); in search a reason in diagnostics.reasons or coverage.reasons
Caller
Fix the request
Code
ERR_OCCUPANCY_NOT_ALLOWED
Status
422
Meaning
Occupancy fits no room (or not the requested one)
Caller
different occupancy/different room
Code
ERR_STAY_LENGTH_NOT_ALLOWED
Status
422
Meaning
a sales rule of the room requires a different length of stay (3.4); in search a reason in diagnostics.reasons
Caller
change the length
Code
ERR_ARRIVAL_DAY_NOT_ALLOWED
Status
422
Meaning
a sales rule requires a different arrival/departure weekday (3.4)
Caller
shift the travel dates
Code
ERR_TRAVEL_DATES_NOT_ALLOWED
Status
422
Meaning
the stay lies outside a rule's sales window (3.4)
Caller
different period
Code
ERR_BOARD_NOT_ALLOWED
Status
422
Meaning
the board is not sold for this stay (3.4)
Caller
different board
Code
ERR_LEAD_TIME_NOT_ALLOWED
Status
422
Meaning
the arrival lies within the release period of a sales rule, counted from the reference date (3.4)
Caller
later arrival
Code
ERR_BOARD_NOT_AVAILABLE
Status
422
Meaning
the board is not sold for this travel party, its surcharge requires a different composition (3.5); in /v1/prices an entry in rooms[].errors[], in search a reason in diagnostics.reasons
Caller
different board/occupancy
Code
ERR_ROOM_RESTRICTION_INVALID
Status
422
Meaning
a sales rule in the contract cannot be evaluated
Caller
Report
Code
ERR_NO_SECTION
Status
422
Meaning
there is no price for a night (season not in the contract); if another room calculates, in /v1/prices an entry in rooms[].errors[], with /v1/price without room in warnings, in search in diagnostics.roomErrors
Caller
different period
Code
ERR_NO_PRICE
Status
422
Meaning
no bookable room for the request, without a more specific reason
Caller
different period/different occupancy
Code
ERR_OCCUPANCY_NIGHT_UNCOVERED
Status
422
Meaning
The contract's occupancy rules do not cover a night
Caller
Report
Code
ERR_OCCUPANCY_INCONSISTENT_MCA
Status
422
Meaning
Minimum occupancy changes within the stay (not supported)
Caller
shorter period or Report
Code
ERR_OCCUPANCY_INCONSISTENT_CHILDREN
Status
422
Meaning
a traveller is a child at one point and an adult at another within the stay (the room's child age band changes with the season; not supported)
Caller
shorter period or Report
Code
ERR_OCCUPANCY_INCONSISTENT_INFANTS
Status
422
Meaning
an infant counts towards occupancy at one point and not at another within the stay (the room's occupancy rule changes with the season), and a surcharge or discount depends on the number of persons; ambiguous
Caller
shorter period or Report
Code
ERR_CHILDREN_ORDER_MISSING
Status
422
Meaning
the contract does not specify whether the oldest or the youngest child counts first, and for these children that makes a price difference (contract error)
Caller
Report
Code
ERR_INVALID_AMOUNT
Status
422
Meaning
an amount or percentage in the contract is not readable
Caller
Report
Code
ERR_AMOUNT_OVERFLOW
Status
422
Meaning
the price exceeds the representable cent range (contract error)
Caller
Report
Code
ERR_CURRENCY_NOT_AVAILABLE
Status
422
Meaning
priceCheck.currency does not match the contract currency, or the latter is unknown; in the open search a reason in coverage.reasons (hotel prices in a currency other than the requested one or in an unknown one); date matrix: the same as 422
Caller
Fix the request or Report
Code
ERR_PRICE_DRIFT
Status
409
Meaning
current price deviates from the priceCheck
Caller
show the new price, book with a new expectedCents
Code
ERR_SOLD_OUT
Status
422
Meaning
Night sold out
Caller
do not retry
Code
ERR_STOP_SALE
Status
422
Meaning
Stop sale
Caller
do not retry
Code
ERR_INVENTORY_CLOSED
Status
422
Meaning
Night closed or on request only
Caller
do not retry
Code
ERR_NO_INVENTORY
Status
422
Meaning
no capacity set up for at least one night (same for booking and search reason)
Caller
do not retry
Code
ERR_NOT_AVAILABLE
Status
–
Meaning
only as a reason in search: every night has capacity, but not every night is open (booking: ERR_SOLD_OUT, ERR_STOP_SALE, ERR_INVENTORY_CLOSED)
Caller
–
Code
ERR_OUTSIDE_PRICE_FILTER
Status
–
Meaning
only as a reason in the open search (every offer of the hotel) or the date matrix (every offer of the cell) lies outside minTotalCents/maxTotalCents
Caller
–
Code
ERR_NO_CATEGORY
Status
–
Meaning
only as a reason (open search; date matrix: every cell of the hotel): filter category, the hotel has no official category in the hotel master data
Caller
Tour operator: maintain the category
Code
ERR_NO_REGION
Status
–
Meaning
only as a reason (open search; date matrix: every cell of the hotel): filter regions, the hotel has no region in the hotel master data
Caller
Tour operator: maintain the region
Code
ERR_NO_GEO
Status
–
Meaning
only as a reason (open search; date matrix: every cell of the hotel): filter geo, the hotel has no coordinates in the hotel master data
Caller
Tour operator: maintain the coordinates
Code
ERR_GROUP_LIMIT
Status
422
Meaning
Customer group's allocation exhausted or not present for room and night
Caller
do not retry
Code
ERR_IDEMPOTENCY_MISMATCH
Status
409
Meaning
idemKey already used with different booking data
Caller
Error in the caller: assign unique keys
Code
ERR_IDEM_KEY_RELEASED
Status
409
Meaning
idemKey belongs to a cancelled booking
Caller
use a new idemKey
Code
ERR_SANDBOX_LIMIT
Status
422
Meaning
Test key: more than 5,000 open test bookings for this access (section 11.2)
Caller
Cancel test bookings
### 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"}

The same applies to /v1/price – a board the room does not offer is never calculated as the price without board:

### 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 Load and operations

Code
ERR_RATE_LIMITED
Status
429
Meaning
too many requests from this key (section 8); open search and date matrix: rate of the search profile exceeded (both together)
Caller
Retry after Retry-After
Code
ERR_SEARCH_BUSY
Status
429
Meaning
too many concurrent searches by the tour operator (open search: also by the key according to the search profile)
Caller
Retry after Retry-After (1 s)
Code
ERR_SEARCH_TIMEOUT
Status
503
Meaning
Search exceeded the time limit (10 s)
Caller
narrow down (destination/destinations, smaller window), then retry
Code
ERR_PRICE_TIMEOUT
Status
503
Meaning
/v1/price without room or /v1/prices exceeded the time limit (2 s)
Caller
switch to /v1/price with room (the limit always applies to /v1/prices, even with room and boards), then retry
Code
ERR_BOOKING_DISABLED
Status
503
Meaning
Sale/cancellation not enabled on this node (test key: sandbox not enabled – a test key never books live)
Caller
Retry; if persistent: Report
Code
ERR_BOOKING_BUSY
Status
503
Meaning
Booking/cancellation did not go through due to concurrent operations on the same hotel; nothing booked or cancelled
Caller
Retry after Retry-After (1 s) with the same idemKey
Code
ERR_INTERNAL
Status
500
Meaning
internal error, e.g. database unreachable (a violated invariant in the calculation core comes as 422 at the query endpoints); open search: result not provable (4a.4)
Caller
Retry with a pause; for /v1/book with the same idemKey
Code
ERR_INVENTORY_DRIFT
Status
500
Meaning
Allotment invariant violated, nothing sold
Caller
Report
Code
ERR_INVENTORY_STATUS_UNKNOWN
Status
500
Meaning
unknown daily status in the allotment, nothing sold
Caller
Report
Code
ERR_RELEASE_DRIFT
Status
500
Meaning
Cancellation blocked due to an allotment invariant, nothing cancelled
Caller
Report

7.5 Contract data (errors at the tour operator)

These codes come from the calculation core when the hotel's contract contains a rule that TourAPI does not calculate (or not in that form). They should not occur after publication, because the contract is checked beforehand. Status always 422. Caller: Report (with hotel, period and message); in search they appear as a reason in diagnostics.reasons (hotel dropped) or in diagnostics.roomErrors (single room skipped). With /v1/price without room, an affected room is skipped and named in warnings as long as another room calculates; /v1/prices names the affected boards in rooms[].errors[] and prices the rest (section 3.2). ERR_NEGATIVE_TRAVELLER_PRICE and ERR_NEGATIVE_PERCENT_BASE depend on occupancy and board: the write gate warns about them but accepts the contract; they can therefore also occur after publication. /v1/prices then skips only the affected board (rooms[].errors[], section 3.2).

Code
ERR_NO_BASECHARGE
Meaning
Base price missing
Code
ERR_SECTION_BAD_DATE, ERR_BOARD_BAD_DATE, ERR_OCCUPANCY_BAD_DATE
Meaning
invalid date in the contract
Code
ERR_OCCUPANCY_INCOMPLETE
Meaning
Occupancy rule incomplete
Code
ERR_AMBIGUOUS_BOARDCHARGE
Meaning
two board surcharges can hit the same person in the same night; which one applies is ambiguous (the write gate does not allow this, legacy data only)
Code
ERR_AMBIGUOUS_BASECHARGE, ERR_AMBIGUOUS_SECTION
Meaning
two base prices of the same type in one season, or two seasons for the same day; which price applies is ambiguous (the write gate does not allow this, legacy data only)
Code
ERR_AMBIGUOUS_FREENIGHT
Meaning
two free night offers can apply to the same stay; which nights the second one waives is undetermined (the write gate does not allow this, legacy data only)
Code
ERR_UNSUPPORTED_FREENIGHT, ERR_UNSUPPORTED_REDUCTION_MODE
Meaning
Free night offer in a form TourAPI does not calculate (e.g. fixed amount instead of percentage, person restriction, night selection "greater/less than")
Code
ERR_NEGATIVE_TRAVELLER_PRICE
Meaning
after all surcharges and discounts a traveller would pay less than 0 (e.g. a fixed discount per person on a child that costs nothing, or stacked discounts above 100 %). A traveller never pays less than 0; message names the traveller and the surcharge/discount. Percentage discounts apply to the amount the traveller owes after child/person reduction and do not trigger the error on their own
Code
ERR_NEGATIVE_PERCENT_BASE
Meaning
a child/person discount (fixed amount) is greater than the daily price or the board it applies to; a percentage discount on it would become a surcharge, a free night (e.g. "7=6") would make the stay more expensive. message names the surcharge/discount or free night, traveller and night
Code
ERR_UNSUPPORTED_OCCUPANCY_PRICEBLOCK
Meaning
Base price per occupancy (from a third-party delivery, e.g. “exactly 1 person” / “2 persons or more”) in a form TourAPI does not calculate — or an infant travels in such a room (the supplier does not sell it with an infant)
Code
ERR_UNSUPPORTED_GUESTCHARGE_OBJECT
Meaning
Person reduction (e.g. child discount) on a room price (per-unit price): there is no per-person price it could apply to (the write gate does not allow this, legacy data only)
Code
ERR_UNSUPPORTED_COMBIGROUP
Meaning
an offer is exclusive in combination group 0; in EDF, group 0 also stands for all offers without a group, so an EDF recipient would calculate differently (the write gate does not allow this, legacy data only)
Code
ERR_COMPATIBLE_WITH_INVALID
Meaning
an offer's “combinable only with groups” list is ambiguous: a group listed twice, outside 0..2147483647 or combined with “only one per group” (the write gate does not allow this, legacy data only)
Code
ERR_CALCMODE_MISSING, ERR_CALCMODE_UNSUPPORTED
Meaning
Calculation mode of the room missing or not supported
Code
ERR_BASE_BOARD_INVALID, ERR_BASE_BOARD_CHARGED
Meaning
Base board of the room (board included in the base price) invalid or carrying its own surcharge (the write gate does not allow this, legacy data only)
Code
ERR_MINCHARGEDPERSONS_MISSING, ERR_INVALID_MIN_CHARGED_PERSONS
Meaning
Minimum number of paying persons missing or invalid
Code
ERR_INVALID_ENUM, ERR_INVALID_WEEKDAY_MASK
Meaning
invalid enumeration value or weekday mask
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
Meaning
Surcharge or discount incomplete or contradictory
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
Meaning
Contract rule TourAPI does not calculate

7.6 EDF delivery (/v1/export/edf/*, section 10)

Code
ERR_EXPORT_BAD_CURSOR
Status
400
Meaning
epoch/since missing or unreadable, until not a valid chain key, max_bytes not a number ≥ 65536, duplicate parameter, page in the middle of a state without until, follow-up page of the full without epoch/since/until
Caller
Fix the request or restart the chain
Code
ERR_EXPORT_NOT_ALLOWED
Status
403
Meaning
Key without export permission
Caller
Ask the tour operator, do not retry
Code
ERR_EXPORT_EPOCH
Status
409
Meaning
epoch does not match (feed rebuilt) or the state (since, until, seq of the acknowledgement) is above the current delivery state (greater than the last delivered state)
Caller
fetch full
Code
ERR_EXPORT_CURSOR_EXPIRED
Status
410
Meaning
State older than the retention period
Caller
fetch full
Code
ERR_EXPORT_NOT_READY
Status
503
Meaning
Delivery for this key not built yet, temporarily not current (delivery lagging more than 2 minutes behind) or not set up on the node
Caller
Retry after Retry-After; if persistent: Report

Also on /v1/export/edf/*: 401/403 from 7.1 (ERR_TENANT_SUSPENDED, ERR_KEY_GROUP_INACTIVE), 422 ERR_UNKNOWN_FIELD (unknown parameter or acknowledgement field) and 429 ERR_RATE_LIMITED for the cadence (10.3; Retry-After up to 3600 s for full – do not wait blindly, plan the next cycle instead).

7.7 Hotel content (/v1/content/*, section 12)

Code
ERR_CONTENT_NOT_ALLOWED
Status
403
Meaning
Content not enabled for the tour operator (also: test environment not served on this installation) or key without content right
Caller
Ask the tour operator, do not retry
Code
ERR_LANGUAGE_NOT_OFFERED
Status
422
Meaning
lang names a language that is not one of the tour operator's content languages (for the catalogue: not a label language); the offered ones are listed in warnings
Caller
Fix the request
Code
ERR_CONTENT_CURSOR_EXPIRED
Status
410
Meaning
since lies before the feed's retention horizon (30 days)
Caller
Fetch the directory again, continue with its feedToken
Code
ERR_CONTENT_NOT_READY
Status
503
Meaning
Content or image addresses not set up on this node
Caller
Retry after Retry-After; if persistent: Report

Also on /v1/content/*: 401/403 from 7.1, 404 ERR_HOTEL_NOT_FOUND (hotel not in the key's directory), 400 ERR_BAD_REQUEST, 422 ERR_BAD_PAGE_SIZE, ERR_BAD_CURSOR and 429 ERR_RATE_LIMITED (rate per key, section 8). Unknown query parameters are ignored and named in warnings.

8. Fairness: rate limit and search gate

Guard
Requests per API key
Limit (default)
200 per second, briefly up to 400 (token bucket)
Response
429 ERR_RATE_LIMITED + Retry-After
Guard
Concurrent searches per tour operator
Limit (default)
8 (computing time: one quarter of the cores, at least 1)
Response
429 ERR_SEARCH_BUSY + Retry-After: 1
Guard
Computing time of one search
Limit (default)
10 s
Response
503 ERR_SEARCH_TIMEOUT
Guard
Computing time of /v1/price without room and /v1/prices
Limit (default)
2 s
Response
503 ERR_PRICE_TIMEOUT
  • The limits apply per server node. They are a protection against loops and load spikes, not a billable quota. The operator can change them.
  • Retry-After is in whole seconds (at least 1). Do not retry before; afterwards retry with the same request (for /v1/book with the same idemKey).
  • If you send again before Retry-After has elapsed and are rejected again, you receive the 429 only after a delay (until the end of the announced wait time, at most 1 s). The wait time applies per API key: if several processes work with the same key in parallel, the delay hits every one that keeps sending after a 429, regardless of how many connections. The connection stays open.
  • Rejected keys (401, 403 ERR_TENANT_SUSPENDED, 403 ERR_KEY_GROUP_INACTIVE) are counted per sender IP: after 20 rejections in quick succession only 5 per second are answered immediately, further ones only with a delay (at most 1 s), regardless of how many connections. Status and response stay the same. Requests with a valid key are never affected.
  • Unknown paths (404) and wrong methods (405 ERR_METHOD_NOT_ALLOWED) count towards the same per-sender-IP limit as rejected keys.
  • /v1/health has its own limit per sender IP: 50 calls immediately, then 10 per second immediately, further ones only with a delay (at most 1 s). The response is always the current state, never a rejection.
  • Rejected requests do not count as usage.
  • Truncated search pages are not an error, see 4.3.

This is what the rate limiting looks like (the examples run against an instance with 1 request per 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"}

Accompanying header: Retry-After: 10.

9. Idempotency and concurrency

Booking:

  • idemKey is required and belongs to the caller. It applies per tour operator and key scope (customer group or base contract): two customer groups can use the same key without interfering with each other.
  • Same idemKey, same booking data (hotel, room, stay, quantity) = the same booking: 200, alreadyBooked: true, same reference. reference, metadata, leadPaxName and priceCheck of a retry are neither compared nor adopted – the first booking applies.
  • Same idemKey, different booking data: 409 ERR_IDEMPOTENCY_MISMATCH.
  • Cancelled idemKey: 409 ERR_IDEM_KEY_RELEASED.
  • The idempotency check comes before reference date, limits and priceCheck: if the booking exists, the retry confirms it even after midnight (arrival now in the past, priceCheck.now no longer the reference date) or after a price change. Only a new idemKey goes through these checks. Formal errors in the request (unknown fields, missing required fields, texts too long) are still rejected beforehand.
  • If the first booking of an idemKey is still running, a retry waits for its outcome and then confirms it (200, alreadyBooked: true); if the first one fails, the retry runs as a new booking. If waiting takes too long: 503 ERR_BOOKING_BUSY with Retry-After – retry with the same idemKey.
  • Two concurrent bookings of the same room can never overbook the allotment: each night is counted atomically, and a booking takes all nights or none.
  • If a booking or cancellation does not go through due to concurrent operations on the same hotel: 503 ERR_BOOKING_BUSY with Retry-After (section 7.4). Nothing is booked or cancelled; after the wait time retry with the same idemKey.
  • If a hotel is deleted while a booking for it is running, order decides: if the booking came first, it remains; otherwise 404 ERR_HOTEL_NOT_FOUND, nothing booked.
  • After a timeout or 5xx it is unclear whether a booking was made: retry with the same idemKey, never with a new one.

Cancellation:

  • Addressed via the booking's idemKey. A double cancellation is a success (alreadyReleased: true). Concurrent cancellations of the same booking return the allotment exactly once.

10. EDF delivery (cache export)

Purpose: a buyer keeps the prices and availabilities of all hotels of its key as EDF files in its own cache and queries live before booking (/v1/price, then /v1/book). /v1/book is always binding; the cache is an offer, not inventory. The complete delivery contract (canonical form of the manifest, limits) is available from the operator on request.

Method/pathResponse
GET /v1/export/edf/full[?max_bytes=N]200 zip with the full snapshot (for large inventories the first page)
GET /v1/export/edf/full?epoch=E&since=S&until=K[&max_bytes=N]200 zip with the next page of the full snapshot
GET /v1/export/edf/changes?epoch=E&since=S[&until=K][&max_bytes=N]200 zip with all changes after state S; 204 if nothing is new
POST /v1/export/edf/ack {"epoch": E, "seq": T}204; reports "processed" (only for monitoring at the tour operator)
  • Scope only via the key: the key determines what is delivered – the same hotels and prices as /v1/search and /v1/price for this key (base contract, customer group with its price, allotment group only hotels with an allocation). There is no parameter that selects tour operator or group; an unknown parameter is 422 ERR_UNKNOWN_FIELD.
  • Export permission: to be enabled per key (tour operator admin, in the console on the key row "Export erteilen"); without permission 403 ERR_EXPORT_NOT_ALLOWED.
  • Published data only: drafts never change the delivery. Changes (publication, stop sale, booking, promotion, allocation) appear in the feed after about one minute at the latest.

10.1 Package

A zip with manifest.json as the first entry, then per hotel one price file (hotels/hotelonly/EDF----<tenant>-<hotel>.xml, EDF 5.1.6) and one allotment file (hotels/hotelonly/allotment/EDF----<tenant>-<hotel>.xml, HotelAllotmentRoot 1.012). Identity comes from the manifest or BasicData, never from the file name (codes may contain -). Every file has its sha256 checksum and length in the manifest; a package is applied in full or not at all.

One entry in objects and one 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: withdrawn hotels are listed under removed with a reason (withdrawn:deleted, withdrawn:variant_error, withdrawn:not_exportable, withdrawn:no_currency, withdrawn:not_in_universe). The buyer deletes them from its cache. A full always has removed: [] – it replaces the cache entirely.
  • Pattern of the allotment file: two characters per night (PatternLength="2", like the MTS deliveries): 00–99 = free units for this key, ** = more than 99 free or free sale, SS = stop sale, RR = on request; a night without capacity is 00. If an allotment file is missing, the hotel counts as not available. Structure per room: <Allotments RoomCode="DZ"><Allotment Start="JJJJ-MM-TT" End="JJJJ-MM-TT" Pattern="…"/>; the first two characters of Pattern belong to the night Start, each further pair to the following night.
  • Pattern and minFree: SS, RR and 00 explicitly result in minFree 0 (not bookable); only ** does not lower minFree, and -1 means: every night **. The minimum across the nights of a stay is availability.minFree of /v1/price for the same key (table in section 3.3) – both come from the same per-night source.

10.2 Full snapshot, changes, acknowledgement

At startup and after 409/410 the buyer fetches the full snapshot. From the manifest it records epoch and to_seq. If the full snapshot does not fit in one package (more than 10,000 files, more than 1 GiB unpacked or more than max_bytes), it comes in pages, see "Pages" below:

### 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": []
}

Headers of every 200/204 response: X-Export-Epoch, X-Export-Seq (last complete state), X-Export-From (state from which delivery started), X-Export-More; for a changes package and for a full page with more: true additionally X-Export-Until (chain key, see below). If a page ends in the middle of a state (to_after in the manifest), X-Export-Seq names the state before it – so on the first pages of a full 0. Authoritative for since, applying and acknowledgement are to_seq/to_after from the manifest. A customer group's key receives its group's state (scope), with the group's prices:

### export-partner
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{rabattKey}}

After that it polls for changes on a cadence. since is the to_seq of the last applied package; if it is already at the current state, the response is 204 without content:

### export-nichts-neu
GET {{baseUrl}}/v1/export/edf/changes?epoch={{exportEpoch}}&since={{exportSeq}}
X-Api-Key: {{apiKey}}

Pages (max_bytes): full and changes are page chains. A page has at most 10,000 files and 1 GiB unpacked; with max_bytes=N the server additionally cuts pages of at most N bytes (at least one file per page); N is at least 65536 (64 KiB), smaller is 400 ERR_EXPORT_BAD_CURSOR. The first page (full without parameters or changes without until) fixes the target of the chain (the current state) and returns it as the chain key in X-Export-Until (for full only if more: true): an opaque, sealed string (u1.… for changes, f1.… for full), bound to key and epoch. The buyer requests follow-up pages with since=<to_seq> or since=<to_seq>:<to_after> (if the manifest carries to_after) and until=<X-Export-Until> (value of the previous page, URL-encoded, unchanged), for full additionally with epoch=<epoch> (value of the first page), until more is false. Every page returns the key afresh; it is valid for 15 minutes after the last page, for full only for exactly the state of the next page. A self-chosen until (number), a foreign or expired key is 400 ERR_EXPORT_BAD_CURSOR – then restart the chain. A full page never carries removed; the first starts at from_seq: 0, each further one at the to of the previous page. Only at the end of the chain is the state consistent: that is where it is applied (swapped), even if the tour operator keeps making changes in the meantime – the chain ends exactly at its target.

Abort and retry: if a package fails after 200 has already been sent (e.g. because a file at the tour operator is now missing), the server aborts the connection. The buyer then sees a transport error (unexpected EOF, connection reset), never a cleanly terminated, shortened package. Nevertheless it must verify every package against its manifest before applying it: every file from objects present, length (bytes) and sha256 match, no extra file. A streaming reader (e.g. Java's ZipInputStream) does not by itself recognise a zip that ends at an entry boundary as incomplete. A follow-up page that fails briefly (transport error, aborted or unreadable package, 5xx) is requested again with the same epoch/since/until (the key is valid for 15 minutes, 503 after Retry-After) instead of restarting the chain – a restart costs the cadence (full: one hour). Only after 400 does it restart the chain; after 409/410 it fetches full. If a chain aborts, the old state remains.

After applying, the buyer acknowledges the state. The acknowledgement is voluntary and does not change anything about the delivery; it lets the tour operator see which state the buyer has processed:

### export-quittung
POST {{baseUrl}}/v1/export/edf/ack
Content-Type: application/json
X-Api-Key: {{apiKey}}

{"epoch": "{{exportEpoch}}", "seq": {{exportSeq}}}

10.3 Errors and cadence

Status
400
Code
ERR_EXPORT_BAD_CURSOR
When
epoch/since missing or unreadable, until not a valid chain key (foreign, expired, number, for full for a different state), max_bytes not a number ≥ 65536, page in the middle of a state without until, state above the chain target, follow-up page of the full without epoch/since/until
Buyer does
Fix the request or restart the chain
Status
403
Code
ERR_EXPORT_NOT_ALLOWED
When
Key without export permission
Buyer does
Ask the tour operator
Status
403
Code
ERR_KEY_GROUP_INACTIVE
When
Key's customer group deactivated (never silently the base contract)
Buyer does
Ask the tour operator
Status
409
Code
ERR_EXPORT_EPOCH
When
epoch does not match (feed rebuilt) or since/until or the acknowledgement's seq is above the current delivery state (greater than the last delivered state; an older since is allowed and delivers everything after it)
Buyer does
full
Status
410
Code
ERR_EXPORT_CURSOR_EXPIRED
When
State older than the retention period (14 days)
Buyer does
full
Status
429
Code
ERR_RATE_LIMITED
When
Cadence exceeded, see below
Buyer does
after Retry-After
Status
503
Code
ERR_EXPORT_NOT_READY
When
Delivery for this key never built yet (new key, new group), or the delivery is temporarily not current (it lags more than 2 minutes behind the data)
Buyer does
after Retry-After, the old state remains valid
### 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"}

Cadence per key: a new full at most once per hour, a new changes chain at most once per minute. Follow-up pages of a chain (full or changes, with the chain key) and acknowledgements have their own generous cadence (5 per second, 50 at once). Which call starting a chain uses up the cadence:

Responseuses up the cadence
400 ERR_EXPORT_BAD_CURSOR, 422 ERR_UNKNOWN_FIELD (invalid request, checked before any work)no
503 ERR_EXPORT_NOT_READY (delivery never built or temporarily not current)no – if you follow Retry-After, you get no 429
200 (including an aborted download), 204 ("nothing new"), 409 ERR_EXPORT_EPOCHyes

The cadence check comes before the check of epoch and state. A buyer with an outdated epoch first sees 429 within the cadence and only after its Retry-After the 409. Example flow for changes (measured, cadence 1 min):

Time
0 s
Request
changes?epoch=E&since=S&foo=1 (typo)
Response
422 ERR_UNKNOWN_FIELD
Buyer does
fix it; cadence not used up
Time
0 s
Request
changes?epoch=E&since=S
Response
204
Buyer does
cadence used up; next chain in 60 s at the earliest
Time
0 s
Request
changes?epoch=E&since=S, the feed has been rebuilt in the meantime (E outdated)
Response
429 ERR_RATE_LIMITED, Retry-After: 60
Buyer does
wait for Retry-After
Time
60 s
Request
the same call
Response
409 ERR_EXPORT_EPOCH
Buyer does
fetch full (own cadence, 1 per hour)

In addition, the key's rate limit applies (section 8). A second full within the same hour:

### 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 Applying and calculating from the cache (reference receiver)

TourAPI has a reference receiver that implements exactly these rules and is checked on every build against the real API: the edf-empfaenger tool (available from the operator on request) (pull, apply, stand, rechne, reset; the key comes only from the environment variable EDF_EMPFAENGER_KEY). What it calculates from the delivery matches, for every request in the test set, /v1/price of the same key to the cent – total price, price per traveller, selected room, available and minFree, and on rejection the same code; this also holds for invalid and multiply invalid requests (which limit applies first). The reference receiver and make e2e-export live in the TourAPI source tree and are not part of the delivery: as a customer you receive a key and an access package and implement the rules of this section in your system – the reference receiver is the proof that they work out.

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 is a list; per request hotel, room (optional, missing = cheapest available room), board, checkIn, checkOut and the travellers' ages as ages (not occupancy.travellers as with /v1/price), e.g. [{"hotel": "TEST-HOTEL-BASE", "room": "DZ", "board": "RO", "checkIn": "2026-10-27", "checkOut": "2026-10-29", "ages": [40, 38]}]. The response names per request anfrage (index), room, currency, totalCents, perTravellerCents, availability (available, minFree) or errorCode.

  • All or nothing: verify the package (sha256 and length per file), build the new state completely next to the old one, then switch atomically; only then are epoch and to_seq considered stored. A chain with pages (full as well as changes) is only switched at the end of the chain (more: false); if it aborts, the old state remains. A full chain always starts with from_seq: 0; a full follow-up page only attaches to the open chain, never to a stored state – even if its from_seq equals the stored to_seq (a full carries no removed; deleted hotels would otherwise remain).
  • Never backwards: what counts is the target of the chain, i.e. the to_seq of the last page (more: false), not that of the first. The first page of a full chain often carries a smaller to_seq than the stored state (e.g. after 410: the oldest files come first) and is still not a step backwards. A full chain of the same epoch whose target is smaller than the stored state, or a full of an older epoch (the epoch is a ULID, sorted by time) is rejected – an old package delivered late does not reset the cache. changes must attach seamlessly to the state (from_seq = stored to_seq). A full of another tenant or scope (swapped key) is also rejected, as a scope error before the backwards check.
  • Rejected full: the old state remains, the cache keeps calculating on it but no longer fetches anything new. This is intentionally loud – never silently jump back. If there is no swapped key and the delivery really is on an older epoch (e.g. after a restore on a machine whose clock is behind: the new epoch ULID is then smaller) or a smaller to_seq, deliberately reset the state (reference receiver: edf-empfaenger reset) and fetch a new full. If in doubt, ask the operator first.
  • Reading during the fetch: a fetch may take time (large chain, waiting on 429). Prices from the cache meanwhile continue on the old state and after the switch on the new one; delete the old state only when no one is reading from it anymore. The reference receiver allows one writer (pull, apply, reset) and any number of readers (stand, rechne) per inventory. For 429 it waits at most --max-warten per response and at most --max-warten-gesamt in total per fetch, then aborts (state unchanged).
  • Retry instead of restarting: the reference receiver fetches a follow-up page with a brief error up to three times with the same parameters (rule in 10.2); it does not retry the start of a chain.
  • Price from the cache: EDF 5.1.6 with the declared rules, plus the API's rules: the same request limits (appendix, same error codes; reference date = today in Europe/Berlin); with a room the cheapest offer of this room code, without a room the cheapest available room, and if none is available the cheapest priceable one (as in section 3.1); a room that does not offer the board is not priced.
  • Declared rules: every hotel file states in SellingData the child order (ChildrenAgeOrder, Descending = oldest child first: who is the "1st child", which child takes a free full-payer slot), FullPayerDoesNotAffectBoardCharges/PersonType=C (a child on the full-payer slot pays the full base price, board at the child rate) and Rounding Mode="Commercial" DecimalPlace="2" Scope="Person" (one rounding per traveller, section 1.4). Where the schema leaves room for interpretation, TourAPI calculates as follows – the cache must do the same, otherwise it deviates: MinCount/MaxCount count children per person type from the first child, adults from the first one after the full payers; an infant counts in a person condition ("from 3 persons") only if the room counts it towards occupancy (Infants/@ApplyToOccupancy Min, Max or Yes); a missing ExtraMaxApply means 1; several date windows with Operator="AND" apply as their intersection; percentage surcharges and discounts alongside a free night apply only to the nights not waived; a fixed amount per person (PS, PN) also applies per person with a per-unit price; room line items (per-unit price, unoccupied full-payer slot) belong to the oldest traveller and are rounded with them.
  • Availability from the pattern: minimum across the nights [checkIn, checkOut) of the room as in 10.1; a night the file does not mention is closed (never "open because nothing was delivered"). The configured field of /v1/price has no equivalent in the cache: a room without an allotment appears there as 00.
  • Live before booking: the cache can be one state behind the API; /v1/book rejects a night that is open in the cache but has since been blocked or sold out (ERR_STOP_SALE, ERR_SOLD_OUT).

11. Sandbox: test keys and test bookings

A test key (tk_test_…) is the twin of a live key: the same tour operator, the same customer group, the same permissions (export permission, later search profile) — taken over from the live key at runtime. It reads the same data as the live key (hotels, prices, destinations, availability, EDF delivery), but bookings end up in the sandbox: never at the tour operator, no allotment, no export, no booking list in the console. This lets you test booking, booking info, cancellation, idempotency and retry logic with real offers.

  • The tour operator issues the test key for the live key in the console (button "Test-Key" on the key row) and passes it on with its own access package – or you issue it yourself in the partner portal (7, 30 or 90 days) if the tour operator allows this for your access. Valid for 30 days (at most 90), at most 5 active test keys per access.
  • If the live key is revoked or the test key has expired: 401 ERR_UNAUTHORIZED. Customer group deactivated: 403 ERR_KEY_GROUP_INACTIVE as in live. After a rotation of the live key the test key keeps working with the new live key; it never ends later than its live key.
  • The mode is determined by the assignment at the tour operator, not by the key's prefix.

11.1 Labelling and protection against mix-ups

Every response to an accepted key carries the header X-TourAPI-Mode: test or live (compare header names case-insensitively, as usual in HTTP: the server writes X-Tourapi-Mode). Booking, booking info and cancellation of a test key additionally carry the field "sandbox": true, and the reference starts with SB- (live TA-). Reading with the test key returns the same response as with the 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 environments (CI, agents, developer machines) send the header X-TourAPI-Require-Mode: test. If a live key then arrives, the API rejects every request and executes nothing – so a live key used by mistake cannot book (the check is on the server, not in the client). X-TourAPI-Require-Mode: live conversely requires a live key; any other value is 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 Test booking, booking info, cancellation

/v1/book with a test key runs through the same checks in the same order as live (fields, retry, hotel, reference date and limits, sales rules, priceCheck, availability) and returns the same error codes. Availability is checked but not consumed:

### 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 and live worlds are separate: a test key sees and cancels only test bookings, a live key only real ones. The same idemKey books independently in both worlds.

### 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}
  • Test bookings are deleted 30 days after creation. At most 5,000 open (not cancelled) test bookings per access; beyond that 422 ERR_SANDBOX_LIMIT.
  • TourAPI logs a test key's calls (method, path, status, errorCode, duration, request and response) for 7 days so that the tour operator can help with troubleshooting. Therefore: use test names in test bookings.
  • EDF delivery with a test key: fetches (full, changes) and acknowledgements (ack) appear only in this log of test calls, never in the tour operator's delivery log – they do not count as a pickup or processing in the delivery's health.

11.3 Scenarios: forcing error cases

With the header X-TourAPI-Sandbox-Scenario a test key forces an error case – for testing retry logic (sections 8 and 9). With a live key, an unknown name or at an endpoint where the scenario has no effect: 422 ERR_SCENARIO_NOT_ALLOWED.

Scenario
booking_busy
Endpoints
/v1/book, /v1/cancel
Effect
first attempt per idemKey and endpoint (booking and cancellation count separately): 503 ERR_BOOKING_BUSY with Retry-After: 1, nothing booked or cancelled; the retry with the same idemKey goes through
Scenario
price_drift
Endpoints
/v1/book with priceCheck
Effect
first attempt per idemKey: 409 ERR_PRICE_DRIFT; the price itself does not change (message states the current and the expected price, both equal); fetch the new price, book again
Scenario
sold_out
Endpoints
/v1/book
Effect
always 422 ERR_SOLD_OUT (after all other checks)
Scenario
rate_limited
Endpoints
all
Effect
first request per test key and endpoint: 429 ERR_RATE_LIMITED with Retry-After: 1
Scenario
search_busy
Endpoints
/v1/search
Effect
first request per test key: 429 ERR_SEARCH_BUSY with Retry-After: 1
Scenario
price_timeout
Endpoints
/v1/price, /v1/prices
Effect
first request per test key and endpoint: 503 ERR_PRICE_TIMEOUT

"First request" applies for 10 minutes; every retry within this time goes through normally. The memory is kept per API node.

### 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 Test key limits

All test keys of an access share one bucket: 20 requests/s, burst 40 (the live key's limit remains unaffected). A tour operator's test keys occupy at most 2 concurrent searches. The EDF delivery cadence (full 1/h, new changes chain 1/min) applies per access, not per test key.

11.5 What the sandbox does not prove

  • Allotment and races for the last room: test bookings consume nothing. A test booking can succeed where live someone else was faster.
  • Processes at the tour operator after the booking (confirmation, modification, invoice).
  • Behaviour under live load (own, smaller bucket, see 11.4).

11.6 AI agents: MCP server in the portal

The portal offers an MCP server (Model Context Protocol, transport “Streamable HTTP”, stateless, tools only) at /mcp. An AI agent such as Claude Code, Codex or Antigravity uses it to call the API through tools instead of writing HTTP code. Every tool is exactly one /v1 call with the key of the MCP request (X-Api-Key or Authorization: Bearer) and X-TourAPI-Require-Mode: test – behaviour, limits, isolation and error codes are those of the API. Test keys only: a live key gets ERR_MODE_MISMATCH (403), nothing is executed. The portal stores no key.

ToolCallArguments
list_destinationsGET /v1/destinationsnone
search_hotelsPOST /v1/searchrequest body; pages via cursor
price_offerPOST /v1/pricerequest body
price_all_roomsPOST /v1/pricesrequest body
sandbox_bookPOST /v1/bookrequest body, idemKey required
get_bookingGET /v1/bookingref
sandbox_cancelPOST /v1/cancelrequest body
get_limitsGET /v1/limitsnone
open_searchPOST /v1/search/openrequest body
open_search_datesPOST /v1/search/open/datesrequest body
hotel_detailsGET /v1/content/hotels/{code}code, lang

Tools with a request body pass their arguments unchanged to the API as the JSON body (schema = OpenAPI of the endpoint). The others take only the arguments listed; any other gives ERR_UNKNOWN_FIELD, an invalid value ERR_BAD_REQUEST – without an API call.

hotel_details only with the content right. tools/list asks GET /v1/limits with the key of the request and lists hotel_details only for content.allowed: true (the same rule as the content API, 4c); without a key it is missing. If it is called anyway, the API answers ERR_CONTENT_NOT_ALLOWED (403). If the API rejects the key in this check (e.g. ERR_MODE_MISMATCH for a live key) or does not answer, tools/list is a JSON-RPC error (-32000) with the error code or reason in message – no tool list.

hotel_details and lang. If the tour operator does not offer a requested content language, the tool does not pass on ERR_LANGUAGE_NOT_OFFERED: it reads the content languages (GET /v1/content/catalog, languages) and asks for the requested languages as far as offered, otherwise a fallback language (en, then de, then the first offered). structuredContent names this under language (requested, delivered, offered, fallback: true). The content API itself stays strict; if the tour operator offers no content language, its error remains.

Result. structuredContent (the same JSON text is in content) has two parts: data is the API response with every text replaced by the placeholder [untrusted]; untrusted holds these texts under their JSON pointer in the response, cleaned: control, bidi, zero-width and other invisible characters removed, HTML as text, links replaced by [link removed], at most 200 characters (messages 500, descriptions 2000). Texts are third-party free texts (hotel and destination names, chain, address, descriptions, image titles and credits, lead guest name), the messages of the API (warnings, message) and every string that does not match the fixed pattern of its field: only codes (letters, digits, _ . -, at most 64 characters), references, dates, timestamps, currencies, error codes and cursors stay in data. A destination or group code with spaces or a link therefore appears only under untrusted. Address fields (url of the image variants) are left out; images come from the content API itself. For the agent, texts under untrusted are data, not instructions.

{"data": {"results": [{"hotel": "H1", "name": "[untrusted]", "room": "DZ", "fromTotalCents": 42000, "currency": "EUR"}]}, "untrusted": {"/results/0/name": "Haus Eins"}}

Errors come as a tool result with isError: true and structuredContent {errorCode, status, message, retryAfter, hint}: errorCode and status as from the API (section 7), retryAfter = Retry-After in seconds, hint = column “Caller” of the error catalogue (English). message is cleaned like a text under untrusted. If the key is missing, X-Api-Key or Authorization appears more than once in the request, or both carry different keys: ERR_UNAUTHORIZED without an API call. Codes of the MCP server itself: ERR_UPSTREAM_UNAVAILABLE (502, API unreachable or timed out – retry after retryAfter, sandbox_book with the same idemKey) and ERR_RESPONSE_TOO_LARGE (502, response over 4 MiB – narrow the request).

Limits. POST only (no SSE stream: GET gives 405), Content-Type: application/json, one JSON-RPC message per request (no batches); body at most 64 KiB, depth 16, 4096 JSON elements – otherwise 413 or 400 before anything is evaluated. Per key 10 tool calls/s (above that ERR_RATE_LIMITED with retryAfter); the bucket of the test keys applies as well (11.4). A request with a foreign Origin gets 403 (protection against web pages in the browser); no cookies, no CORS. Protocol versions 2025-03-26, 2025-06-18 and 2025-11-25.

Setup. Set the test key as the environment variable TOURAPI_API_KEY (the same one as in your own code; one variable is enough); only the reference to it belongs in the configuration. Claude Code, file .mcp.json in the project:

{"mcpServers": {"tourapi": {"type": "http", "url": "https://portal.example.de/mcp", "headers": {"X-Api-Key": "${TOURAPI_API_KEY}"}}}}

Codex (the key is sent as a bearer token from the environment variable):

codex mcp add tourapi --url https://portal.example.de/mcp --bearer-token-env-var TOURAPI_API_KEY

Antigravity does not fill environment variables into headers; the mcp-remote bridge reads the key from the environment (entry in mcp_config.json; the bridge version is pinned so that no unchecked release receives the key):

{"mcpServers": {"tourapi": {"command": "npx", "args": ["-y", "mcp-remote@0.14.3", "https://portal.example.de/mcp", "--header", "X-Api-Key:${TOURAPI_API_KEY}"]}}}

The “AI agents” page in the portal shows the same entries with the address of your portal.

12. Hotel content: /v1/content/*

The Content API delivers the tour operator's hotel content for the consumers' websites and catalogues: master data (accommodation type, chain, address), location (coordinates), categories, facts, texts, images and amenities. It is a separate path next to price and booking: content never changes a price or availability.

RoutePurpose
GET /v1/content/hotelsDirectory of the key's hotels, ascending by code, paged
GET /v1/content/hotels/{code}Content of one hotel (ETag, If-None-Match → 304)
GET /v1/content/changes?since=…Changes since a state, in the order in which they were saved
GET /v1/content/catalogCatalogues with labels (accommodation types, category schemes, text and image types, amenities)

12.1 Access and scope

  • The Content API only answers if both apply: the platform operator has enabled content for the tour operator, and the key carries the content right (granted by the tour operator admin under API access, off by default). Otherwise 403 ERR_CONTENT_NOT_ALLOWED. A test key has the right of its live key and reads the same content.
  • A tour operator that the platform operator runs as a test environment is only served by the installation set up for it. Everywhere else it counts as not enabled: the same answer 403 ERR_CONTENT_NOT_ALLOWED.
  • Which hotels a key sees is determined by the key as for /v1/destinations (1.1): without a customer group and with a price group all hotels of the tour operator, with an allotment group only hotels with an allotment – independent of availability. Every other hotel is 404 ERR_HOTEL_NOT_FOUND.
  • Only visible texts in the tour operator's content languages and visible, fully processed images are delivered. The hotel's contact details (phone, mail, web) do not exist in the Content API, not even as an empty field.
  • Images are served from public, unguessable addresses (<base>/m/<key>/<size>.jpg, retrievable without a key, cacheable for 1 day). Next to every image there is credit; with attributionRequired: true the credit must be shown next to the image.
  • Keys do not belong in browser code (1.1): the website reads the Content API on the server, only the image addresses go to the browser.

12.2 Directory

### 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 (default 500). The next page is fetched with cursor=<nextCursor>; without nextCursor the directory is complete.
  • contentVersion counts every change to what is delivered for a hotel; 0 = no content yet (then updatedAt is missing). category is the official national category, geo the location – both only if maintained. websiteReady and missing as in a hotel's content (12.3).
  • feedToken is the state from which /v1/content/changes continues. All pages of one directory pass carry the same state (that of the first page).
  • scopeHash changes as soon as the key's set of hotels changes (new or deleted hotel, allotment added or removed). This is not in the feed: then fetch the directory again.
  • cursor, feedToken and next are opaque and bound to the key: with another key, modified or after the server changed its key 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 Content of a hotel

### 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"]
}
  • Languages: without lang all texts in all of the tour operator's content languages are returned. With lang (1–5 languages, ISO 639-1 lower case, comma-separated) there is exactly one entry per text type and requested language. If the text is missing in that language, the tour operator's default language steps in, then English; the entry then carries fallbackFrom with the requested language (here: no text in Turkish, German is returned). If there is none there either, the entry is missing. Titles (titles) and alternative texts (alts) of the images follow the same rule. A language the tour operator does not offer: 422 ERR_LANGUAGE_NOT_OFFERED (the active languages are listed in warnings).
  • html contains only p, br, b, strong, i, em, ul, ol, li without attributes. machineTranslated: true marks a machine translation.
  • categories: kind official (national category) or operator (the tour operator's rating), scheme stars or keys, value 1–5 in half steps; "4 Superior" is value: 4, superior: true, never 4.5.
  • geo.precision: address, street, locality or unknown.
  • media in display order, order: 1 is the main image. Per image variants with the generated widths (w320 to w2048, never wider than the original), each with width, height, bytes and url; focus (x, y in percent) is the most important image point for your own crops. id is stable as long as the image stays with the hotel.
  • amenities: amenities with a code from the catalogue (12.5). available: false means explicitly not present; a missing amenity is unknown. Depending on the amenity with count, distanceM, areaM2, ref (airport code for the distance) and, for amenities that can be chargeable, charge (included, extra, unknown).
  • name, destination and giataCode come from the hotel's contract. If one of them changes, the hotel appears in the feed (12.4, reason: contract), and from then on detail and directory return the new value.
  • websiteReady and missing: the "website ready" maturity level – whether the operator has maintained enough content for a website. A key figure only; it changes neither sales nor delivery. missing lists the unmet criteria in a fixed order, empty = website ready: general_text (general text in the operator's default language), geo (location), category (category or accommodation type), images (at least 5 delivered images including the main image), amenities (at least 10 maintained amenities, explicit "not available" included). The assessment only changes with the content (new contentVersion); the operator's console shows the same one. Ignore unknown values in missing (criteria may be added). The directory carries both fields as well.

Every response carries an ETag. It changes with the content, the language selection and the presentation. With If-None-Match the answer is 304 without a body as long as nothing has changed:

### inhalt-hotel-unveraendert
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE?lang=de,tr
X-Api-Key: {{apiKey}}
If-None-Match: {{inhaltEtag}}

Without lang all content languages (here en as a machine translation):

### 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 Changes

### inhalt-aenderungen
GET {{baseUrl}}/v1/content/changes?since={{feedToken}}
X-Api-Key: {{apiKey}}
{"changes": [], "next": "{{*}}", "more": false, "scopeHash": "{{*}}"}
  • since is the directory's feedToken or next of the previous response (mandatory, otherwise 400 ERR_BAD_REQUEST). pageSize 1–1000 (default 500).
  • Per entry code, change (upsert = content changed, fetch again; removed = hotel deleted), for upsert the current contentVersion, and optionally reason (languages = the tour operator's content languages have changed, media_ready = an image has been fully processed, contract = name, destination or giataCode has changed with the contract, deleted for removed). Ignore unknown values.
  • In the order in which the changes were saved, without gaps; a hotel appears at most once per page (with its latest state). more: true = continue reading with next right away.
  • Only the key's hotels. A change of the set of hotels is not in the feed but in the scopeHash.
  • The feed goes back 30 days. An older state: 410 ERR_CONTENT_CURSOR_EXPIRED – then fetch the directory again and continue with its feedToken.

12.5 Catalogue

### 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 are the tour operator's content languages, defaultLanguage its default language. lang selects the label languages (de, en, tr; without lang all).
  • Per amenity group, valueType (flag, anzahl, meter, flaeche_m2, meter_mit_bezug), unit and chargeable. A code is never reinterpreted; locked: true means discontinued (stays readable). version changes with every new catalogue; the catalogue carries an ETag.

12.6 Synchronisation for consumers

  1. Initial load: fetch the directory page by page, remember feedToken and scopeHash, fetch the content of each hotel and store the ETag.
  2. Ongoing (roughly every few minutes): changes?since=<token> (on the first call feedToken, afterwards the last remembered next), fetch the changed hotels again, delete removed, remember next. If scopeHash changes: step 3 right away.
  3. Daily and on 410: fetch the directory again and reconcile it with your own data (delete missing hotels, fetch new ones, compare contentVersion).
  4. Always fetch content with If-None-Match.

Access and scope errors:

### 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. Planned and limits

Planned: Within /v1 only additive changes are made (section 1.9); every change is listed with its date in the changelog. No change is currently announced that would require an existing integration to be adapted.

Limits: The numeric values (sizes, rates, time limits) are in the appendix "Limits at a glance"; what the sandbox does not cover is in 11.5.

Appendix: Limits at a glance

What
Body per request
Value
1 MiB
What
Nights per stay
Value
1–30
What
Arrival
Value
reference date to reference date + 732 days
What
Travellers per request, age
Value
1–20, 0–120
What
Rooms per booking (quantity)
Value
1–1,000,000
What
idemKey, reference
Value
128 characters (longer: 422 ERR_VALIDATION)
What
leadPaxName
Value
255 characters (longer: 422 ERR_VALIDATION)
What
metadata.correlationId
Value
64 characters (longer: 422 ERR_VALIDATION)
What
Rate per key
Value
200/s, burst 400
What
Test keys per access
Value
at most 5, valid up to 90 days; together 20/s, burst 40; 2 concurrent searches per tour operator
What
Test bookings
Value
5,000 open per access, deleted 30 days after creation; log of test calls 7 days
What
Search time limit
Value
10 s
What
Search page
Value
default 50, at most 100 hotels
What
Open search
Value
per key according to the search profile (window, lengths, destinations, page, time budget, rate; date matrix: window and cells), retrievable with GET /v1/limits; page 20 if omitted; cursor 15 min
What
Connection
Value
15 s read, 15 s write
What
EDF delivery per key
Value
full 1/h, new changes chain 1/min, follow-up pages/acknowledgements 5/s (burst 50); package responses may write for up to 10 min
What
Content API
Value
directory and feed page 1–1000 (default 500), lang 1–5 languages, feed 30 days