Search for Hotels
The search endpoint allows you to find available hotels matching your criteria. It queries multiple providers in parallel and returns normalized results.
Endpoint
POST /connect/hotels/v1/availability
How a search runs
Each connectionCode in settings is queried in parallel. You still get a single response with merged options; use tracing.accessSpans (and optional auditData when debugging) to see per-supplier outcomes.
Request
Request Body Structure
The request body contains criteria and settings:
| Parameter | Type | Description |
|---|---|---|
criteria | object | Search criteria (required) |
settings | object | Request settings (required) |
Search Criteria
| Parameter | Type | Description |
|---|---|---|
checkIn | string | Check-in date and time (ISO 8601, required) |
checkOut | string | Check-out date and time (ISO 8601, required) |
occupancies | array | Room occupancies with paxes (required). One entry per room; each lists all guests with ages. See Multi-room occupancies |
hotels | array | Optional: restrict search to specific hotel codes (often from Content API) |
destination | object | Optional: narrow by destination when supported (e.g. code, or latitude/longitude, or geohash). Exact shape may vary by deployment—confirm against API Reference |
currency | string | Currency code (ISO 4217) |
language | string | Response language code (ISO 639-1) |
nationality | string | Guest nationality code |
exclusions | array | Exclusion filters |
businessRules | object | Business rules for search |
additionalParams | object | Optional flags such as skipMarkup, skipHotelCodesMapping (see below) |
resultFilters | object | Optional post-processing filters that reduce response size: cheapestOnly, refundableFilter, boardCodes, cheapestStrategy. See Result filters |
Request settings
| Parameter | Type | Description |
|---|---|---|
connectionCodes | array | Required. Which configured connections to query in parallel |
timeout | number | Request timeout hint in milliseconds; some suppliers require higher minimums for search/book |
requestId | string | Your correlation id for logs and support |
auditTransactions | boolean | When true, includes supplier-level trace data in auditData (use for debugging; redact in production) |
testMode | boolean | When true, non-production handling where supported |
Example request
{
"criteria": {
"checkIn": "2025-06-15T00:00:00Z",
"checkOut": "2025-06-17T00:00:00Z",
"occupancies": [
{
"paxes": [
{ "name": "John", "surname": "Doe", "age": 35 },
{ "name": "Jane", "surname": "Doe", "age": 33 },
{ "name": "Child", "surname": "Doe", "age": 8 }
]
}
],
"hotels": ["12345", "67890"],
"currency": "EUR",
"language": "en",
"nationality": "US"
},
"settings": {
"connectionCodes": ["testb-hbds-1876", "testb-hbds-1877"],
"timeout": 30000
}
}
Response
Success Response
{
"options": [
{
"optionRefId": "OPT-123456789",
"hotel": {
"code": "12345",
"name": "Example Hotel Barcelona",
"location": {
"city": "Barcelona",
"country": "ES"
}
},
"rooms": [
{
"description": "Standard Double Room",
"boardCode": "BB",
"price": {
"currency": "EUR",
"net": 150.00,
"suggested": 180.00
},
"cancelPolicy": {
"refundable": true,
"cancelPenalties": []
}
}
],
"price": {
"currency": "EUR",
"net": 150.00,
"suggested": 180.00,
"binding": false
}
}
],
"tracing": {
"status": "OK",
"accessSpans": [
{
"access": "testb-hbds-1876",
"status": "OK",
"hotelsRequested": 10,
"hotelsReturned": 8
}
]
},
"warnings": []
}
Key Fields
optionRefId
Unique identifier for this option. Use it to:
- Quote the option (recheck pricing)
- Book the option
- Reference in subsequent operations
Important: optionRefId is only valid for a limited time (typically 4-5 minutes). Always quote immediately before booking.
Destination
You can specify destination in multiple ways:
// By code (IATA, city code, etc.)
{ "destination": { "code": "BCN" } }
// By coordinates
{ "destination": { "latitude": 41.3851, "longitude": 2.1734 } }
// By geohash
{ "destination": { "geohash": "sp3e3" } }
Occupancies
Define guest configuration using paxes array:
{
"criteria": {
"occupancies": [
{
"paxes": [
{ "name": "John", "surname": "Doe", "age": 35 },
{ "name": "Jane", "surname": "Doe", "age": 33 }
]
},
{
"paxes": [
{ "name": "Adult", "surname": "Doe", "age": 40 },
{ "name": "Child1", "surname": "Doe", "age": 8 },
{ "name": "Child2", "surname": "Doe", "age": 10 }
]
}
]
}
}
This example searches for:
- Room 1: 2 adults (ages 35 and 33)
- Room 2: 1 adult (age 40) + 2 children (ages 8 and 10)
Search Criteria Options
Common criteria fields:
{
"criteria": {
"checkIn": "2025-06-15T00:00:00Z",
"checkOut": "2025-06-17T00:00:00Z",
"occupancies": [...],
"hotels": ["12345", "67890"], // Optional: specific hotel codes
"currency": "EUR", // Response currency
"language": "en", // Response language
"nationality": "US", // Guest nationality
"exclusions": [...], // Exclusion filters
"businessRules": {...}, // Business rules
"additionalParams": {...} // Additional parameters (see below)
}
}
Additional Parameters
The additionalParams object allows you to pass extra configuration:
| Parameter | Type | Description |
|---|---|---|
skipMarkup | string | When "true", skip margin calculation. markupGross equals the provider gross. |
skipHotelCodesMapping | string | When "true", hotel codes are sent directly to providers without mapping. |
{
"criteria": {
"checkIn": "2025-06-15T00:00:00Z",
"checkOut": "2025-06-17T00:00:00Z",
"occupancies": [...],
"additionalParams": {
"skipMarkup": "true"
}
}
}
- Your org margin is applied on top of the provider gross (
gross/suggested), never onnet. - If a connection has no markup configured,
markupGrossequals the provider gross and aNO_MARKUP_CONFIGUREDwarning is returned. - Use
skipMarkup: "true"when markup is handled externally or for testing purposes. - The
markupGrossfield contains the final selling price (provider gross + your margin). - Use
marginAmountandmarginPercentto analyze applied margins. - Markup Priority: When multiple markups exist, only the highest priority one is applied (based on
priorityfield, then creation date). - Configure one global markup per connection for consistent pricing.
Result filters
resultFilters is an optional object inside criteria that lets you trim the response after suppliers have replied, before the payload is delivered. Use it to keep only the options your UI will actually render — typical wins are 10×–100× smaller payloads on broad searches with hundreds of hotels.
| Field | Type | Default | Effect |
|---|---|---|---|
cheapestOnly | boolean | false | Keep at most one option per (connectionCode, hotelCode) pair — the cheapest one according to cheapestStrategy. |
refundableFilter | enum | REFUNDABLE_FILTER_UNSPECIFIED | Keep only refundable, only non-refundable, or both (default). |
boardCodes | array of string | [] | Whitelist of board codes (e.g. BB, HB, RO, AI). Empty array = no filter. |
cheapestStrategy | enum | CHEAPEST_STRATEGY_SUGGESTED | Which price to compare on cheapestOnly: SUGGESTED (with markup) or NET. |
General behavior
- All filters are opt-in. If you omit
resultFilters(or send{}), the response is identical to the current behavior. - Filters compose as logical AND: with
refundableFilter=ONLY_REFUNDABLEandboardCodes=["BB"], an option must be both refundable andBBto survive. tracing.accessSpans[].hotelsReturnedis recomputed after filtering to reflect the final response.tracing.statusis not modified — it still reflects supplier outcomes.boardCodesmatching is case-insensitive (bbmatchesBB); leading/trailing whitespace is trimmed; blank entries are ignored.cheapestOnlyties on price are broken deterministically by the lexicographically smalleroptionRefId, so identical inputs yield identical outputs.- When the requested
criteria.currencyis set,cheapestOnlyprefers options in that currency over cheaper options in a different currency.
cheapestOnly
Keep one option per hotel.
{
"criteria": {
"checkIn": "2026-06-15T00:00:00Z",
"checkOut": "2026-06-17T00:00:00Z",
"occupancies": [
{ "paxes": [
{ "name": "John", "surname": "Doe", "age": 35 },
{ "name": "Jane", "surname": "Doe", "age": 33 }
]}
],
"currency": "EUR",
"resultFilters": {
"cheapestOnly": true
}
},
"settings": {
"connectionCodes": ["testb-hbds-1876"]
}
}
If a hotel returns 12 options at €120, €130, €145 …, only the €120 one is kept.
cheapestStrategy
Choose how the cheapest option is selected. Defaults to SUGGESTED (price after markup) — recommended for B2C UIs that show a single customer-facing price.
{
"criteria": {
"resultFilters": {
"cheapestOnly": true,
"cheapestStrategy": "CHEAPEST_STRATEGY_NET"
}
}
}
Use NET when markup configurations vary across hotels and you want the lowest supplier cost regardless of margin.
refundableFilter
Three-state enum on cancelPolicy.refundable.
{
"criteria": {
"resultFilters": {
"refundableFilter": "REFUNDABLE_FILTER_ONLY_REFUNDABLE"
}
}
}
| Value | Behavior |
|---|---|
REFUNDABLE_FILTER_UNSPECIFIED (or omitted) | No filter |
REFUNDABLE_FILTER_ONLY_REFUNDABLE | Keep options where cancelPolicy.refundable = true |
REFUNDABLE_FILTER_ONLY_NON_REFUNDABLE | Keep options where cancelPolicy.refundable = false (or cancelPolicy is missing) |
boardCodes
Case-insensitive whitelist of meal-plan codes.
{
"criteria": {
"resultFilters": {
"boardCodes": ["BB", "HB"]
}
}
}
Common codes: RO (room only), BB (bed & breakfast), HB (half board), FB (full board), AI (all-inclusive). Suppliers may emit additional codes; consult your provider catalog.
Combining filters — full example
A B2C metasearch backend that wants only refundable, breakfast-included rooms, one per hotel, sorted by net cost:
- cURL
- JavaScript
- Python
curl -X POST https://api.bundleport.com/connect/hotels/v1/availability \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"criteria": {
"checkIn": "2026-06-15T00:00:00Z",
"checkOut": "2026-06-17T00:00:00Z",
"occupancies": [
{ "paxes": [
{"name": "John", "surname": "Doe", "age": 35},
{"name": "Jane", "surname": "Doe", "age": 33}
]}
],
"hotels": ["12345", "67890", "11122", "33445"],
"currency": "EUR",
"language": "en",
"nationality": "US",
"resultFilters": {
"cheapestOnly": true,
"cheapestStrategy": "CHEAPEST_STRATEGY_NET",
"refundableFilter": "REFUNDABLE_FILTER_ONLY_REFUNDABLE",
"boardCodes": ["BB"]
}
},
"settings": {
"connectionCodes": ["testb-hbds-1876", "testb-hbds-1877"]
}
}'
const response = await fetch('https://api.bundleport.com/connect/hotels/v1/availability', {
method: 'POST',
headers: {
'Authorization': 'ApiKey YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
criteria: {
checkIn: '2026-06-15T00:00:00Z',
checkOut: '2026-06-17T00:00:00Z',
occupancies: [
{ paxes: [
{ name: 'John', surname: 'Doe', age: 35 },
{ name: 'Jane', surname: 'Doe', age: 33 },
]},
],
hotels: ['12345', '67890', '11122', '33445'],
currency: 'EUR',
resultFilters: {
cheapestOnly: true,
cheapestStrategy: 'CHEAPEST_STRATEGY_NET',
refundableFilter: 'REFUNDABLE_FILTER_ONLY_REFUNDABLE',
boardCodes: ['BB'],
},
},
settings: {
connectionCodes: ['testb-hbds-1876', 'testb-hbds-1877'],
},
}),
});
const data = await response.json();
console.log(`Returned ${data.options.length} options across ${data.tracing.accessSpans.length} suppliers`);
import requests
payload = {
"criteria": {
"checkIn": "2026-06-15T00:00:00Z",
"checkOut": "2026-06-17T00:00:00Z",
"occupancies": [
{"paxes": [
{"name": "John", "surname": "Doe", "age": 35},
{"name": "Jane", "surname": "Doe", "age": 33},
]}
],
"hotels": ["12345", "67890", "11122", "33445"],
"currency": "EUR",
"resultFilters": {
"cheapestOnly": True,
"cheapestStrategy": "CHEAPEST_STRATEGY_NET",
"refundableFilter": "REFUNDABLE_FILTER_ONLY_REFUNDABLE",
"boardCodes": ["BB"],
},
},
"settings": {
"connectionCodes": ["testb-hbds-1876", "testb-hbds-1877"],
},
}
resp = requests.post(
"https://api.bundleport.com/connect/hotels/v1/availability",
headers={"Authorization": "ApiKey YOUR_API_KEY", "Content-Type": "application/json"},
json=payload,
)
data = resp.json()
print(f"Returned {len(data['options'])} options")
Before / after the filter
Without resultFilters, a search across two suppliers might return 320 options for 8 hotels:
{
"options": [ /* 320 entries: many BB+HB+RO rates per hotel, refundable + non-refundable */ ],
"tracing": {
"status": "OK",
"accessSpans": [
{ "access": "testb-hbds-1876", "status": "OK", "hotelsReturned": 8 },
{ "access": "testb-hbds-1877", "status": "OK", "hotelsReturned": 8 }
]
}
}
With cheapestOnly=true, boardCodes=["BB"], refundableFilter=ONLY_REFUNDABLE, the response collapses to one BB-refundable rate per (supplier, hotel) and hotelsReturned is recomputed:
{
"options": [ /* up to 16 entries: 1 per (supplier, hotel) that has a BB-refundable option */ ],
"tracing": {
"status": "OK",
"accessSpans": [
{ "access": "testb-hbds-1876", "status": "OK", "hotelsReturned": 8 },
{ "access": "testb-hbds-1877", "status": "OK", "hotelsReturned": 8 }
]
}
}
resultFilters is a post-processing step that runs after suppliers have already responded with the full set of options. It lowers your payload size and rendering cost, but it does not save supplier latency or quota. To reduce the request scope, narrow your hotels, destination, or connectionCodes first.
Multi-Provider Behavior
When multiple providers are configured:
- Parallel Queries - All providers queried simultaneously
- Deduplication - Same hotel from different providers is consolidated
- Ranking - Business rules determine best options
- Partial Results - If one provider fails, others continue
Partial Failure Example
{
"options": [...], // Results from successful providers
"tracing": {
"status": "PARTIAL",
"accessSpans": [
{ "access": "testb-hbds-1876", "status": "OK", "hotelsReturned": 45 },
{ "access": "testb-hbds-1877", "status": "OK", "hotelsReturned": 32 },
{ "access": "testb-hbds-1878", "status": "ERROR", "errorCode": "TIMEOUT" }
]
},
"warnings": [
{ "code": "PARTIAL_RESPONSE", "description": "testb-hbds-1878 timeout" }
]
}
Best practices (search-focused)
1. Scope your search
optionRefId expires quickly—quoting and booking are covered in Quote and Book. On this page, focus on returning a manageable set of options: use hotels, destination (when available), and date ranges that match your UX.
2. Use appropriate filters
Narrow down results to improve performance:
// ✅ Good - Specific criteria
{
"criteria": {
"checkIn": "2025-06-15T00:00:00Z",
"checkOut": "2025-06-17T00:00:00Z",
"occupancies": [...],
"hotels": ["12345", "67890"], // Specific hotels
"currency": "EUR",
"language": "en"
},
"settings": {
"connectionCodes": ["testb-hbds-1876"]
}
}
// ❌ Bad - Too broad
{
"criteria": {
"checkIn": "2025-06-15T00:00:00Z",
"checkOut": "2025-06-17T00:00:00Z",
"occupancies": [...],
"currency": "EUR"
// No hotel filter = searches all hotels = too many results
}
}
3. Monitor Tracing Data
Use tracing to optimize provider selection:
const result = await searchHotels(criteria);
result.tracing.accessSpans.forEach(span => {
console.log(`${span.access}: ${span.status}, ${span.hotelsReturned} hotels, ${span.processTime}ms`);
if (span.status === 'ERROR') {
// Log provider issues
console.error(`Provider ${span.access} failed: ${span.errorCode}`);
}
});
4. Handle Warnings
Warnings indicate data quality issues but don't prevent success:
if (result.warnings.length > 0) {
result.warnings.forEach(warning => {
if (warning.code === 'UNMAPPED_HOTEL') {
// Hotel exists but content not fully mapped
// Still usable, but may have limited information
}
if (warning.additionalData?.warning_type === 'NO_MARKUP_CONFIGURED') {
// Connection has no markup configured
// markupGross equals the provider gross/suggested
console.log(`No markup for connection: ${warning.connectionCode}`);
}
});
}
Common Warning Types
| Warning Type | Description |
|---|---|
PARTIAL_RESPONSE | One or more providers failed or timed out |
UNMAPPED_HOTEL | Hotel content not fully mapped |
NO_MARKUP_CONFIGURED | Connection has no margin; markupGross = provider gross/suggested |
Code Examples
- cURL
- JavaScript
- Python
- Java
- C#
curl -X POST https://api.bundleport.com/connect/hotels/v1/availability \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"criteria": {
"checkIn": "2025-06-15T00:00:00Z",
"checkOut": "2025-06-17T00:00:00Z",
"occupancies": [
{
"paxes": [
{"name": "John", "surname": "Doe", "age": 35},
{"name": "Jane", "surname": "Doe", "age": 33}
]
}
],
"currency": "EUR",
"language": "en",
"nationality": "US"
},
"settings": {
"connectionCodes": ["testb-hbds-1876"]
}
}'
const response = await fetch('https://api.bundleport.com/connect/hotels/v1/availability', {
method: 'POST',
headers: {
'Authorization': 'ApiKey YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
criteria: {
checkIn: '2025-06-15T00:00:00Z',
checkOut: '2025-06-17T00:00:00Z',
occupancies: [
{
paxes: [
{ name: 'John', surname: 'Doe', age: 35 },
{ name: 'Jane', surname: 'Doe', age: 33 },
],
},
],
currency: 'EUR',
language: 'en',
nationality: 'US',
},
settings: {
connectionCodes: ['testb-hbds-1876'],
},
}),
});
const data = await response.json();
console.log(`Found ${data.options.length} options`);
import requests
url = "https://api.bundleport.com/connect/hotels/v1/availability"
headers = {
"Authorization": "ApiKey YOUR_API_KEY",
"Content-Type": "application/json"
}
payload = {
"criteria": {
"checkIn": "2025-06-15T00:00:00Z",
"checkOut": "2025-06-17T00:00:00Z",
"occupancies": [
{
"paxes": [
{"name": "John", "surname": "Doe", "age": 35},
{"name": "Jane", "surname": "Doe", "age": 33}
]
}
],
"currency": "EUR",
"language": "en",
"nationality": "US"
},
"settings": {
"connectionCodes": ["testb-hbds-1876"]
}
}
response = requests.post(url, json=payload, headers=headers)
data = response.json()
print(f"Found {len(data['options'])} options")
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
var body = """
{
"criteria": {
"checkIn": "2025-06-15T00:00:00Z",
"checkOut": "2025-06-17T00:00:00Z",
"occupancies": [
{ "paxes": [
{"name": "John", "surname": "Doe", "age": 35},
{"name": "Jane", "surname": "Doe", "age": 33}
]}
],
"currency": "EUR",
"language": "en",
"nationality": "US"
},
"settings": { "connectionCodes": ["testb-hbds-1876"] }
}
""";
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.bundleport.com/connect/hotels/v1/availability"))
.header("Authorization", "ApiKey YOUR_API_KEY")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
var client = HttpClient.newHttpClient();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var payload = new
{
criteria = new
{
checkIn = "2025-06-15T00:00:00Z",
checkOut = "2025-06-17T00:00:00Z",
occupancies = new[] {
new { paxes = new[] {
new { name = "John", surname = "Doe", age = 35 },
new { name = "Jane", surname = "Doe", age = 33 }
}}
},
currency = "EUR",
language = "en",
nationality = "US"
},
settings = new { connectionCodes = new[] { "testb-hbds-1876" } }
};
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("ApiKey", "YOUR_API_KEY");
var content = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8,
"application/json");
var response = await client.PostAsync(
"https://api.bundleport.com/connect/hotels/v1/availability",
content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
Next Steps
- Quote an Option - Revalidate pricing before booking
- Create a Booking - Book the selected option
- Content API - Access hotel catalog data