Download OpenAPI specification:
Travel product aggregator for hotels, transfers, and other services.
Downloads: this specification as openapi.yaml, and a
Postman collection made from it — set its apiKey and accessToken
variables, and every /api request carries them.
Initializes a search session and returns a session event. Offers are NOT
returned here — fetch them from POST /api/hotels/results/poll[/{cursor}]
or POST /api/hotels/results/stream[/{cursor}]. Send the same body to those
routes; the server re-derives the session ID from it. The session string
returned here is informational (debug/logging) — clients do not have to pass
it back.
expiresAt marks the end of the result-validity (bookability) window.required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" Guest nationality — ISO 3166-1 alpha-2 |
| currency required | string = 3 characters Preferred currency — ISO 4217, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object Internal/service key-value fields. Any JSON value is accepted. | |
| timeout | integer [ 5 .. 300 ] Default: 30 Maximum search duration in seconds |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-06-21T00:00:00+03:00",
- "checkOut": "2026-06-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}, - {
- "adults": 1,
- "childAges": [ ]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "timeout": 30
}{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990000,
- "handled_by": "hotels.search"
}, - "status": "initialized",
- "data": {
- "event": "session",
- "session": "h2:1:d123:2026-06-21:2026-06-28:IL:2+5,9|1:USD",
- "expiresAt": "2026-06-21T21:45:00Z",
- "status": "initialized",
- "providers": [
- {
- "id": 6192,
- "name": "Hotelbeds IL"
}
], - "rooms": [
- {
- "id": 0,
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}, - {
- "id": 1,
- "adults": 1
}
]
}
}Returns one enveloped JSON batch of offers for the session derived from
the body. The body is identical to the search request (the server
re-derives the session ID from it); read-time filters and streaming
sub-objects may also be supplied. Call again with the cursor from the
prior response while hasMore is true.
The original search-request body (so the server can re-derive the
session ID) plus the read-time filters and streaming blocks.
required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" Guest nationality — ISO 3166-1 alpha-2 |
| currency required | string = 3 characters Preferred currency — ISO 4217, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object Internal/service key-value fields. Any JSON value is accepted. | |
| timeout | integer [ 5 .. 300 ] Default: 30 Maximum search duration in seconds |
object Post-hoc filters applied to already-fetched offers (no effect on what
providers return). Both keys are include booleans, so only an explicit
Unknown filter keys are ignored. For example, Changing the filter requires restarting from the beginning (call
On On | |
object Controls how results are delivered. |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-05-21T00:00:00+03:00",
- "checkOut": "2026-05-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "timeout": 30,
- "filters": {
- "onRequest": false
}, - "streaming": {
- "pushInterval": 1,
- "resultsPerPush": 1000
}
}{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990010,
- "handled_by": "hotels.results.poll"
}, - "status": "working",
- "data": {
- "session": "h2:1:d123:2026-06-21:2026-06-28:IL:2+5,9|1:USD",
- "cursor": "Gk7Pq2mXwz",
- "status": "working",
- "progress": 42,
- "offers": [
- {
- "code": "4Zo386UWxlL3SADqSHZuAV",
- "hotelId": 215134,
- "pricing": {
- "total": {
- "amount": 523.61,
- "currency": "EUR"
}, - "taxes": [
- {
- "type": "VAT",
- "included": true,
- "percent": 10
}, - {
- "type": "City Tax",
- "included": false,
- "amount": 6,
- "currency": "EUR"
}
]
}, - "conditions": {
- "confirmation": "immediate",
- "payment": "pre",
- "packageRate": true
}, - "providerID": 6192,
- "cancellation": {
- "policy": "NR",
- "frames": [
- {
- "from": "2026-06-07T23:59:00+02:00",
- "penalty": 100
}
]
}, - "board": {
- "code": "DB",
- "name": "BED AND BREAKFAST"
}, - "promotions": [
- {
- "name": "Exclusive discount",
- "percent": 9
}, - {
- "name": "3rd night half price"
}
], - "rooms": [
- {
- "name": "Family Room",
- "type": "room",
- "category": "family",
- "bedding": "",
- "id": 0
}, - {
- "name": "Single Room",
- "type": "room",
- "category": "",
- "bedding": "single",
- "id": 1
}
]
}
], - "hasMore": true
}
}Same as POST /api/hotels/results/poll but resumes strictly after
the offer identified by the given cursor token, so polls never return
duplicates and never skip entries across disconnects.
| cursor required | string [ 8 .. 32 ] characters ^[0-9A-Za-z]+$ Example: Gk7Pq2mXwz Opaque cursor token marking the last entry the client already consumed. It is
the obfuscated form of the order-stream entry id (resume-only, and distinct from
an offer's |
The original search-request body (so the server can re-derive the
session ID) plus the read-time filters and streaming blocks.
required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" Guest nationality — ISO 3166-1 alpha-2 |
| currency required | string = 3 characters Preferred currency — ISO 4217, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object Internal/service key-value fields. Any JSON value is accepted. | |
| timeout | integer [ 5 .. 300 ] Default: 30 Maximum search duration in seconds |
object Post-hoc filters applied to already-fetched offers (no effect on what
providers return). Both keys are include booleans, so only an explicit
Unknown filter keys are ignored. For example, Changing the filter requires restarting from the beginning (call
On On | |
object Controls how results are delivered. |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-05-21T00:00:00+03:00",
- "checkOut": "2026-05-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "timeout": 30,
- "filters": {
- "onRequest": false
}, - "streaming": {
- "pushInterval": 1,
- "resultsPerPush": 1000
}
}{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990010,
- "handled_by": "hotels.results.poll"
}, - "status": "working",
- "data": {
- "session": "h2:1:d123:2026-06-21:2026-06-28:IL:2+5,9|1:USD",
- "cursor": "Gk7Pq2mXwz",
- "status": "working",
- "progress": 42,
- "offers": [
- {
- "code": "4Zo386UWxlL3SADqSHZuAV",
- "hotelId": 215134,
- "pricing": {
- "total": {
- "amount": 523.61,
- "currency": "EUR"
}, - "taxes": [
- {
- "type": "VAT",
- "included": true,
- "percent": 10
}, - {
- "type": "City Tax",
- "included": false,
- "amount": 6,
- "currency": "EUR"
}
]
}, - "conditions": {
- "confirmation": "immediate",
- "payment": "pre",
- "packageRate": true
}, - "providerID": 6192,
- "cancellation": {
- "policy": "NR",
- "frames": [
- {
- "from": "2026-06-07T23:59:00+02:00",
- "penalty": 100
}
]
}, - "board": {
- "code": "DB",
- "name": "BED AND BREAKFAST"
}, - "promotions": [
- {
- "name": "Exclusive discount",
- "percent": 9
}, - {
- "name": "3rd night half price"
}
], - "rooms": [
- {
- "name": "Family Room",
- "type": "room",
- "category": "family",
- "bedding": "",
- "id": 0
}, - {
- "name": "Single Room",
- "type": "room",
- "category": "",
- "bedding": "single",
- "id": 1
}
]
}
], - "hasMore": true
}
}Opens a Server-Sent-Events (text/event-stream) stream of session/offers/done
frames. The only deviation from textbook SSE is that this is a POST (so it
carries the auth headers and the search body); the response is otherwise
canonical SSE, and each offers/done frame carries the resume cursor in the
SSE id: line. A batch is flushed whenever streaming.resultsPerPush offers
accumulate or streaming.pushInterval seconds elapse — whichever happens first.
When the search is idle the server emits empty offers frames as keepalives so
dead clients surface within seconds.
The original search-request body (so the server can re-derive the
session ID) plus the read-time filters and streaming blocks.
required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" Guest nationality — ISO 3166-1 alpha-2 |
| currency required | string = 3 characters Preferred currency — ISO 4217, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object Internal/service key-value fields. Any JSON value is accepted. | |
| timeout | integer [ 5 .. 300 ] Default: 30 Maximum search duration in seconds |
object Post-hoc filters applied to already-fetched offers (no effect on what
providers return). Both keys are include booleans, so only an explicit
Unknown filter keys are ignored. For example, Changing the filter requires restarting from the beginning (call
On On | |
object Controls how results are delivered. |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-05-21T00:00:00+03:00",
- "checkOut": "2026-05-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "timeout": 30,
- "filters": {
- "onRequest": false
}, - "streaming": {
- "pushInterval": 1,
- "resultsPerPush": 1000
}
}event: session data: {"event":"session","session":"h2:1:d123:2026-06-21:2026-06-28:IL:2+5,9|1:USD","expiresAt":"2026-06-21T21:45:00Z","status":"working"} id: Gk7Pq2mXwz event: offers data: {"event":"offers","cursor":"Gk7Pq2mXwz","status":"working","progress":42,"offers":[{"code":"4Zo386UWxlL3SADqSHZuAV","hotelId":215134,"pricing":{"total":{"amount":523.61,"currency":"EUR"},"taxes":[{"type":"VAT","included":true,"percent":10},{"type":"City Tax","included":false,"amount":6.00,"currency":"EUR"}]},"conditions":{"confirmation":"immediate","payment":"pre","packageRate":true},"providerID":6192,"cancellation":{"policy":"NR","frames":[{"from":"2026-06-07T23:59:00+02:00","penalty":100}]},"board":{"code":"DB","name":"BED AND BREAKFAST"},"promotions":[{"name":"Exclusive discount","percent":9},{"name":"3rd night half price"}],"rooms":[{"name":"Family Room","type":"room","category":"family","bedding":"","id":0},{"name":"Single Room","type":"room","category":"","bedding":"single","id":1}]}]} id: Lp3Zr8nYtd event: done data: {"event":"done","cursor":"Lp3Zr8nYtd","status":"done","progress":100,"total":2847}
Same as POST /api/hotels/results/stream but resumes strictly after
the offer identified by the given cursor token. Use this when a streaming
connection was interrupted — the client passes back the cursor (the SSE
id: / the cursor field of the last frame it processed) and continues
without dupes or gaps.
| cursor required | string [ 8 .. 32 ] characters ^[0-9A-Za-z]+$ Example: Gk7Pq2mXwz Opaque cursor token marking the last entry the client already consumed. It is
the obfuscated form of the order-stream entry id (resume-only, and distinct from
an offer's |
The original search-request body (so the server can re-derive the
session ID) plus the read-time filters and streaming blocks.
required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" Guest nationality — ISO 3166-1 alpha-2 |
| currency required | string = 3 characters Preferred currency — ISO 4217, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object Internal/service key-value fields. Any JSON value is accepted. | |
| timeout | integer [ 5 .. 300 ] Default: 30 Maximum search duration in seconds |
object Post-hoc filters applied to already-fetched offers (no effect on what
providers return). Both keys are include booleans, so only an explicit
Unknown filter keys are ignored. For example, Changing the filter requires restarting from the beginning (call
On On | |
object Controls how results are delivered. |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-05-21T00:00:00+03:00",
- "checkOut": "2026-05-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "timeout": 30,
- "filters": {
- "onRequest": false
}, - "streaming": {
- "pushInterval": 1,
- "resultsPerPush": 1000
}
}event: session data: {"event":"session","session":"h2:1:d123:2026-06-21:2026-06-28:IL:2+5,9|1:USD","expiresAt":"2026-06-21T21:45:00Z","status":"working"} id: Gk7Pq2mXwz event: offers data: {"event":"offers","cursor":"Gk7Pq2mXwz","status":"working","progress":42,"offers":[{"code":"4Zo386UWxlL3SADqSHZuAV","hotelId":215134,"pricing":{"total":{"amount":523.61,"currency":"EUR"},"taxes":[{"type":"VAT","included":true,"percent":10},{"type":"City Tax","included":false,"amount":6.00,"currency":"EUR"}]},"conditions":{"confirmation":"immediate","payment":"pre","packageRate":true},"providerID":6192,"cancellation":{"policy":"NR","frames":[{"from":"2026-06-07T23:59:00+02:00","penalty":100}]},"board":{"code":"DB","name":"BED AND BREAKFAST"},"promotions":[{"name":"Exclusive discount","percent":9},{"name":"3rd night half price"}],"rooms":[{"name":"Family Room","type":"room","category":"family","bedding":"","id":0},{"name":"Single Room","type":"room","category":"","bedding":"single","id":1}]}]} id: Lp3Zr8nYtd event: done data: {"event":"done","cursor":"Lp3Zr8nYtd","status":"done","progress":100,"total":2847}
Fills in a cancellation policy the search left unknown (policy: "NA") by asking the
owning provider, synchronously. For a provider that already returned the policy during
search — or has no separate service for it — this is a no-op and the stored offer comes
back as it was.
The body is the same search request sent to /search (the session ID is re-derived
from it) and access is bound to the account that created the session. The result is
not written back to the cache, so the stored offer keeps its code and stays exactly
as delivered.
It checks no price: the offer comes back at the price it was delivered at. A price that
moved since the search is reported by /check-availability/{code} and /pre-book/{code}.
| code required | string^[0-9A-Za-z]+$ Example: 4Zo386UWxlL3SADqSHZuAV The offer's |
The original search-request body, so the server can re-derive the session ID the
offer belongs to. Read-time filters/streaming blocks have no meaning here and are
ignored.
required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" Guest nationality — ISO 3166-1 alpha-2 |
| currency required | string = 3 characters Preferred currency — ISO 4217, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object Internal/service key-value fields. Any JSON value is accepted. | |
| timeout | integer [ 5 .. 300 ] Default: 30 Maximum search duration in seconds |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-05-21T00:00:00+03:00",
- "checkOut": "2026-05-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "timeout": 30
}{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990010,
- "handled_by": "hotels.cancellationPolicy"
}, - "status": "done",
- "data": {
- "code": "4Zo386UWxlL3SADqSHZuAV",
- "hotelId": 215134,
- "pricing": {
- "total": {
- "amount": 523.61,
- "currency": "EUR"
}
}, - "conditions": {
- "confirmation": "immediate",
- "payment": "pre",
- "packageRate": false
}, - "providerID": 6192,
- "cancellation": {
- "policy": "PR",
- "frames": [
- {
- "from": "2026-06-07T23:59:00+02:00",
- "penalty": 50
}
]
}, - "board": {
- "code": "RO",
- "name": "ROOM ONLY"
}, - "rooms": [
- {
- "name": "Standard Double",
- "type": "room",
- "category": "standard",
- "bedding": "double"
}
]
}
}Confirms that an offer is still bookable as delivered. Resellers commonly serve search results from their own cache, so a room may already be sold — or repriced by a dynamic rate — by the time the user picks it.
Body and access rules are identical to /cancellation-policy/{code}. Three outcomes:
200 with the offer and no advisories.200 with the current offer under a new code, plus warnings
and/or notices describing each difference (the CH codes of Notice: price and
fee, cancellation policy, board, room name, confirmation, charges at the property,
payment terms). status stays done: a change is not a failure. The superseded
code is removed, so book the code returned here.409 with error H013. The offer is dropped from the session; a repeat
request for that code returns H010.Any other failure — 502 with error H012, or 422 with H017 when the answer cannot
be priced — also drops the offer: one we could not confirm is not bookable, and the
reason does not change that. Start a new /search to obtain a confirmable offer.
The prices compared are your account's (see Pricing), so a pricing rule edited or a rate
moved since the search raises CH001 too, and CH008 for a pay-at-hotel offer's fee.
A provider that does not implement the operation returns the offer as it was — priced afresh like any answer — so the endpoint is always safe to call.
| code required | string^[0-9A-Za-z]+$ Example: 4Zo386UWxlL3SADqSHZuAV The offer's |
The original search-request body, so the server can re-derive the session ID the
offer belongs to. Read-time filters/streaming blocks have no meaning here and are
ignored.
required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" Guest nationality — ISO 3166-1 alpha-2 |
| currency required | string = 3 characters Preferred currency — ISO 4217, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object Internal/service key-value fields. Any JSON value is accepted. | |
| timeout | integer [ 5 .. 300 ] Default: 30 Maximum search duration in seconds |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-05-21T00:00:00+03:00",
- "checkOut": "2026-05-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "timeout": 30
}{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990010,
- "handled_by": "hotels.checkAvailability"
}, - "status": "done",
- "warnings": [
- {
- "code": "CH001",
- "message": "Price has changed",
- "details": "523.61 EUR → 628.33 EUR"
}
], - "notices": [
- {
- "code": "CH003",
- "message": "Board has changed",
- "details": "RO → BB"
}
], - "data": {
- "code": "1Kd902PqRt7ZmXwzAB4cDe",
- "hotelId": 215134,
- "pricing": {
- "total": {
- "amount": 628.33,
- "currency": "EUR"
}
}, - "conditions": {
- "confirmation": "immediate",
- "payment": "pre",
- "packageRate": false
}, - "providerID": 6192,
- "cancellation": {
- "policy": "PR",
- "frames": [
- {
- "from": "2026-06-07T23:59:00+02:00",
- "penalty": 50
}
]
}, - "board": {
- "code": "BB",
- "name": "BED AND BREAKFAST"
}, - "rooms": [
- {
- "name": "Standard Double",
- "type": "room",
- "category": "standard",
- "bedding": "double"
}
]
}
}The pre-book / pre-reservation step, taken immediately before booking. Behaviourally
identical to /check-availability/{code} — same body, same outcomes, same
advisories — and differs only in which provider operation is dispatched; for most
providers both map onto the same upstream call.
Use it as the final confirmation in a booking flow: it revalidates price and conditions, and it lets the provider return a pre-book reference that the booking step will use (kept server-side, never delivered).
Unlike /check-availability/{code} it also delivers pricing.flow — how the price was
built, account by account — and, when your own account's pricing rules could not be
applied to the offer (your account sold at its cost), the PR007 warning.
| code required | string^[0-9A-Za-z]+$ Example: 4Zo386UWxlL3SADqSHZuAV The offer's |
The original search-request body, so the server can re-derive the session ID the
offer belongs to. Read-time filters/streaming blocks have no meaning here and are
ignored.
required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" Guest nationality — ISO 3166-1 alpha-2 |
| currency required | string = 3 characters Preferred currency — ISO 4217, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object Internal/service key-value fields. Any JSON value is accepted. | |
| timeout | integer [ 5 .. 300 ] Default: 30 Maximum search duration in seconds |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-05-21T00:00:00+03:00",
- "checkOut": "2026-05-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": 2,
- "childAges": [
- 5,
- 9
]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "timeout": 30
}{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990010,
- "handled_by": "hotels.checkAvailability"
}, - "status": "done",
- "warnings": [
- {
- "code": "CH001",
- "message": "Price has changed",
- "details": "523.61 EUR → 628.33 EUR"
}
], - "notices": [
- {
- "code": "CH003",
- "message": "Board has changed",
- "details": "RO → BB"
}
], - "data": {
- "code": "1Kd902PqRt7ZmXwzAB4cDe",
- "hotelId": 215134,
- "pricing": {
- "total": {
- "amount": 628.33,
- "currency": "EUR"
}
}, - "conditions": {
- "confirmation": "immediate",
- "payment": "pre",
- "packageRate": false
}, - "providerID": 6192,
- "cancellation": {
- "policy": "PR",
- "frames": [
- {
- "from": "2026-06-07T23:59:00+02:00",
- "penalty": 50
}
]
}, - "board": {
- "code": "BB",
- "name": "BED AND BREAKFAST"
}, - "rooms": [
- {
- "name": "Standard Double",
- "type": "room",
- "category": "standard",
- "bedding": "double"
}
]
}
}Creates a real reservation with the provider that produced this offer.
It revalidates like /pre-book/{code} — same live call, same CH advisories when the
provider's answer differs — and then goes one step further: an offer that reached the
provider is consumed, booked or not, so one code can never produce two reservations
and a repeat request returns H010.
A 200 does not necessarily mean a reservation exists, and it can mean that for two
different reasons.
Some providers book in two upstream steps — they block the rate first, which is when their real price appears, and confirm it second. If the offer changed in between, the booking stops rather than buying something the customer did not agree to, and the reply carries the current offer under a new code instead of a booking.
Separately, a request can reach the provider and its answer be unreadable — a timeout, a
5xx, a truncated body. Whether a reservation was created is then genuinely unknown, and
the reply says so: status: initialized, with no provider.reference. That is not a
failure and not a reservation; it is an attempt, and resolving it is yours (see the
Unresolved example).
So reading a 200 is two steps:
data.status — a booking has one, an offer has no status field at all.data.provider.reference — present if and only if a reservation exists.See the response examples.
Two things differ from the other single-offer routes:
stay.rooms[] is expanded from occupancy counts
into named guests, plus an optional holder, credit card, client reference and free
text for the hotel. The session ID is derived from the occupancy those guests imply,
so a party that does not reproduce the search resolves to a session that does not
exist (H006). Room order matters; child-age order does not. A provider that cannot
represent a non-guest holder rejects one with V038 before the booking lock, hold or
booking request; the offer remains usable. Provider-specific wire restrictions are also
checked there: GoGlobal rejects non-ASCII guest names with V039, a guest last name
not beginning with two letters with V041, and card details with V040, without
contacting upstream. A provider that accepts only specific card networks rejects an
unsupported request card with V045; a missing card required by its payment method is
V046.provider.data is delivered (see the schema).A provider that has no booking implementation answers 501 (H014) and the offer is
kept: nothing was asked upstream, so nothing was learned about it.
Pricing. The searched path is booked while it is still current; only when it is
gone is it replaced, by the cheapest current path to the same provider contract at
Hold's net. The path is settled before Hold and fixed through Book. One pricing view
serves the whole request: rules and rates stay fixed, while each fresh provider net is
recalculated along that path. If a path, rule or rate change since the search changes
your total or fee, it stops the gate (CH001 or CH008, respectively). A gate abort
answers the offer at the held price it compared. The booking's offer and a
gate-abort offer carry pricing.flow, and PR007 when your own account's rules could not
be applied. A held offer that cannot be priced is 422 (H017): nothing is booked yet,
so the hold is released and the offer dropped. After a successful booking there is no
H017: the booked offer is priced from the provider's booking answer (a change from what
you agreed to raises its CH advisory), and only if that answer cannot be priced is the
booking answered with the total and fee the gate accepted — its taxes and every other
term being what the booking confirmed.
A concurrent attempt on the same offer is refused with 409 (H015) rather than
booking twice.
If the response never arrives, a reservation may still exist upstream, and the offer is dropped either way — it cannot be known whether the booking went through, so it must not be bookable again. How that is reported depends on the provider, and a client should handle both: one that distinguishes a delivered-but-unanswered request answers
200withstatus: initializedand noprovider.reference(resolving the attempt is then yours — ask the provider whether the booking exists, and record its real status and locator); one that does not answers502(H012). Thereferenceyou sent is the key either lookup takes, and on theinitializedreply the provider's echo of it comes back inprovider.data.
| code required | string^[0-9A-Za-z]+$ Example: 4Zo386UWxlL3SADqSHZuAV The offer's |
The search-request body with stay.rooms expanded from occupancy counts into named
guests, plus the fields a reservation needs. The occupancy the guests imply must match
the search exactly, or the session cannot be found.
required | object (HotelSearchTarget) Search target. Exactly one of |
required | object |
| guestCountry required | string = 2 characters Enum: "IL" "US" "AE" "TR" "GB" "DE" "FR" |
| currency required | string = 3 characters ISO 4217 code, canonicalized (trimmed, upper-cased, aliases resolved) before anything else reads it. On |
object | |
| timeout | integer [ 5 .. 300 ] Default: 30 |
object (HotelHolder) The person responsible for the booking and the one billing documents are issued to.
They generally need not be a guest. A provider that cannot represent a non-guest holder
rejects that request with | |
| reference | string <= 20 characters ^[A-Za-z0-9._~-]+$ Required. The booking's identifier in your system. It is the key every later
operation names this reservation by — it goes in the path of
It is passed to the provider untouched apart from trimming, and echoed back in
Two constraints follow from what it is used for, and both bind whatever generates it:
at most 20 characters (the tightest provider limit on the field it is forwarded
into), and only RFC 3986 unreserved characters ( |
object (HotelCreditCard) Payment card, needed only by rates whose provider requires card payment. Every field
except The card is never stored, never returned, and never written to an application log — the access log masks it. One gap is known and documented: the outbound provider exchange log still records the request verbatim. | |
| specialRequests | string <= 1024 characters Free text you want passed to the hotel — the classic name for it, and deliberately
not |
{- "search": {
- "destinationId": 123
}, - "stay": {
- "checkIn": "2026-05-21T00:00:00+03:00",
- "checkOut": "2026-05-28T00:00:00+03:00",
- "rooms": [
- {
- "adults": [
- {
- "title": "mr",
- "firstName": "John",
- "lastName": "Doe"
}, - {
- "title": "mrs",
- "firstName": "Jane",
- "lastName": "Doe",
- "countryCode": "IL"
}
], - "children": [
- {
- "title": "mstr",
- "firstName": "Sam",
- "lastName": "Doe",
- "age": 7
}
]
}
]
}, - "guestCountry": "IL",
- "currency": "USD",
- "holder": {
- "firstName": "Ann",
- "lastName": "Smith",
- "email": "ann@example.com",
- "phone": "+972501234567",
- "address": "1 Main St",
- "city": "Tel Aviv",
- "zip": "6100000",
- "country": "IL"
}, - "reference": "AG-99123",
- "specialRequests": "Late arrival, twin beds preferred"
}data.provider.reference is present, so a reservation exists.
{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990010,
- "handled_by": "hotels.book"
}, - "status": "done",
- "warnings": [
- {
- "code": "CH001",
- "message": "Price has changed",
- "details": "523.61 EUR → 534.08 EUR"
}
], - "data": {
- "status": "confirmed",
- "reference": "AG-99123",
- "createdAt": "2026-05-16T00:00:00Z",
- "offer": {
- "code": "4Zo386UWxlL3SADqSHZuAV",
- "hotelId": 215134,
- "pricing": {
- "total": {
- "amount": 534.08,
- "currency": "EUR"
}
}, - "conditions": {
- "confirmation": "immediate",
- "payment": "pre",
- "packageRate": false
}, - "providerID": 6192,
- "cancellation": {
- "policy": "FR",
- "frames": [
- {
- "from": "2026-05-18T23:59:00+02:00",
- "penalty": 100
}
]
}, - "board": {
- "code": "BB",
- "name": "BED AND BREAKFAST"
}, - "rooms": [
- {
- "name": "Double Deluxe",
- "type": "room",
- "category": "deluxe",
- "bedding": "double"
}
]
}, - "holder": {
- "firstName": "Ann",
- "lastName": "Smith",
- "email": "ann@example.com",
- "phone": "+972501234567"
}, - "rooms": [
- {
- "status": "confirmed",
- "reference": "SUP-778",
- "adults": [
- {
- "title": "mr",
- "firstName": "John",
- "lastName": "Doe"
}, - {
- "title": "mrs",
- "firstName": "Jane",
- "lastName": "Doe"
}
]
}
], - "provider": {
- "reference": "1-4075658",
- "relationID": 4471,
- "data": {
- "reference": "1-4075658",
- "clientReference": "AG-99123"
}
}
}
}Cancels a reservation with the provider that made it. Unlike every other hotel operation this one takes no search body: a booking outlives the search session that produced it, and this service does not persist bookings — the caller is the system of record and hands back the provider node returned by /api/hotels/book/{code}.
The reservation is named twice, and the two are different identifiers. The path carries your reference, the one /book required; the body carries the provider's node, including their own locator. Both are needed because this service stores nothing: it cannot translate your reference into a vendor locator, so you supply the locator, and it echoes your reference back so the answer says which reservation it is about.
The operation is idempotent by contract, which is what makes retrying the correct response to a failure whose answer was lost.
| reference required | string <= 20 characters ^[A-Za-z0-9._~-]+$ Example: AG-99123 The booking's It is your reference, not the provider's. The vendor's own locator travels in the body,
inside the Because it appears here in a URL path, it is restricted to RFC 3986 unreserved characters
( |
required | object or object The At least one of Neither is permission to drop the other: where a provider keeps its locator in Hand back what arrived, unchanged. |
{- "provider": {
- "id": 8123,
- "reference": "string",
- "relationID": 4471,
- "data": {
- "localizador": "55664094",
- "localizador_corto": "53531982"
}
}
}{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990010,
- "handled_by": "hotels.cancel"
}, - "status": "done",
- "data": {
- "status": "cancelled",
- "reference": "AG-99123",
- "provider": {
- "reference": "BA284463"
}
}
}Reports what one reservation currently looks like at the provider that made it. It takes the same path parameter and body as /api/hotels/cancel/{reference} and identifies the reservation the same way — a booking outlives the search session that produced it, and this service does not persist bookings, so the caller is the system of record and hands back the provider node returned by /api/hotels/book/{code}.
Unlike every other hotel operation this one only reads. Nothing is consumed by a failure, no lock is taken, and there is no such thing as an ambiguous outcome: a failed status check leaves the reservation exactly as it was, so retrying is always safe.
Two vendor limits are worth knowing. H016 ("no such booking") is raised only where the vendor states absence by name — RateHawk does, both for an unknown identifier and for a booking it reports as never created, while Hotelbeds, Restel and GoGlobal publish no such status — so on those providers an unknown reference comes back as an ordinary H012, because a wrong 404 would send the caller hunting for a typo in a correct reference. A handle for a booking reported as initialized is answerable here for the same reason it is accepted at all: the provider knows from the handle that the attempt was never resolved, and asks the upstream call that can settle it. And a provider may be unable to distinguish a reservation cancelled with fees from a live one, in which case it reports confirmed; this endpoint is therefore not proof that a booking was never cancelled — the caller's own record is.
| reference required | string <= 20 characters ^[A-Za-z0-9._~-]+$ Example: AG-99123 The booking's It is your reference, not the provider's. The vendor's own locator travels in the body,
inside the Because it appears here in a URL path, it is restricted to RFC 3986 unreserved characters
( |
required | object or object The At least one of Neither is permission to drop the other: where a provider keeps its locator in Hand back what arrived, unchanged. |
{- "provider": {
- "id": 8123,
- "reference": "string",
- "relationID": 4471,
- "data": {
- "localizador": "55664094",
- "localizador_corto": "53531982"
}
}
}{- "meta": {
- "request_id": "550e8400-e29b-41d4-a716-446655440000",
- "request_time": 1714990010,
- "handled_by": "hotels.status"
}, - "status": "done",
- "data": {
- "status": "confirmed",
- "reference": "AG-99123",
- "provider": {
- "reference": "LOC12345"
}
}
}Our own geography and hotel content: countries, destinations, hotels, images and timezones. A search returns hotel ids; these endpoints say what those ids mean, so a client can fetch the content once and keep it.
Everything here is a GET with no body. Cacheable successful responses carry an ETag
(plus Last-Modified where the data has a modification time); the change feed and every
error use Cache-Control: no-store.
A few hundred rows that change almost never — the endpoint most worth caching.
{- "status": "done",
- "data": [
- {
- "id": "IT",
- "name": "Italy",
- "continent": "Europe",
- "region": "Southern Europe"
}
]
}All ids in one response — fetch detail from /api/static/destinations/{ids}.
| countryId required | string^[A-Za-z]{2}$ Example: IT ISO 3166-1 alpha-2 code, case-insensitive. |
{- "status": "done",
- "data": [
- 0
]
}Only active destinations are returned, so an id that is unknown or retired is simply absent from the response rather than an error.
| ids required | string^[0-9]+(,[0-9]+)*$ Example: 12,24,51 Comma-separated positive integers. Every operation has an independent configured cap; all currently default to 100. Ids are deduplicated and sorted server-side, so |
{- "status": "done",
- "data": [
- {
- "id": 5252,
- "name": "Rome",
- "type": "city",
- "countryId": "IT",
- "geo": {
- "lat": 41.9028,
- "lon": 12.4964
}, - "searchable": true,
- "seoName": "rome"
}
]
}Grouped by destination. surroundings holds hotels attached to a destination as nearby
rather than inside it.
Every active relationship for the requested destinations is returned in one response.
| ids required | string^[0-9]+(,[0-9]+)*$ Example: 12,24,51 Comma-separated positive integers. Every operation has an independent configured cap; all currently default to 100. Ids are deduplicated and sorted server-side, so |
{- "status": "done",
- "data": [
- {
- "destinationId": 5252,
- "hotelIds": [
- 0
], - "surroundings": [
- 0
]
}
]
}Everything needed to render a property — name, address, stars, description, facilities,
remark — plus mainImage, the cover.
The cover is here so a client painting a list of search results can show a picture per
hotel without fetching the whole gallery. mainImage is absent when no cover is
assigned or the assigned one has been withdrawn; it is never replaced by another picture
from the gallery.
The full gallery lives at /api/static/hotels/{ids}/images.
These records are the only ones carrying a modification time, so this is the only
endpoint that also sends Last-Modified.
| ids required | string^[0-9]+(,[0-9]+)*$ Example: 12,24,51 Comma-separated positive integers. Every operation has an independent configured cap; all currently default to 100. Ids are deduplicated and sorted server-side, so |
{- "status": "done",
- "data": [
- {
- "id": 12,
- "name": "The Republic Hotel",
- "address": "VIA GAETA, 61",
- "zip": "185",
- "phone": "string",
- "fax": "string",
- "stars": 3,
- "geo": {
- "lat": 41.9028,
- "lon": 12.4964
}, - "preferred": true,
- "top": true,
- "seoName": "fiamma-hotel-12",
- "updated": 0,
- "description": "string",
- "facilities": [
- "bar",
- "internet",
- "restaurant"
], - "remark": "string",
- "mainImage": {
- "id": "3218179480102161220",
- "title": "string",
- "width": 1280,
- "height": 853,
- "main": true
}
}
]
}Images in catalogue order, with the cover flagged main: true. The cover is a member of
the gallery, not an alternative to it.
A hotel with no images is omitted from the response rather than returned empty.
Its id cap is configured independently from the other operations and currently defaults to 100. A hotel can carry over a hundred pictures, so this cap can be tightened without reducing the hotel-detail batch size.
| ids required | string^[0-9]+(,[0-9]+)*$ Example: 12,24,51 Comma-separated positive integers. Every operation has an independent configured cap; all currently default to 100. Ids are deduplicated and sorted server-side, so |
{- "status": "done",
- "data": [
- {
- "hotelId": 12,
- "images": [
- {
- "id": "3218179480102161220",
- "title": "string",
- "width": 1280,
- "height": 853,
- "main": true
}
]
}
]
}Refresh a local copy of the catalogue without pulling all of it. active contains
currently active hotels to fetch from /api/static/hotels/{ids}; deactivated contains
every changed hotel whose current status is not 1, so clients can remove or explain it.
The lower bound is inclusive (updated >= timestamp) so second-resolution timestamps
cannot lose an update at the boundary. Safe duplicates are therefore possible. The scan
is bounded by updated <= asOf, which freezes the result while its id-ordered pages are
consumed.
Store the asOf from the response and use it as the next sync's timestamp. It is
the moment the scan started, and it is carried inside the cursor so every page of one
scan reports the same value. A hotel modified while the scan was running has an
updated past that mark, so the next sync picks it up — nothing is lost even though
pages are ordered by id.
This is the one endpoint that is never cached: every distinct timestamp is a distinct
answer. It sends Cache-Control: no-store and no ETag.
The timestamp must be within the last 180 days and must not be in the future. Older clients must bootstrap a fresh catalogue copy.
| timestamp required | integer <int64> >= 0 Example: 1785595091 Unix seconds. Hotels modified at or after this instant are returned. Use the |
| limit | integer >= 1 Example: limit=100 Change-feed page size. Defaults to |
| after | string The |
{- "status": "done",
- "data": {
- "active": [
- 0
], - "deactivated": [
- 0
], - "nextCursor": "string",
- "asOf": 0
}
}Returns one group per distinct timezone, because a list of hotels can legitimately cover
several. Hotels with no resolvable zone — unknown, inactive, or holding no coordinates —
are reported in unresolved rather than dropped.
The answer is an IANA name and needs no dates: the name determines the offset for any instant, summer or winter.
| ids required | string^[0-9]+(,[0-9]+)*$ Example: 12,24,51 Comma-separated positive integers. Every operation has an independent configured cap; all currently default to 100. Ids are deduplicated and sorted server-side, so |
{- "status": "done",
- "data": {
- "groups": [
- {
- "zone": "Europe/Rome",
- "hotelIds": [
- 12,
- 24,
- 51
]
}
]
}
}One group per distinct zone across the destinations asked about; groups carry no
hotelIds, because a destination names no hotel. Destinations that resolve to nothing
are listed in unresolved.
| ids required | string^[0-9]+(,[0-9]+)*$ Example: 12,24,51 Comma-separated positive integers. Every operation has an independent configured cap; all currently default to 100. Ids are deduplicated and sorted server-side, so |
{- "status": "done",
- "data": {
- "groups": [
- {
- "zone": "Europe/Rome",
- "hotelIds": [
- 12,
- 24,
- 51
]
}
]
}
}One group, with no hotelIds. Ocean positions resolve to a nautical Etc/GMT±N zone,
so in practice any valid coordinate has an answer; a point the resolver knows nothing
about returns an empty groups list rather than a default.
| lat required | number <double> [ -90 .. 90 ] Example: 41.9028 |
| lon required | number <double> [ -180 .. 180 ] Example: 12.4964 |
{- "status": "done",
- "data": {
- "groups": [
- {
- "zone": "Europe/Rome",
- "hotelIds": [
- 12,
- 24,
- 51
]
}
]
}
}