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?
- Price Validation - Prices can change between search and booking
- Availability Confirmation - Inventory may have been sold
- Policy Updates - Cancellation policies may have changed
- Expected before book - Booking should use the latest prebooking output (
optionRefIdand prices), not a stale search result
Request
Request Body Structure
The request body contains criteria and settings:
| Parameter | Type | Description |
|---|---|---|
criteria | object | Quote criteria (required) |
settings | object | Request settings (required) |
Quote Criteria
| Parameter | Type | Description |
|---|---|---|
optionRefId | string | Option reference ID from availability search (required) |
additionalData | object | Additional parameters (optional) |
Additional Data Parameters
| Parameter | Type | Description |
|---|---|---|
skipMarkup | string | When "true", skip margin calculation. markupGross equals the provider gross. |
paymentMode | string | Payment 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:
| Value | Behavior |
|---|---|
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_ONLY | The 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):
- Per-request
criteria.additionalData.paymentMode(the value in the request body). - Per-connection map on the aggregator (
AGGREGATOR_PAYMENT_MODE_BY_CONNECTIONenv, JSON map with optional*prefix patterns, e.g.{"testb-gog-*": "DIRECT_ONLY"}). - Cluster-wide default (
AGGREGATOR_PAYMENT_MODE_DEFAULTenv, falls back toMERCHANT).
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.
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:
- Check
warningsforPRICE_CHANGED - Compare
price.netwith your stored value - Present updated price to user
- 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:
- Perform a new search
- Select a new option
- Quote the new option
- 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
- JavaScript
- Python
- Java
- C#
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"]
}
}'
async function quoteAndBook(optionRefId, travellerData) {
// Quote first
const quote = await fetch('https://api.bundleport.com/connect/hotels/v1/prebooking', {
method: 'POST',
headers: {
'Authorization': 'ApiKey YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
criteria: {
optionRefId: optionRefId,
},
settings: {
connectionCodes: ['testb-hbds-1876'],
},
}),
}).then(r => r.json());
// Check for errors
if (quote.errors?.length > 0) {
throw new Error(quote.errors[0].message);
}
// Check for price changes
if (quote.warnings?.some(w => w.code === 'PRICE_CHANGED')) {
console.log('Price changed:', quote.optionQuote.price);
}
// Book with quoted option
return await bookOption(optionRefId, travellerData);
}
import requests
def quote_and_book(option_ref_id, traveller_data):
# Quote first
url = "https://api.bundleport.com/connect/hotels/v1/prebooking"
headers = {
"Authorization": "ApiKey YOUR_API_KEY",
"Content-Type": "application/json"
}
payload = {
"criteria": {
"optionRefId": option_ref_id
},
"settings": {
"connectionCodes": ["testb-hbds-1876"]
}
}
quote = requests.post(url, json=payload, headers=headers).json()
# Check for errors
if quote.get("errors"):
raise Exception(quote["errors"][0]["message"])
# Check for price changes
warnings = quote.get("warnings", [])
if any(w.get("code") == "PRICE_CHANGED" for w in warnings):
print(f"Price changed: {quote['optionQuote']['price']}")
# Book with quoted option
return book_option(option_ref_id, traveller_data)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
var body = """
{
"criteria": { "optionRefId": "OPT-123456789" },
"settings": { "connectionCodes": ["testb-hbds-1876"] }
}
""";
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.bundleport.com/connect/hotels/v1/prebooking"))
.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 { optionRefId = "OPT-123456789" },
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/prebooking",
content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
Next Steps
- Create a Booking - Book the quoted option
- Search for Hotels - Find available options
- Best Practices - Recommended patterns