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.
- 1. Getting started
- 2. Integration flow
- 3. Price: /v1/price and /v1/prices (BA)
- 4. Search: /v1/search (BA)
- 4a. Open search: POST /v1/search/open (BA)
- 4b. Date matrix: POST /v1/search/open/dates (BA)
- 4c. Limits of the key: GET /v1/limits
- 5. Booking (B) and booking info
- 6. Cancellation (S): POST /v1/cancel
- 7. Error catalogue
- 8. Fairness: rate limit and search gate
- 9. Idempotency and concurrency
- 10. EDF delivery (cache export)
- 11. Sandbox: test keys and test bookings
- 12. Hotel content: /v1/content/*
- 13. Planned and limits
- Appendix: Limits at a glance
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.
| Code | Meaning | Endpoint |
|---|---|---|
| BA | Query availability and price, search | /v1/search, /v1/price, /v1/prices |
| BA | open search without fixed dates (permission per key) | /v1/search/open (section 4a) |
| BA | date matrix of a hotel (permission per key) | /v1/search/open/dates (section 4b) |
| – | effective limits of your own key | /v1/limits (section 4c) |
| B | Book | /v1/book |
| – | Read booking info | /v1/booking |
| S | Cancel | /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, scenarios | all 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-Keyheader. 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.
- 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
(
- 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 reasonERR_HOTEL_NOT_FOUNDindiagnostics.reasons,/v1/bookalso rejects with404) – 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 theX-TourAPI-Modeheader.
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 withinv1is 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.GETendpoints are/v1/booking,/v1/destinations,/v1/healthand the EDF delivery (/v1/export/edf/full,/changes); all others arePOST. 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/…) returns404without a JSON body (text/plain, noerrorCode); all other errors have the form from 1.6.
1.3 Date, stay, reference date
- Date fields are calendar days
JJJJ-MM-TTwithout time or zone. - A stay is half-open:
checkInis the first night,checkOutthe departure day.checkIn=2026-10-26,checkOut=2026-10-28are 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
nowis only tolerated: leave it empty. If you send a different date, the query still uses the server date and says so inwarnings;/v1/bookrejects a differingpriceCheck.now(ERR_NOW_MISMATCH). - Limits per request (applied before any calculation):
| Limit | Value | Code (422) |
|---|---|---|
| Nights per stay | 1–30 | ERR_EMPTY_STAY, ERR_STAY_TOO_LONG |
| Earliest arrival | today (reference date) | ERR_STAY_IN_PAST |
| Latest arrival | reference date + 732 days | ERR_STAY_TOO_FAR |
| Travellers per request | 1–20 | ERR_NO_TRAVELLERS, ERR_TOO_MANY_TRAVELLERS |
| Age | 0–120 | ERR_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 (
roundingin 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 isperTravellerCents[i],totalCentsis 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 inbreakdown[].amountExact(section 3.1). - The currency is the hotel's contract currency (
currency, ISO 4217). TourAPI does not convert.currencyin the request is a preference: if it differs, the response comes in the contract currency with a note inwarnings. If no currency is set on the contract,currencystays empty, also with a note – "EUR" is not guessed. - With
/v1/bookandpriceCheck, 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:
200with the result. Errors:4xx/5xxwith{"errorCode": "ERR_…", "message": "…", "warnings": ["…"]}errorCodeis stable and intended for programs;messageis 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, ignorednow. 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 headerServer-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 inwarnings. Withinoccupancyit is rejected (422 ERR_UNKNOWN_FIELD), because a typo there changes the price./v1/bookand/v1/cancelreject 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 % onTEST-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
epochandto_seqfrom the manifest of theexport-vollexample
- Placeholder
{{ohnePreisRef}},{{zweiteRef}}- Meaning
- Booking references from the examples
buchen-ohne-preispruefungandbuchen-gleiche-kundenreferenzrespectively
- 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
buchenexample
- Placeholder
{{feedToken}},{{inhaltCursor}}- Meaning
feedTokenandnextCursorfrom theinhalt-verzeichnisexample
- Placeholder
{{inhaltEtag}}- Meaning
ETagfrom theinhalt-hotelexample
- 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 /v1 | Never within /v1 (that would be /v2) |
|---|---|
| new optional request fields | new required request fields |
| new response fields | removing or renaming fields |
| new endpoints | changing the type or meaning of a field |
new ERR_* codes, each with documented handling in the error catalogue | changing 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). messageandwarningsare 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: validdestinationcodes 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, withidemKeyandpriceCheck(the price just quoted)./v1/booking: reads the booking via our reference or your own./v1/cancel: cancels via the sameidemKey.
Rules you need to know:
- Search and price are informational; only
/v1/bookis binding. Between price and booking the tour operator can change prices or allotment. WithpriceCheck,/v1/bookrejects a changed price (409 ERR_PRICE_DRIFT) instead of silently booking at the new one. - 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). - 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 inwarningswith room and code; if no room calculates, the error comes as 422. Withroom, that room's error always comes as 422.
- Field
board- Required
- yes
- Meaning
- Board code, exactly as in the contract (
RO, notro). Missing:ERR_BOARD_MISSING; if the room (withoutroom: 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
roomin 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 inperTravellerCents),night(index from 0,-1= per stay),amountExact(exact amount as a decimal, e.g."-13.485"),amountCents, for extrasapplianceCode, for extras of an extra family (several extras with the sameapplianceCode) additionallyvariant(the variant; only both together identify the extra), for the line items of a free night additionallyfreeNight: 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.codeidentifies the extra, for an extra family together withvariant.amountCentsis the exact sum of the extra, rounded once; withinTotal=trueit can differ from the sum of itsbreakdownrows by several cents –breakdownis 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
roomalso 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:
| Night | Contribution to minFree | Allotment file (section 10) |
|---|---|---|
| 1 to 99 units free | the count | 01–99 |
| more than 99 free or free sale | does not lower minFree | ** |
| Stop sale | 0 | SS |
| On request | 0 | RR |
| sold out, closed, no capacity, customer group without allocation for room and night | 0 | 00 |
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 roomavailable=false(minFree0), but it staysconfigured=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:
| Code | The rule requires … | Caller |
|---|---|---|
ERR_STAY_LENGTH_NOT_ALLOWED | a different length of stay (minimum or maximum nights) | change the length |
ERR_ARRIVAL_DAY_NOT_ALLOWED | a different arrival or departure weekday | shift the travel dates |
ERR_TRAVEL_DATES_NOT_ALLOWED | travel dates within a specific period (sales window) | different period |
ERR_BOARD_NOT_ALLOWED | a different board for this stay | different board |
ERR_LEAD_TIME_NOT_ALLOWED | more lead time: the arrival lies within the release period | later 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: withoutroom, 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'sERR_OCCUPANCY_NOT_ALLOWEDbecause 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 indiagnostics.reasons./v1/book: withpriceCheckthe rule applies to the requested board; withoutpriceCheck(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: withoutroom, 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 inrooms[].errors[]witherrorCode(3.2); the rest stays priced./v1/search: the hotel is dropped, the code appears indiagnostics.reasons./v1/bookwithpriceCheck: 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.
| Field | Required | Meaning |
|---|---|---|
destination | no | Destination or airport code of the hotel (below); empty = all hotels |
board, checkIn, checkOut, occupancy, currency, now | as /v1/price | |
includeUnavailable | no | true: non-bookable hotels are included, flagged (default false) |
pageSize, cursor | no | Pages, 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=falsethe 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 withincludeUnavailable): 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/bookrejects the same stay withERR_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
pageSizehotels (default 50, at most 100; outside 1–100:422 ERR_BAD_PAGE_SIZE) in hotel code order. - If there are more hotels,
nextCursoris in the response. The next page: the same request plus"cursor": "<nextCursor>". On the last pagenextCursoris absent. - Price sorting applies per page. If you need a complete list by price, page
through and sort yourself.
diagnosticsapplies 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 withoutcursor.pageSizeandcurrencymay 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, andnextCursorleads on. This is not an error: just keep paging. - If you send neither
pageSizenorcursorand there are more hotels, there is also a note inwarnings– 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.
| Field | Meaning |
|---|---|
destinations[].code | Code, to be passed exactly like this in destination |
destinations[].name | Plain 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.totalCentsandbest.perTravellerCentsare bit-identical to/v1/pricewith 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 withboards: 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):
schemestarsorkeys(required),min/maxlevel 1 to 5 in half steps as a number (e.g.3.5, equivalently3.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) andradiusKm(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
nextCursorof 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, alwaysavailable: 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/roomsaccordingly. 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/priceanswers 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, orERR_OUTSIDE_PRICE_FILTERif its price lies outside the filter; in additionERR_CURRENCY_NOT_AVAILABLE(hotel prices in a different or unknown currency),ERR_HOTEL_NOT_FOUND(no offer for the customer group) andERR_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:
nextCursorleads to the next page: the same request plus"cursor": "<nextCursor>";pageSizemay change. The next page continues after the last result (criterion, then hotel code) – all pages together are one sorted list. On the last pagenextCursoris missing; that page may also be empty. Withsort=priceandpricePerNighta 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 withoutcursor. A cursor from/v1/searchis not valid here. - Data state:
standchanges 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, withcoverage.standChanged: trueand a note inwarnings. Then (as also with bookings by others between two pages) a hotel may appear twice or be missing: deduplicate byhotel. The prices of each page apply to its data state; when booking,priceChecksecures 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 andnextCursorfor 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_LIMITEDorERR_SEARCH_BUSY, withRetry-After); a search rejected with429 ERR_SEARCH_BUSYdoes not consume rate. If a search calculates for longer than 10 s, it ends with503 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_INTERNALinstead 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/pricewith the same hotel, room, board, arrival and departure and the same occupancy. - Matches the open search: with the same filter and the same
stand,bestfrom 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). WithperBoard,bestis 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[].hotelfrom 4a)
- Field
arrivalFrom,arrivalTo- Required
- yes
- Meaning
- Arrival window as in 4a.2; at most
matrixMaxWindowDaysdays (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
boardsmust 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 (defaultfalse: 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 (
cellsempty); if it lacks the value, every cell isnonewithERR_NO_CATEGORY,ERR_NO_REGIONorERR_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(withperBoardthen board); every cell withcheckIn,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, alwaysavailable: 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 dataERR_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 cellunchecked),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 access403 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). Ifroomis missing:400 ERR_INVALID_BUCKET, with and withoutpriceCheck
- 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
priceCheckis 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 priceCheck | totalCents = checked price, currency = contract currency, board = board from the priceCheck |
without priceCheck | totalCents: null, currency: "", board: "" (empty strings, no price recorded) |
without reference | customerReference absent |
without metadata | metadata absent |
| with a key without a customer group | group 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-Moderequires 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-Scenariowith 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),
refmissing,priceCheck.tolerancePercentnegative,priceCheck.currencylonger than 3 characters (shorter or unknown:422 ERR_CURRENCY_NOT_AVAILABLE); Content API:sincemissing, duplicate parameter,langwith 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/limitsshows 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(messagenames 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
boardmissing (/v1/price,/v1/search,priceCheck)- Caller
- Fix the request
- Code
ERR_NOW_MISMATCH- Status
- 422
- Meaning
priceCheck.nowis 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);messagenames field and limit - Caller
- Fix the request
- Code
ERR_QUANTITY_INVALID- Status
- 400
- Meaning
quantityoutside 1–1,000,000- Caller
- Fix the request
- Code
ERR_INVALID_IDEM_KEY- Status
- 400
- Meaning
idemKeymissing (/v1/book,/v1/cancel)- Caller
- Fix the request
- Code
ERR_INVALID_BUCKET- Status
- 400
- Meaning
roommissing (/v1/book, with and withoutpriceCheck)- 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
pageSizeoutside 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/sinceunreadable, modified or from another key - Caller
- start over without
cursoror 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/searchor/v1/search/openwithoutcursor: there is no hotel fordestinationordestinations[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/arrivalTomissing orarrivalTobeforearrivalFrom - Caller
- Fix the request
- Code
ERR_BAD_NIGHTS- Status
- 422
- Meaning
- open search and date matrix:
nightsMin/nightsMaxmissing, < 1 ornightsMin>nightsMax - Caller
- Fix the request
- Code
ERR_BAD_TARGET- Status
- 422
- Meaning
- open search:
destinationsandhotelstogether, empty list, empty code, or neither although the search profile only allows individual destinations; date matrix:hotelmissing - Caller
- Fix the request
- Code
ERR_BAD_SORT- Status
- 422
- Meaning
- open search:
sortunknown (price,pricePerNight,hotel) - Caller
- Fix the request
- Code
ERR_BAD_FILTER- Status
- 422
- Meaning
- open search and date matrix: empty
boards/boardTypes/roomslist or empty entry, unknown board type, price filter negative orminTotalCents>maxTotalCents,currencynot 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 (
maxWindowDaysormatrixMaxWindowDays,messagenames 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 + 1too wide; date matrix: length outside the allowed lengths (messagenames the limit) - Caller
- adjust the length
- Code
ERR_SEARCH_TOO_BROAD- Status
- 422
- Meaning
- open search: too many
destinationsorhotelsin the request or too many hotels in the search scope; date matrix: more cells thanmatrixMaxCells(messagenames 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,
currencyis missing (messagenames 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
referencematches several bookings (/v1/booking);messagenames theTA-…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 matchesboards/boardTypes); in search a reason indiagnostics.reasonsorcoverage.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/pricesan entry inrooms[].errors[], in search a reason indiagnostics.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/pricesan entry inrooms[].errors[], with/v1/pricewithoutroominwarnings, in search indiagnostics.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.currencydoes not match the contract currency, or the latter is unknown; in the open search a reason incoverage.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
idemKeyalready used with different booking data- Caller
- Error in the caller: assign unique keys
- Code
ERR_IDEM_KEY_RELEASED- Status
- 409
- Meaning
idemKeybelongs 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/pricewithoutroomor/v1/pricesexceeded the time limit (2 s)- Caller
- switch to
/v1/pricewithroom(the limit always applies to/v1/prices, even withroomandboards), 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 sameidemKey
- 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/bookwith the sameidemKey
- 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;
messagenames 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.
messagenames 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/sincemissing or unreadable,untilnot a valid chain key,max_bytesnot a number ≥ 65536, duplicate parameter, page in the middle of a state withoutuntil, follow-up page of thefullwithoutepoch/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
epochdoes not match (feed rebuilt) or the state (since,until,seqof 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
langnames 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 inwarnings- Caller
- Fix the request
- Code
ERR_CONTENT_CURSOR_EXPIRED- Status
- 410
- Meaning
sincelies 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/pricewithoutroomand/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-Afteris in whole seconds (at least 1). Do not retry before; afterwards retry with the same request (for/v1/bookwith the sameidemKey).- If you send again before
Retry-Afterhas elapsed and are rejected again, you receive the429only 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 a429, 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/healthhas 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:
idemKeyis 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,leadPaxNameandpriceCheckof 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.nowno longer the reference date) or after a price change. Only a newidemKeygoes 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
idemKeyis 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_BUSYwithRetry-After– retry with the sameidemKey. - 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_BUSYwithRetry-After(section 7.4). Nothing is booked or cancelled; after the wait time retry with the sameidemKey. - 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
5xxit is unclear whether a booking was made: retry with the sameidemKey, 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/path | Response |
|---|---|
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/searchand/v1/pricefor 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 is422 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
removedwith a reason (withdrawn:deleted,withdrawn:variant_error,withdrawn:not_exportable,withdrawn:no_currency,withdrawn:not_in_universe). The buyer deletes them from its cache. Afullalways hasremoved: []– 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 is00. 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 ofPatternbelong to the nightStart, each further pair to the following night. - Pattern and
minFree:SS,RRand00explicitly result inminFree0(not bookable); only**does not lowerminFree, and-1means: every night**. The minimum across the nights of a stay isavailability.minFreeof/v1/pricefor 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/sincemissing or unreadable,untilnot a valid chain key (foreign, expired, number, forfullfor a different state),max_bytesnot a number ≥ 65536, page in the middle of a state withoutuntil, state above the chain target, follow-up page of thefullwithoutepoch/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
epochdoes not match (feed rebuilt) orsince/untilor the acknowledgement'sseqis above the current delivery state (greater than the last delivered state; an oldersinceis 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:
| Response | uses 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_EPOCH | yes |
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 (Eoutdated)- 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
epochandto_seqconsidered stored. A chain with pages (fullas well aschanges) is only switched at the end of the chain (more: false); if it aborts, the old state remains. Afullchain always starts withfrom_seq: 0; afullfollow-up page only attaches to the open chain, never to a stored state – even if itsfrom_seqequals the storedto_seq(afullcarries noremoved; deleted hotels would otherwise remain). - Never backwards: what counts is the target of the chain, i.e. the
to_seqof the last page (more: false), not that of the first. The first page of afullchain often carries a smallerto_seqthan the stored state (e.g. after410: the oldest files come first) and is still not a step backwards. Afullchain of the sameepochwhose target is smaller than the stored state, or afullof an olderepoch(theepochis a ULID, sorted by time) is rejected – an old package delivered late does not reset the cache.changesmust attach seamlessly to the state (from_seq= storedto_seq). Afullof 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 olderepoch(e.g. after a restore on a machine whose clock is behind: the newepochULID is then smaller) or a smallerto_seq, deliberately reset the state (reference receiver:edf-empfaenger reset) and fetch a newfull. 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. For429it waits at most--max-wartenper response and at most--max-warten-gesamtin 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
SellingDatathe 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) andRounding 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/MaxCountcount 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/@ApplyToOccupancyMin,MaxorYes); a missingExtraMaxApplymeans 1; several date windows withOperator="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"). Theconfiguredfield of/v1/pricehas no equivalent in the cache: a room without an allotment appears there as00. - Live before booking: the cache can be one state behind the API;
/v1/bookrejects 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_INACTIVEas 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
idemKeyand endpoint (booking and cancellation count separately):503 ERR_BOOKING_BUSYwithRetry-After: 1, nothing booked or cancelled; the retry with the sameidemKeygoes through
- Scenario
price_drift- Endpoints
/v1/bookwithpriceCheck- Effect
- first attempt per
idemKey:409 ERR_PRICE_DRIFT; the price itself does not change (messagestates 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_LIMITEDwithRetry-After: 1
- Scenario
search_busy- Endpoints
/v1/search- Effect
- first request per test key:
429 ERR_SEARCH_BUSYwithRetry-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.
| Tool | Call | Arguments |
|---|---|---|
list_destinations | GET /v1/destinations | none |
search_hotels | POST /v1/search | request body; pages via cursor |
price_offer | POST /v1/price | request body |
price_all_rooms | POST /v1/prices | request body |
sandbox_book | POST /v1/book | request body, idemKey required |
get_booking | GET /v1/booking | ref |
sandbox_cancel | POST /v1/cancel | request body |
get_limits | GET /v1/limits | none |
open_search | POST /v1/search/open | request body |
open_search_dates | POST /v1/search/open/dates | request body |
hotel_details | GET /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_KEYAntigravity 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.
| Route | Purpose |
|---|---|
GET /v1/content/hotels | Directory 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/catalog | Catalogues 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 is404 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 iscredit; withattributionRequired: truethe 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": "{{*}}"
}pageSize1–1000 (default 500). The next page is fetched withcursor=<nextCursor>; withoutnextCursorthe directory is complete.contentVersioncounts every change to what is delivered for a hotel;0= no content yet (thenupdatedAtis missing).categoryis the official national category,geothe location – both only if maintained.websiteReadyandmissingas in a hotel's content (12.3).feedTokenis the state from which/v1/content/changescontinues. All pages of one directory pass carry the same state (that of the first page).scopeHashchanges 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,feedTokenandnextare opaque and bound to the key: with another key, modified or after the server changed its key422 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
langall texts in all of the tour operator's content languages are returned. Withlang(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 carriesfallbackFromwith 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 inwarnings). htmlcontains onlyp,br,b,strong,i,em,ul,ol,liwithout attributes.machineTranslated: truemarks a machine translation.categories:kindofficial(national category) oroperator(the tour operator's rating),schemestarsorkeys,value1–5 in half steps; "4 Superior" isvalue: 4, superior: true, never 4.5.geo.precision:address,street,localityorunknown.mediain display order,order: 1is the main image. Per imagevariantswith the generated widths (w320tow2048, never wider than the original), each with width, height, bytes andurl;focus(x, y in percent) is the most important image point for your own crops.idis stable as long as the image stays with the hotel.amenities: amenities with a code from the catalogue (12.5).available: falsemeans explicitly not present; a missing amenity is unknown. Depending on the amenity withcount,distanceM,areaM2,ref(airport code for the distance) and, for amenities that can be chargeable,charge(included,extra,unknown).name,destinationandgiataCodecome 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.websiteReadyandmissing: 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.missinglists 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 (newcontentVersion); the operator's console shows the same one. Ignore unknown values inmissing(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": "{{*}}"}sinceis the directory'sfeedTokenornextof the previous response (mandatory, otherwise400 ERR_BAD_REQUEST).pageSize1–1000 (default 500).- Per entry
code,change(upsert= content changed, fetch again;removed= hotel deleted), forupsertthe currentcontentVersion, and optionallyreason(languages= the tour operator's content languages have changed,media_ready= an image has been fully processed,contract=name,destinationorgiataCodehas changed with the contract,deletedforremoved). 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 withnextright 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 itsfeedToken.
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": "{{*}}"
}languagesare the tour operator's content languages,defaultLanguageits default language.langselects the label languages (de,en,tr; withoutlangall).- Per amenity
group,valueType(flag,anzahl,meter,flaeche_m2,meter_mit_bezug),unitandchargeable. A code is never reinterpreted;locked: truemeans discontinued (stays readable).versionchanges with every new catalogue; the catalogue carries anETag.
12.6 Synchronisation for consumers
- Initial load: fetch the directory page by page, remember
feedTokenandscopeHash, fetch the content of each hotel and store theETag. - Ongoing (roughly every few minutes):
changes?since=<token>(on the first callfeedToken, afterwards the last rememberednext), fetch the changed hotels again, deleteremoved, remembernext. IfscopeHashchanges: step 3 right away. - Daily and on
410: fetch the directory again and reconcile it with your own data (delete missing hotels, fetch new ones, comparecontentVersion). - 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
full1/h, newchangeschain 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),
lang1–5 languages, feed 30 days