Skip to main content

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:

ParameterTypeDescription
criteriaobjectSearch criteria (required)
settingsobjectRequest settings (required)

Search Criteria

ParameterTypeDescription
checkInstringCheck-in date and time (ISO 8601, required)
checkOutstringCheck-out date and time (ISO 8601, required)
occupanciesarrayRoom occupancies with paxes (required). One entry per room; each lists all guests with ages. See Multi-room occupancies
hotelsarrayOptional: restrict search to specific hotel codes (often from Content API)
destinationobjectOptional: narrow by destination when supported (e.g. code, or latitude/longitude, or geohash). Exact shape may vary by deployment—confirm against API Reference
currencystringCurrency code (ISO 4217)
languagestringResponse language code (ISO 639-1)
nationalitystringGuest nationality code
exclusionsarrayExclusion filters
businessRulesobjectBusiness rules for search
additionalParamsobjectOptional flags such as skipMarkup, skipHotelCodesMapping (see below)
resultFiltersobjectOptional post-processing filters that reduce response size: cheapestOnly, refundableFilter, boardCodes, cheapestStrategy. See Result filters

Request settings

ParameterTypeDescription
connectionCodesarrayRequired. Which configured connections to query in parallel
timeoutnumberRequest timeout hint in milliseconds; some suppliers require higher minimums for search/book
requestIdstringYour correlation id for logs and support
auditTransactionsbooleanWhen true, includes supplier-level trace data in auditData (use for debugging; redact in production)
testModebooleanWhen 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:

ParameterTypeDescription
skipMarkupstringWhen "true", skip margin calculation. markupGross equals the provider gross.
skipHotelCodesMappingstringWhen "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"
}
}
}
Markup Behavior
  • Your org margin is applied on top of the provider gross (gross/suggested), never on net.
  • If a connection has no markup configured, markupGross equals the provider gross and a NO_MARKUP_CONFIGURED warning is returned.
  • Use skipMarkup: "true" when markup is handled externally or for testing purposes.
  • The markupGross field contains the final selling price (provider gross + your margin).
  • Use marginAmount and marginPercent to analyze applied margins.
  • Markup Priority: When multiple markups exist, only the highest priority one is applied (based on priority field, 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.

FieldTypeDefaultEffect
cheapestOnlybooleanfalseKeep at most one option per (connectionCode, hotelCode) pair — the cheapest one according to cheapestStrategy.
refundableFilterenumREFUNDABLE_FILTER_UNSPECIFIEDKeep only refundable, only non-refundable, or both (default).
boardCodesarray of string[]Whitelist of board codes (e.g. BB, HB, RO, AI). Empty array = no filter.
cheapestStrategyenumCHEAPEST_STRATEGY_SUGGESTEDWhich 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_REFUNDABLE and boardCodes=["BB"], an option must be both refundable and BB to survive.
  • tracing.accessSpans[].hotelsReturned is recomputed after filtering to reflect the final response. tracing.status is not modified — it still reflects supplier outcomes.
  • boardCodes matching is case-insensitive (bb matches BB); leading/trailing whitespace is trimmed; blank entries are ignored.
  • cheapestOnly ties on price are broken deterministically by the lexicographically smaller optionRefId, so identical inputs yield identical outputs.
  • When the requested criteria.currency is set, cheapestOnly prefers 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"
}
}
}
ValueBehavior
REFUNDABLE_FILTER_UNSPECIFIED (or omitted)No filter
REFUNDABLE_FILTER_ONLY_REFUNDABLEKeep options where cancelPolicy.refundable = true
REFUNDABLE_FILTER_ONLY_NON_REFUNDABLEKeep 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 -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"]
}
}'

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 }
]
}
}
Filter at the source when possible

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:

  1. Parallel Queries - All providers queried simultaneously
  2. Deduplication - Same hotel from different providers is consolidated
  3. Ranking - Business rules determine best options
  4. 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)

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 TypeDescription
PARTIAL_RESPONSEOne or more providers failed or timed out
UNMAPPED_HOTELHotel content not fully mapped
NO_MARKUP_CONFIGUREDConnection has no margin; markupGross = provider gross/suggested

Code Examples

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"]
}
}'

Next Steps