Skip to main content

Quote an Option

Prebooking (POST /connect/hotels/v1/prebooking) is the quote/recheck step: it validates that a specific option is still available at the advertised price. In guides we call this quote; the HTTP path name is prebooking. Prefer running it immediately before booking so price and option state match what the supplier expects. See When you can skip quote for the Hotelbeds exception.

Endpoint

POST /connect/hotels/v1/prebooking

Why Quote Before Booking?

  1. Price Validation - Prices can change between search and booking
  2. Availability Confirmation - Inventory may have been sold
  3. Policy Updates - Cancellation policies may have changed
  4. Expected before book for most providers - Booking should use the latest prebooking output (optionRefId and prices), not a stale search result

When you can skip quote

Default: keep Search → Quote → Book for every connection.

Hotelbeds (HBDS) only: availability can surface a room/option remark that tells you whether a recheck is mandatory:

SignalMeaningAction
option.remarks entry with type: "RATE_TYPE" and value containing RECHECKRate must be revalidatedCall quote, then book with the quote’s optionRefId
No such remark (treat as BOOKABLE)Supplier allows book from searchYou may book directly with the search optionRefId

Do not skip quote for providers that always require a price check (for example Expedia Rapid), even if no RATE_TYPE remark appears.

tip

Skipping an unnecessary recheck reduces latency and allotment races. Quoting when RECHECK is present is still required for a reliable confirmation.

Request

Request Body Structure

The request body contains criteria and settings:

ParameterTypeDescription
criteriaobjectQuote criteria (required)
settingsobjectRequest settings (required)

Quote Criteria

ParameterTypeDescription
optionRefIdstringOption reference ID from availability search (required)
additionalDataobjectAdditional parameters (optional)

Additional Data Parameters

ParameterTypeDescription
skipMarkupstringWhen "true", skip margin calculation. markupGross equals the provider gross.
paymentModestringPayment policy applied to this quote. "MERCHANT" (default) or "DIRECT_ONLY". See Payment Mode below.

Payment Mode

Some hotel rates require a Virtual Credit Card (VCC) at booking — typically non-refundable or merchant-collected rates. The aggregator surfaces this with optionQuote.acceptVCard = true.

You can control how these rates are returned via criteria.additionalData.paymentMode:

ValueBehavior
MERCHANT (default)All rates are returned. If your booking flow can collect a paymentCard, you can book any rate. Without one, rates with acceptVCard=true are rejected at book with ERR_CODE_MISSING_FIELDS and a clear, structured description.
DIRECT_ONLYThe aggregator rejects rates that require a VCC at quote time. The response contains errors[0].code = ERR_CODE_MISSING_FIELDS and optionQuote is omitted. Use this when your integration does not (yet) capture VCC details.

Resolution order (highest priority first):

  1. Per-request criteria.additionalData.paymentMode (the value in the request body).
  2. Per-connection map on the aggregator (AGGREGATOR_PAYMENT_MODE_BY_CONNECTION env, JSON map with optional * prefix patterns, e.g. {"testb-gog-*": "DIRECT_ONLY"}).
  3. Cluster-wide default (AGGREGATOR_PAYMENT_MODE_DEFAULT env, falls back to MERCHANT).

If you call the API through the agency stack (bundleport-booking-hotel), an optional BOOKING_PAYMENT_MODE env on the agency deployment can force a single mode on every prebook/book it issues. When unset (the default), the agency forwards the call as-is and lets the aggregator resolve the mode per-connection.

QA shortcut

The Connect UI at /connect/api-search/hotels recognizes a paymentMode query parameter (e.g. https://dev-app.bundleport.com/connect/api-search/hotels?paymentMode=DIRECT_ONLY). It is copied verbatim into additionalData.paymentMode for both the quote and the book call of that session, so you can validate the policy without changing the cluster or the agency env. This is debug-only — production traffic should rely on the per-connection or default resolution.

Example Request

{
"criteria": {
"optionRefId": "OPT-123456789",
"additionalData": {
"skipMarkup": "true",
"paymentMode": "DIRECT_ONLY"
}
},
"settings": {
"connectionCodes": ["testb-hbds-1876"],
"requestId": "quote-001"
}
}

Response

Success Response

{
"optionQuote": {
"optionRefId": "OPT-123456789",
"hotel": {
"code": "12345",
"name": "Example Hotel Barcelona"
},
"rooms": [
{
"description": "Standard Double Room",
"boardCode": "BB",
"price": {
"currency": "EUR",
"net": 150.00,
"suggested": 150.00,
"gross": 150.00,
"markupGross": 180.00,
"markupNet": 180.00,
"markupCurrency": "EUR",
"markupBinding": true,
"marginAmount": 30.00,
"marginPercent": 20.0,
"marginType": "PERCENTAGE",
"binding": true
}
}
],
"price": {
"currency": "EUR",
"net": 150.00,
"suggested": 150.00,
"gross": 150.00,
"markupGross": 180.00,
"markupNet": 180.00,
"markupCurrency": "EUR",
"markupBinding": true,
"marginAmount": 30.00,
"marginPercent": 20.0,
"marginType": "PERCENTAGE",
"binding": true
},
"cancelPolicy": {
"refundable": true,
"cancelPenalties": []
},
"warnings": []
},
"errors": []
}

Price Changed Response

If the price has changed:

{
"optionQuote": {
"optionRefId": "OPT-123456789",
"price": {
"currency": "EUR",
"net": 165.00, // Price increased from 150.00
"suggested": 165.00,
"gross": 165.00,
"markupGross": 198.00,
"markupNet": 198.00,
"markupCurrency": "EUR",
"markupBinding": true,
"marginAmount": 33.00,
"marginPercent": 20.0,
"marginType": "PERCENTAGE",
"binding": true
}
},
"warnings": [
{
"code": "PRICE_CHANGED",
"description": "Price has changed since search"
}
]
}

Option No Longer Available

If the option is no longer available:

{
"errors": [
{
"code": "OPTION_EXPIRED",
"message": "Option is no longer available",
"type": "CLIENT"
}
]
}

Key Fields

binding Price

When price.binding is true, the price is guaranteed and will not change before booking. When false, the price may still change.

Price Changes

If the price changes:

  1. Check warnings for PRICE_CHANGED
  2. Compare price.net with your stored value
  3. Present updated price to user
  4. Book with new price if acceptable

No Markup Warning

If a connection has no markup configured, you'll receive a warning:

{
"warnings": [
{
"code": "WARN_CODE_NONE",
"description": "No markup configured for connection; selling price uses provider gross/suggested",
"connectionCode": "testb-hbds-1876",
"additionalData": {
"warning_type": "NO_MARKUP_CONFIGURED",
"connection_code": "testb-hbds-1876"
}
}
]
}

This is informational only—the quote is still valid, but markupGross equals the provider gross (no org margin added).

Option Expiration

If optionRefId has expired:

  1. Perform a new search
  2. Select a new option
  3. Quote the new option
  4. Book immediately

Best Practices

These flows mirror what you should implement in production: quote refreshes price and locks policy context when a recheck is required; book must run on a fresh optionRefId (from quote, or from search when the Hotelbeds BOOKABLE skip applies). The samples show control flow—rename helpers to match your HTTP client layer.

1. Always quote before booking

// ✅ Good - Quote then book
const search = await searchHotels(criteria);
const option = search.options[0];

// Quote immediately
const quote = await quoteOption(option.optionRefId);

// Check for price changes
if (quote.warnings.some(w => w.code === 'PRICE_CHANGED')) {
// Show updated price to user
showPriceUpdate(quote.optionQuote.price);
}

// Book with quoted option
const booking = await bookOption(option.optionRefId, travellerData);

// ❌ Bad - Book without quoting
const search = await searchHotels(criteria);
const option = search.options[0];
// Price may have changed or option may be sold out
const booking = await bookOption(option.optionRefId, travellerData);

2. Handle price changes

If the supplier moves price between search and quote, you may see warnings or different totals. Reconfirm with the traveller before calling book.

const quote = await quoteOption(optionRefId);

// Check for price changes
const priceChanged = quote.warnings.some(w => w.code === 'PRICE_CHANGED');

if (priceChanged) {
const oldPrice = storedOption.price.net;
const newPrice = quote.optionQuote.price.net;

if (newPrice > oldPrice) {
// Ask user to confirm new price
const confirmed = await confirmPriceChange(newPrice);
if (!confirmed) {
// User declined, search again
return searchHotels(criteria);
}
}
}

// Proceed with booking
const booking = await bookOption(optionRefId, travellerData);

3. Check option availability

Treat hard errors (expired option, no availability) as a new search, not a retry of the same book path.

const quote = await quoteOption(optionRefId);

if (quote.errors.length > 0) {
const error = quote.errors[0];

if (error.code === 'OPTION_EXPIRED' || error.code === 'NO_AVAILABILITY') {
// Option no longer available
// Search again for alternatives
return searchHotels(criteria);
}

// Other error - handle appropriately
throw new Error(error.message);
}

Code Examples

curl -X POST https://api.bundleport.com/connect/hotels/v1/prebooking \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"criteria": {
"optionRefId": "OPT-123456789"
},
"settings": {
"connectionCodes": ["testb-hbds-1876"]
}
}'

Next Steps