Book a locked rate
Books the offer locked by pre_book and confirms it: there is no hold or pay-later option for API keys, and your agency is debited as part of the booking, when it is confirmed (the sandbox debits nothing).
/api/v1/hotels/book_hotelSend the booking_id (the partnerOrderId of pre_book), the guests of each room and the contact of the booker; the price, the hotel and the dates come from the locked offer, and the pre-booked rate and hotel are always the ones booked: a book_hash or hotel_id in the body is optional and never an error (a value that is not the offer's is ignored, with an ignored_field warning). The answer is pending: the supplier confirms in the background, usually 4 to 14 seconds later. Read GET / every 2 seconds until status is confirmed (or failed), then fetch the voucher. FX-Port sends no webhooks. Call book_hotel once per booking_id: a second call answers 409 duplicate_booking and does not reach the supplier. After a timeout or a 5xx, read the booking before you do anything else. A child must be a guest with is_child true and an age. commission_override sets your commission for this booking. In the sandbox every booking is a real order on the supplier's test contract: cancel it.
curl https://api.fx-port.com/api/v1/hotels/book_hotel \
-H "Authorization: Bearer fxp_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"booking_id": "HMVXX4ZQ3",
"rooms": [
{
"guests": [
{
"first_name": "Jane",
"last_name": "Smith"
},
{
"first_name": "John",
"last_name": "Smith"
}
]
}
],
"user": {
"email": "[email protected]",
"phone": "+15555550123"
},
"send_email": false
}'{
"environment": "sandbox",
"success": true,
"status": "pending",
"bookingId": "HMVXX4ZQ3",
"partnerOrderId": "HMVXX4ZQ3",
"hotelId": "test_hotel_do_not_book",
"checkinDate": "2026-12-01",
"checkoutDate": "2026-12-04",
"pricing": {
"totalPrice": 1484.97,
"agencyCommission": 2.97,
"currency": "DZD",
"supplierCurrency": "EUR",
"originalPrice": 3
},
"postProcessing": {
"bookingDbId": "24fec1e5-a09f-48ec-8050-1adaab0a8de1",
"webhookSent": false,
"bucketLink": null
},
"processingTime": 5.449,
"balanceDebited": false,
"transactionCreated": false,
"payoutCreated": false
}Request
Body
Required, JSON.
booking_idstringrequiredThe id of the booking: the partnerOrderId that pre_book returned. Book it once.
roomsarray of objectrequiredThe guests of each room, in the same order and with the same occupancy as the search: one object per room, one entry per adult and per child.
ShowHide 1 attribute
guestsarray of objectrequiredEvery guest of the room, adults and children, as many as you searched (one object per adult and per child).
ShowHide 4 attributes
first_namestringrequiredGiven name of the guest. Letters of any script, spaces, -, ,, . and ' are kept; digits and other symbols are replaced by a space.
last_namestringrequiredFamily name of the guest, same rules as first_name.
is_childbooleantrue for a child. A child must carry is_child and age, or the supplier refuses the booking with incorrect_guests_number. The numbers 1 and 0, and the texts true, false, yes, no, on, off, 1 and 0, are read as the boolean (a text with a coerced_value warning); null or an empty text means not set (an adult); any other value is 400 invalid_is_child.
ageintegerAge of the child in years at check-in, 0 to 17. Required with is_child. A number written as text ("8") is read as the number, with a coerced_value warning.
Between 0 and 17.
userobjectrequiredThe person who makes the booking.
ShowHide 3 attributes
emailstringrequiredE-mail address of the booker. FX-Port sends its confirmation here (unless send_email is false); the supplier gets an address of FX-Port's own.
phonestringPhone number of the booker, with country code.
commentstringFree text for the hotel, forwarded to the supplier.
languagestringLanguage of the confirmation e-mail and of the voucher.
Default en.
send_emailbooleanfalse to stop FX-Port from sending its confirmation e-mail to user.email. Use it when you test. The numbers 1 and 0, and the texts true, false, yes, no, on, off, 1 and 0, are read as the boolean (a text with a coerced_value warning); null means the default; any other value is 400 invalid_send_email.
Default true.
commission_overrideobjectSets your commission for this booking instead of your default one. The traveler price becomes the price you pay plus this commission; what you are debited does not change.
ShowHide 2 attributes
typestringrequiredfixed: value is your commission per room, in your agency currency. percentage: value is a percentage of the price you pay.
One of fixed, percentage.
valuenumberrequiredThe amount or the percentage, 0 or more. A negative value is ignored.
At least 0.
book_hashstringDo not send it: FX-Port keeps the rate you locked with pre_book under booking_id and always books that rate. A hash the offer was made from (the bookHash of pre_book, the bookHash of get_rates, a match hash) is accepted silently; any other value is ignored, with an ignored_field warning, and the pre-booked rate is booked. It is never an error.
hotel_idstringDo not send it: FX-Port keeps the hotel you locked with pre_book under booking_id and always books that hotel. The same hotel (in another case, its numeric supplier id, the id get_rates was called with) is accepted silently; any other value is ignored, with an ignored_field warning, and the pre-booked hotel is booked. It is never an error.
partner_order_idstringdeprecatedLegacy name, still accepted. Use booking_id instead.
The booking id, in its older spelling. Use booking_id.
partnerOrderIdstringdeprecatedLegacy name, still accepted. Use booking_id instead.
The booking id, in its older spelling. Use booking_id.
bookingIdstringdeprecatedLegacy name, still accepted. Use booking_id instead.
The booking id, in its older spelling. Use booking_id.
bookHashstringdeprecatedLegacy name, still accepted. Use book_hash instead.
The rate token, in its older spelling. Do not send it: see book_hash.
hotelIdstringdeprecatedLegacy name, still accepted. Use hotel_id instead.
The hotel id, in its older spelling. Do not send it: see hotel_id.
Response
200The supplier accepted the booking: it is pending.
environmentstringThe environment of the key that made the call: sandbox for fxp_test_ keys, live for fxp_live_ keys. Present in every JSON body.
One of sandbox, live.
request_idstringThe id of the request, also in the X-Request-Id header.
warningsarray of objectPresent only when the request used a previous spelling of a field or something the endpoint ignored.
ShowHide 5 attributes
codestringWhat the warning is about.
One of deprecated_field, ignored_field, coerced_value, deprecated_value, deprecated_endpoint.
fieldstringThe request field the warning is about.
replacementstringThe name to use instead, for deprecated_* warnings.
messagestringA sentence that says what happened and what to do.
documentation_urlstringWhere the naming rules are explained.
successbooleantrue when the supplier accepted the booking.
statusstringAlways pending: the supplier confirms in the background. Poll the booking until it is confirmed.
Always pending.
bookingIdstringdeprecatedLegacy name, still accepted. Use booking_id instead.
The id of the booking. The same value as partnerOrderId and as the booking_id you sent.
booking_idstringThe id of the booking. The same value as partnerOrderId and as the booking_id you sent. The snake_case twin of bookingId, with the same value: use this name.
partnerOrderIdstringdeprecatedLegacy name, still accepted. Use partner_order_id instead.
The id of the booking, as the supplier knows it.
partner_order_idstringThe id of the booking, as the supplier knows it. The snake_case twin of partnerOrderId, with the same value: use this name.
hotelIdstringdeprecatedLegacy name, still accepted. Use hotel_id instead.
The id of the hotel that was booked.
hotel_idstringThe id of the hotel that was booked. The snake_case twin of hotelId, with the same value: use this name.
checkinDatestringdeprecatedLegacy name, still accepted. Use checkin instead.
Check-in date, YYYY-MM-DD, taken from the locked offer.
checkinstringCheck-in date, YYYY-MM-DD, taken from the locked offer. The snake_case twin of checkinDate, with the same value: use this name.
checkoutDatestringdeprecatedLegacy name, still accepted. Use checkout instead.
Check-out date, YYYY-MM-DD.
checkoutstringCheck-out date, YYYY-MM-DD. The snake_case twin of checkoutDate, with the same value: use this name.
pricingobjectThe price of the booking as it was locked by pre_book.
ShowHide 9 attributes
totalPricenumberdeprecatedLegacy name, still accepted. Use total_price instead.
What the traveler pays, in currency.
total_pricenumberWhat the traveler pays, in currency. The snake_case twin of totalPrice, with the same value: use this name.
agencyCommissionnumberdeprecatedLegacy name, still accepted. Use agency_commission instead.
Your commission, in currency. You are debited totalPrice - agencyCommission.
agency_commissionnumberYour commission, in currency. You are debited totalPrice - agencyCommission. The snake_case twin of agencyCommission, with the same value: use this name.
currencystringdeprecatedLegacy name, still accepted. Use agency_currency instead.
The currency of your agency.
agency_currencystringThe currency of your agency. The snake_case twin of currency, with the same value: use this name.
supplierCurrencystringdeprecatedLegacy name, still accepted. Use supplier_currency instead.
The currency the supplier priced in: EUR.
supplier_currencystringThe currency the supplier priced in: EUR. The snake_case twin of supplierCurrency, with the same value: use this name.
originalPricenumberThe supplier's price in supplierCurrency, for information.
postProcessingobjectInternal progress of the booking. Poll the booking instead of reading it.
ShowHide 4 attributes
bookingDbIdstringThe internal id of the booking row (a UUID). The booking detail calls it id.
webhookSentbooleanDescribes an internal step of FX-Port (the voucher rendering), not a message to you: FX-Port sends no webhooks to customers.
bucketLinkstringnullabledeprecatedLegacy name, still accepted. Use voucher_url instead.
The voucher link. Always null here: the voucher is made after the call; read it with get_voucher.
voucher_urlstringnullableThe voucher link. Always null here: the voucher is made after the call; read it with get_voucher. The snake_case twin of bucketLink, with the same value: use this name.
processingTimenumberdeprecatedLegacy name, still accepted. Use processing_time instead.
Seconds the call took.
processing_timenumberSeconds the call took. The snake_case twin of processingTime, with the same value: use this name.
balanceDebitedbooleandeprecatedLegacy name, still accepted. Use balance_debited instead.
Sandbox only: false, nothing was debited. The key is absent on live.
balance_debitedbooleanSandbox only: false, nothing was debited. The key is absent on live. The snake_case twin of balanceDebited, with the same value: use this name.
transactionCreatedbooleandeprecatedLegacy name, still accepted. Use transaction_created instead.
Sandbox only: false. Absent on live.
transaction_createdbooleanSandbox only: false. Absent on live. The snake_case twin of transactionCreated, with the same value: use this name.
payoutCreatedbooleandeprecatedLegacy name, still accepted. Use payout_created instead.
Sandbox only: false. Absent on live.
payout_createdbooleanSandbox only: false. Absent on live. The snake_case twin of payoutCreated, with the same value: use this name.
Errors
| Status | Code | Example message |
|---|---|---|
| 400 | missing_field | Missing required field: rooms |
| 400 | invalid_rooms | 'rooms' must be a non-empty list: one {guests: [...]} object per room. |
| 400 | invalid_send_email | 'send_email' must be true or false (got 'maybe'). |
| 400 | invalid_is_child | 'rooms[0].guests[1].is_child' must be true or false (got 'maybe'). |
| 400 | invalid_request | 'partner.partner_order_id' differs from 'booking_id': send the booking id once, or the same value twice. |
| 400 | incorrect_guests_number | Incorrect guests number |
| 400 | order_not_found | Order not found |
| 402 | insufficient_balance | The agency balance is lower than the amount to debit. |
| 403 | read_write_permission_required | This endpoint requires an API key with read_write permission. |
| 409 | duplicate_booking | A booking already exists for this partner_order_id: do not call book_hotel again, read its status with GET /api/v1/bookings/{booking_id}?product=hotels. |
| 409 | cached_offer_required | Trusted cached hotel offer not found. Re-run pre_book before booking so the backend can verify the exact debit amount. |
| 500 | post_booking_failed | The booking was accepted but its post-processing failed; read its status with GET /api/v1/bookings/{booking_id}?product=hotels. |
| 502 | supplier_invalid_response | The hotel supplier returned an unreadable answer to the booking request, so the booking may or may not have been made: read its status with GET /api/v1/bookings/{booking_id}?product=hotels before you retry. |
| 503 | balance_lock_timeout | Another booking of this agency is still being processed; retry in a few seconds. |
| 503 | supplier_busy | The supplier is temporarily at capacity; retry after the time given in the Retry-After header. |
The shape of the error body depends on the endpoint. See Errors for every shape and code.