Hotel Search
Hotel integration takes four calls, all on the v3 API:
- Resolve a destination or hotel —
GET /api/v3/searchor the location catalogs - Search availability —
GET /api/v3/hotels/availability - Get bookable rooms —
GET /api/v3/hotels/{id}/rooms/availability - Book —
POST /api/v3/bookings
A static hotel catalog is also available to sync hotel metadata (names, images, locations) without running an availability search.
If you own your hotels rather than only selling them, Manage Hotels covers creating and editing them.
Legacy v1/v2 endpoints
The v1/v2 hotel endpoints stay live for existing integrations and are documented in the API Reference. New integrations must use v3 — it is the only version receiving new features.
v3 conventions
These rules hold on every v3 endpoint, so your client code needs no special cases:
Authentication: send your API key in the
X-API-Keyheader on every request. See Authentication.IDs: everywhere an ID is accepted (path parameters, filters, booking payloads), both the raw numeric form (
4242) and the encoded form returned by the API are valid, interchangeably.Fixed response schema: every documented response field is always present. An empty collection is
[](never omitted, nevernull); an absent scalar isnull(never omitted). You can parse one stable shape without existence checks.Array query parameters: use bracket notation, either indexed (
hotels[0]=1&hotels[1]=2) or plain (hotels[]=1&hotels[]=2). Nested objects use deep-object encoding:rooms[0][adults]=2.Pagination: list endpoints take
page(1-based) andperPage(default 20, max 1000) and return ametaobject:json{ "currentPage": 1, "lastPage": 8, "perPage": 20, "total": 154 }Errors: error responses are JSON with
name,message, a machine-readablecode, optionaldetails, and arequestId. Quote therequestIdwhen contacting support — it lets us find your exact request in our logs.json{ "name": "BadRequestError", "message": "Invalid countries id 'Morocco': expected a numeric id or an encoded id", "code": "INVALID_LOCATION_ID", "details": { "param": "countries", "value": "Morocco" }, "requestId": "1f6b9c2e-..." }Rate limits: every response carries
X-RateLimit-LimitandX-RateLimit-Remainingheaders. When the limit is exceeded you get429(codeRATE_LIMIT_EXCEEDED) with aRetry-Afterheader in seconds. Search endpoints have their own, stricter bucket. Back off and retry after the indicated delay.
Resolve a destination
Fuzzy search across hotels, locations, experiences and trip templates. Use it to turn free text ("bali beach") into the IDs the availability and catalog filters accept.
Endpoint: GET /api/v3/search
Query parameters:
| Parameter | Required | Description |
|---|---|---|
query | yes | Free-text search terms, minimum 2 characters. |
types | yes | Result types to search: hotel, experience, activity, transfer, program, city, region, country, trip_template. Repeat the parameter (types=hotel&types=city) or pass a comma-separated value. Unknown values return 400 with code INVALID_FILTER_VALUE. |
locale | no | Locale to boost matches in (e.g. fr); falls back to en matches. |
countries | no | Country IDs to filter by; repeat the parameter (countries=1&countries=2). |
organizationId | no | Only return hotels, experiences and trip templates owned by this organization; locations are unaffected. |
standaloneExperiencesOnly | no | Only return experiences bookable on their own. Default false. |
searchDescriptions | no | Also match experience descriptions, highlights and day programs. Default true. |
page, perPage | no | Pagination. |
curl "https://api.koob.tech/api/v3/search?query=bali%20beach&types=hotel,city,region,country" \
-H "X-API-Key: your-api-key-here"Response:
{
"data": [
{
"id": "4242",
"type": "hotel",
"title": "Bali Beach Resort",
"description": "Beachfront resort in Sanur",
"score": 12.4,
"alpha2": "ID"
},
{
"id": "318",
"type": "region",
"title": "Bali",
"score": 9.1,
"alpha2": "ID"
}
],
"meta": { "currentPage": 1, "lastPage": 1, "perPage": 20, "total": 2 }
}Results are ordered by relevance (score). Keep the id: a hotel ID goes into the hotels filter, and a city / region / country ID into the matching cities / regions / countries filter.
Location catalogs
When you need full location lists rather than fuzzy search (for example to build a destination picker), three flat catalogs are available:
GET /api/v3/countriesGET /api/v3/regions— optionalcountriesfilter (repeat the parameter)GET /api/v3/cities— optionalcountriesandregionsfilters
All three take search (accent-insensitive name filter), locale (localized country names, default en), scope=organization (only locations where your organization operates; for a group, where any of its organizations operates), sort (name or id, - prefix for descending; default name) and pagination. The IDs they return are the same location IDs the hotel and experience filters accept.
curl "https://api.koob.tech/api/v3/cities?countries=62&search=ubud" \
-H "X-API-Key: your-api-key-here"Hotel catalog
Static hotel metadata — no prices, no availability. Use it to sync or display hotel content; use the availability search for prices.
Endpoints:
GET /api/v3/hotels— paginated listGET /api/v3/hotels/{id}— one hotel
List query parameters (all optional, combined with AND):
| Parameter | Description |
|---|---|
search | Free-text hotel name filter (fuzzy, accent-insensitive). |
countries, regions, cities | Location IDs (from GET /api/v3/search or the location catalogs). |
hotels | Hotel IDs. |
stars | Exact star-rating filter (1-5). |
kinds, styles | Hotel kind / style IDs. |
sustainability | Sustainability level (exact match): zero, low, medium, strong. |
locale | Locale for localized labels (country, styles, kinds), e.g. fr; defaults to en. Also accepted on GET /api/v3/hotels/{id}. |
sort | name or stars, - prefix = descending (e.g. sort=-stars). Omitted = relevance order, search matches first. |
page, perPage | Pagination. |
curl "https://api.koob.tech/api/v3/hotels?cities[0]=318&stars=5" \
-H "X-API-Key: your-api-key-here"Response (one element of data):
{
"id": "4242",
"name": "Bali Beach Resort",
"address": "Jalan Pantai, Sanur",
"city": { "id": "3411", "name": "Sanur" },
"country": { "id": "62", "name": "Indonesia" },
"lat": "-8.6785",
"lon": "115.2621",
"stars": 5,
"styles": [{ "id": "8", "name": "Beachfront" }],
"kinds": [{ "id": "2", "name": "Resort" }],
"currency": "USD",
"images": [
{
"id": "911",
"url": "https://cdn.koob.tech/hotels/4242/main.jpg",
"name": "Facade"
}
],
"giataId": "123456",
"koediaId": "KD-99812",
"rakutenId": null,
"organizationId": "456",
"organization": { "id": "456", "name": "Bali Horizons DMC" }
}Every key is always present: a value the hotel doesn't have is null (empty collections are []). city, country, styles and kinds are nested references — their id values work directly as cities/countries/styles/kinds filter values. Each image's id is the attachment ID the image upload endpoints accept, so you can update a hotel's images without losing the existing ones. giataId, koediaId and rakutenId are external identifiers you can use to map the hotel to your own or third-party content databases. organization names the owner: a group's list holds every child's hotels, and this field tells them apart.
Availability search
Search hotels with live prices for a stay. Returns one page of hotels, each with a headline price and a lightweight room list. Full bookable rooms (bed choices, cancellation conditions) live on the rooms availability endpoint.
Endpoint: GET /api/v3/hotels/availability
Query parameters:
| Parameter | Required | Description |
|---|---|---|
search | one destination criterion required | Free-text destination (hotel, city, region or country name), resolved server-side. |
cities, regions, countries | ↑ | Location IDs (from GET /api/v3/search with types=city,region,country, or the location catalogs). |
hotels | ↑ | Hotel IDs. |
checkIn | yes | Check-in date, YYYY-MM-DD. |
checkOut | yes | Check-out date, YYYY-MM-DD; must be strictly after checkIn. |
rooms | yes | Room occupancies, deep-object encoded: rooms[0][adults]=2&rooms[0][childrenBirthdates][0]=2019-05-01. One entry per room; for N identical rooms repeat the entry N times. Children are declared by birthdate (YYYY-MM-DD) because suppliers price by age, computed server-side. Optional firstAdultNationality (ISO 3166-1 alpha-2) — some suppliers price by nationality. |
stars | no | Exact star-rating filter (1-5). |
kinds, styles | no | Hotel kind / style IDs. |
sustainability | no | Sustainability level (exact match): zero, low, medium, strong. |
priceMin, priceMax | no | Bounds on the headline price (inclusive); use either or both. |
suppliers | no | Restrict to these suppliers (Koob, Koedia, Rakuten, STAAH; case-insensitive). Omitted = all suppliers. |
tripId | no | Trip ID; derives the pricing organization and currency server-side. |
replaceBookingId | no | Booking being replaced; its allotment is released for this search. |
locale | no | Content language (ISO 639-1, e.g. fr) for room and board texts; unknown or omitted = en. Own-inventory texts fall back to en; bed-bank suppliers are queried in that language. |
sort | no | Comma-separated sort keys among price, promotionsCount, instantAvailability; - prefix = descending (e.g. sort=-price,promotionsCount). Omitted = instantly-bookable hotels first. Any other key returns 400 with code INVALID_SORT. |
page, perPage | no | Pagination. |
The response currency is derived from your organization — it is not a request parameter.
Passing anything other than an ID (for example a country name) in cities / regions / countries returns 400 with code INVALID_LOCATION_ID; the same in hotels / kinds / styles returns 400 with code INVALID_FILTER_VALUE.
curl "https://api.koob.tech/api/v3/hotels/availability?checkIn=2026-11-12&checkOut=2026-11-16&cities[0]=318&rooms[0][adults]=2&rooms[0][childrenBirthdates][0]=2019-05-01" \
-H "X-API-Key: your-api-key-here"Response (one element of data):
{
"id": "4242",
"name": "Bali Beach Resort",
"price": 640.0,
"currency": "EUR",
"promotions": [
{
"id": "promo-77",
"name": "Early Bird",
"numberOfNights": 4,
"minNumberOfRooms": 1
}
],
"instantAvailability": true,
"minimumStay": 1,
"rooms": [
{
"id": "812",
"name": "Deluxe Garden View",
"description": "32m² room with balcony",
"price": 640.0,
"minimumStay": 1,
"tags": ["Breakfast included", "Refundable"],
"mealPlans": [
{
"code": "breakfast",
"label": "Breakfast included",
"startAt": null,
"endAt": null
}
],
"instantAvailability": true,
"supplier": {
"name": "Koob",
"code": "Koob",
"flux": null
}
}
]
}Field notes:
priceis the headline (lowest) total price for the whole stay and party;nullwhen unpriced. All prices are rounded to 2 decimals.instantAvailability:truemeans instantly bookable;falsemeans on-request (the booking needs supplier confirmation).roomsis a lightweight preview. To book, call the rooms availability endpoint — it returns the bookable bed choices and the booking handshake.mealPlansis the structured board (room_only,breakfast,lunch,dinner,half_board,full_board,all_inclusive). Prefer it over parsingtags; the full semantics — nullablecode, validity windows, empty-array meaning — are on Meal plans.supplieridentifies the sales channel.name/codeis the channel (Koob,Koedia,Rakuten,STAAH);fluxidentifies the bed bank, chain or inventory behind a Koedia rate (with its ownname,code,logo) and isnullfor suppliers that sell direct.