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. Always run it immediately before booking so price and option state match what the supplier expects.

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 - Booking should use the latest prebooking output (optionRefId and prices), not a stale search result

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; book must run on that fresh quote. 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​