Skip to main content

Create a Booking

The book endpoint creates a hotel reservation. Prefer quoting the option immediately before booking so price and option state match what the supplier expects. Some Hotelbeds rates allow booking from search when no RECHECK remark is present — see Quote — When you can skip.

Timeouts, idempotency, and recovery

Booking confirmation is slower than search or quote. Use these figures:

GuidanceValue
Platform floor for book90000 ms — anything lower is raised to this floor
Recommended settings.timeout on book≥ 92000 ms, or omit to use the connection ceiling (~2 minutes)
Your HTTP client / socket timeoutA few seconds above settings.timeout

Abandoning a book early does not undo a confirmation the provider may already have accepted. If your client cuts the connection first, you can end up with a reservation and no reference on your side.

If the HTTP client times out, the connection drops, or the error includes recommended_action: verify_booking_then_restart:

  1. Use a unique clientReference for every booking attempt (e.g. UUID). Store it before you send the request.
  2. Reconcile with booking list — call POST /connect/hotels/v1/bookinglist with typeSearch: BOOKING_LIST_CRITERIA_TYPE_REFERENCES and references: [{ "clientReference": "<your id>" }] (see List bookings).
  3. Only retry a full book after you have confirmed there is no booking for that clientReference, and only with a new search/optionRefId when the failure was allotment or “do not retry”. Follow buyer action hints when present.

Send X-Request-ID (or rely on settings.requestId) on every call for end-to-end tracing with support.

Endpoint

POST /connect/hotels/v1/booking

Request

Request Body Structure

The request body contains input and settings:

ParameterTypeDescription
inputobjectBooking input data (required)
settingsobjectRequest settings (required)

Booking Input

ParameterTypeDescription
optionRefIdstringOption reference ID from prebooking quote (required)
holderobjectBooking holder (main guest) information (required)
roomsarrayRoom configurations with guest details (required)
paymentCardobjectPayment card information (if required)
clientReferencestringYour internal booking reference — unique per attempt; required for timeout recovery
remarksarrayFree-text or typed remarks (e.g. late check-in); see Remarks
priceDeltanumberMax allowed price increase as a percentage. Buyer decision — no platform default. Omit or 0 = strict. See priceDelta.
additionalDataobjectAdditional parameters (optional)

priceDelta (price tolerance)

priceDelta is the maximum percentage increase you accept between the last known price (quote or search) and the price at confirmation.

You sendEffect
Omit or 0Strict matching — a one-cent increase rejects the booking. Bundleport does not inject another default.
2Allow up to +2% (aligned with Hotelbeds’ own documented default when you choose that commercial posture).
Any other valueThat percentage; capped at 999.99.

For providers that expose an upstream tolerance tag (Hotelbeds), the value is forwarded as-is. For every provider it also drives Bundleport’s own price check. The cost of any accepted increase is borne by the buyer, not by the provider.

Additional Data Parameters

ParameterTypeDescription
skipMarkupstringWhen "true", skip margin calculation. markupGross equals the provider gross.
paymentModestringPayment policy mirror of the value used at quote. "MERCHANT" (default) or "DIRECT_ONLY". See Payment Mode at quote. The book endpoint does not enforce this field today — the policy is applied at quote/prebooking time, where DIRECT_ONLY rejects rates with acceptVCard=true before they ever reach book. Pass it for audit-log consistency and forward compatibility. If you book a rate that requires a Virtual Credit Card without supplying paymentCard, the upstream provider error is returned verbatim (no aggregator-side translation).

Example Request

{
"input": {
"optionRefId": "OPT-123456789",
"holder": {
"name": "John",
"surname": "Doe",
"title": "MR"
},
"rooms": [
{
"occupancyRefId": 1,
"paxes": [
{
"name": "John",
"surname": "Doe",
"age": 35
},
{
"name": "Jane",
"surname": "Doe",
"age": 33
}
]
}
],
"paymentCard": {
"type": "VI",
"number": "4111111111111111",
"expire": {
"month": 12,
"year": 2027
},
"holder": {
"name": "John",
"surname": "Doe",
"contactInfo": {
"email": "john.doe@example.com",
"phone": {
"countryCode": "+34",
"number": "600123456"
}
}
}
},
"clientReference": "BOOKING-2025-001",
"remarks": [
{
"type": "GENERAL",
"value": "Late check-in requested"
}
]
},
"settings": {
"connectionCodes": ["testb-hbds-1876"],
"requestId": "book-001"
}
}

Multi-room example

Two rooms: first room with two adults and one child (age 8), second room with one adult. Match occupancyRefId to the room index from your search/quote response.

{
"input": {
"optionRefId": "OPT-MULTI-ROOM",
"holder": {
"name": "John",
"surname": "Doe",
"title": "MR"
},
"rooms": [
{
"occupancyRefId": 1,
"paxes": [
{ "name": "John", "surname": "Doe", "age": 35 },
{ "name": "Jane", "surname": "Doe", "age": 33 },
{ "name": "Alex", "surname": "Doe", "age": 8 }
]
},
{
"occupancyRefId": 2,
"paxes": [
{ "name": "Chris", "surname": "Doe", "age": 40 }
]
}
],
"clientReference": "BOOKING-MULTI-001"
},
"settings": {
"connectionCodes": ["testb-hbds-1876"],
"requestId": "book-multi-001"
}
}

Remarks (special requests)

Use remarks to pass non-structural requests to the property (e.g. late arrival, floor preference). Each item typically has:

  • type — e.g. GENERAL or a supplier-specific code when documented for your connection.
  • value — human-readable text.

Suppliers may ignore or map remarks differently; treat them as best-effort, not guaranteed. For critical requirements, confirm with your connectivity team.

Response

Success Response

{
"booking": {
"bookingID": "BK-987654321",
"status": "CONFIRMED",
"clientReference": "BOOKING-2025-001",
"hotel": {
"code": "12345",
"name": "Example Hotel Barcelona"
},
"stay": {
"checkIn": "2025-06-15",
"checkOut": "2025-06-17"
},
"rooms": [
{
"description": "Standard Double Room",
"boardCode": "BB",
"confirmationReference": "ROOM-001",
"paxes": [
{
"name": "John",
"surname": "Doe",
"age": 35
},
{
"name": "Jane",
"surname": "Doe",
"age": 33
}
]
}
],
"holder": {
"name": "John",
"surname": "Doe",
"email": "john.doe@example.com",
"phone": "+34600123456"
},
"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"
},
"cancelPolicy": {
"refundable": true,
"cancelPenalties": []
},
"remarks": ["Late check-in requested"],
"createdAt": "2025-06-01T10:30:00Z",
"updatedAt": "2025-06-01T10:30:00Z"
},
"warnings": [],
"errors": []
}

On Request Response

Some bookings require provider confirmation:

{
"booking": {
"bookingID": "BK-987654321",
"status": "ON_REQUEST",
"reference": {
"bookingID": "BK-987654321"
},
...
},
"warnings": [
{
"code": "ON_REQUEST",
"description": "Booking requires provider confirmation"
}
]
}

ON_REQUEST bookings (pending confirmation)

When status is ON_REQUEST, the supplier has not yet returned a final confirmation. Your application should:

  1. Show a clear pending state to the end user (e.g. “Awaiting hotel confirmation”).
  2. Poll booking detail — call POST /connect/hotels/v1/bookingdetail with bookingID on a backoff schedule (e.g. every 30–120 seconds) until status becomes CONFIRMED, FAILED, or CANCELLED, or until a maximum wait you define in product policy.
  3. Use webhooks when configured — if you enable webhooks, listen for booking.confirmed / booking.failed to update your system without tight polling.
  4. Stop polling after final status or timeout; escalate via support with bookingID and X-Request-ID / requestId if stuck.

There is no single global SLA for how long ON_REQUEST lasts; it depends on the supplier and product type.

Key Fields

Booking Status

StatusDescription
CONFIRMEDBooking confirmed by provider
PENDINGBooking request submitted, awaiting confirmation
ON_REQUESTProvider needs to manually confirm
FAILEDBooking could not be completed

Booking References

Store these references for later operations:

  • bookingID - Bundleport booking ID (use for retrieve, cancel)
  • clientReference - Your internal reference (use for list/reconciliation after timeouts)

Holder Information

The holder is the main guest and booking contact:

  • Required: name, surname (and usually title where you collect it)
  • Contact: put email and phone in contactInfo (not as top-level fields on holder) so the payload matches the API schema
  • Used for: booking confirmation, hotel communication

Guest Information (Paxes)

For each room, provide guest details:

  • Adults: name, surname, age (optional)
  • Children: name, surname, age (required)
  • Type: "ADULT" or "CHILD"

Payment Card

Required for some providers. Include:

  • Type: Card type (VI, MC, AX, etc.)
  • Number: Card number
  • Expire: Expiration month and year
  • Holder: Cardholder name

Best Practices

Operational habits that prevent double bookings, stale prices, and stuck ON_REQUEST states. Pseudocode uses placeholder functions such as quoteOption—wire them to prebooking and booking in your stack.

1. Prefer quote before booking

// ✅ Good - Quote then book (default for most providers)
const quote = await quoteOption(optionRefId);
if (quote.errors?.length > 0) {
throw new Error('Option no longer available');
}
const booking = await bookOption(quote.optionRefId ?? optionRefId, travellerData);

// ⚠️ Only for Hotelbeds BOOKABLE rates (no RATE_TYPE=RECHECK remark)
// const booking = await bookOption(searchOptionRefId, travellerData);

See Quote — When you can skip. Set an explicit priceDelta when your policy allows a limited increase.

2. Store all references

Persist Bundleport bookingID, your clientReference, and supplier references as soon as book succeeds—they are required for support, reconciliation, and cancel.

const booking = await bookOption(optionRefId, travellerData);

// Store all references
await saveBooking({
bundleportId: booking.booking.bookingID,
clientReference: booking.booking.clientReference,
status: booking.booking.status,
});

3. Handle ON_REQUEST bookings

const booking = await bookOption(optionRefId, travellerData);

if (booking.booking.status === 'ON_REQUEST') {
// Inform user that confirmation is pending
await notifyUser({
message: 'Your booking is being confirmed. You will receive an email when confirmed.',
bookingId: booking.booking.bookingID,
});

// Set up webhook or polling to check status
await monitorBookingStatus(booking.booking.bookingID);
}

4. Validate guest information

Mismatch between search occupancies and book payloads is a common 400 source; validate before POST.

function validateGuests(rooms, occupancies) {
for (let i = 0; i < rooms.length; i++) {
const room = rooms[i];
const occupancy = occupancies[i];

// Check total guests match
const totalGuests = room.paxes.length;
const expectedTotal = occupancy.adults + (occupancy.children || 0);

if (totalGuests !== expectedTotal) {
throw new Error(`Room ${i + 1}: Expected ${expectedTotal} guests, got ${totalGuests}`);
}

// Check children ages
const children = room.paxes.filter(p => p.type === 'CHILD' || p.age < 18);
if (children.length !== (occupancy.children || 0)) {
throw new Error(`Room ${i + 1}: Children count mismatch`);
}
}
}

Code Examples

curl -X POST https://api.bundleport.com/connect/hotels/v1/booking \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {
"optionRefId": "OPT-123456789",
"holder": {
"name": "John",
"surname": "Doe",
"title": "MR"
},
"rooms": [
{
"occupancyRefId": 1,
"paxes": [
{ "name": "John", "surname": "Doe", "age": 35 },
{ "name": "Jane", "surname": "Doe", "age": 33 }
]
}
]
},
"settings": {
"connectionCodes": ["testb-hbds-1876"]
}
}'

Next Steps