Skip to content
FX-Port/Docs
GuidesAPI referenceChangelog
Get API keys
  • Introduction
  • Quickstart
  • Security
  • Authentication
  • Environments and Postman
  • Sandbox
  • Conventions and naming
  • Legacy names and twins
  • Errors
  • Rate limits and fair use
  • Streaming search results

Concepts

  • Overview
  • Sandbox testing
  • Mini and full data
  • Guests and rooms
  • Pricing

Search

  • Geo search
  • Locations and coordinates
  • Name search
  • Hotel details

Rates and booking

  • Rates
  • Pre-book
  • Book

After booking

  • Get a booking
  • Voucher
  • Cancel
  • Use these docs with AI

Hotels · Rates and booking

Book a hotel

Book a pre-booked rate with the guest details: the booking confirms at once, with no hold or pay-later. Requires Read+Write permission.

POST/api/v1/hotels/book_hotel

Finish the booking of a rate you locked with pre_book. Booking a hotel 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). The balance is checked before anything is booked (live only: 402 insufficient_balance). You send the booking id (the partnerOrderId of pre_book), the guests of each room and the contact of the booker. The price, the hotel, the dates and the residency come from the offer that pre_book stored, not from this request.

Permission: Read+Write (a read key answers 403 read_write_permission_required)

In the sandbox only the supplier test hotels can be booked

Book test_hotel_do_not_book (every residency) or test_hotel (residency dz, ru or us). For any other hotel pre_book answers 409 hotel_not_bookable_in_sandbox, and a book_hotel that reaches the supplier anyway fails with order_not_found. Every sandbox booking is a real order on the supplier's test contract: cancel it (free until 21:00 UTC on the check-in day). Sandbox testing has a script that books and cancels.

Do not retry book_hotel blindly

The answer is pending: the supplier confirms in the background. If a call times out or fails with a 5xx (except 503 supplier_busy, which is refused before anything is sent), read the booking before you do anything else (how). A second book_hotel for the same booking id answers 409 duplicate_booking and does not reach the supplier.

Request body

POST /api/v1/hotels/book_hotel

FieldTypeRequiredDefaultDescription
booking_idstringYesThe partnerOrderId of pre_book. A booking id is used once.
roomsarrayYesOne object per room, in the order you searched. See Guests and rooms.
rooms[].guestsarrayYesOne { first_name, last_name } per guest of the room, adults and children. A child also carries is_child: true and age. is_child is a boolean: 1 and 0, and texts such as "true", "false", "yes" or "no", are read as one; anything else is 400 invalid_is_child.
userobjectYesThe booker: email (required), phone (with country code) and an optional comment for the hotel. FX-Port sends its confirmation e-mail to email.
languagestringNoenLanguage of the confirmation e-mail and of the voucher.
send_emailbooleanNotruefalse to stop FX-Port from sending its confirmation e-mail. Use it when you test. Read like is_child ("false", "no" and 0 work); anything that is not a boolean is 400 invalid_send_email.
commission_overrideobjectNoYour commission for this booking: see Commission override.

The body does not need the hotel, the rate or the dates: FX-Port keeps them under the booking id and always books the pre-booked rate and hotel. Do not send book_hash or hotel_id. If you do, a hash the offer was made from (the bookHash of pre_book or of get_rates, a match hash) and the offer's own hotel (in any case, its numeric hid, the id get_rates was called with) are accepted silently; any other value is ignored, never refused: the answer carries an ignored_field warning (the pre-booked rate was booked, see Conventions).

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
}'
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": "Anna",
          "last_name": "Smith"
        },
        {
          "first_name": "Ben",
          "last_name": "Smith"
        },
        {
          "first_name": "Cleo",
          "last_name": "Smith",
          "is_child": true,
          "age": 8
        }
      ]
    },
    {
      "guests": [
        {
          "first_name": "Dan",
          "last_name": "Jones"
        }
      ]
    }
  ],
  "user": {
    "email": "[email protected]",
    "phone": "+15555550123",
    "comment": "Late arrival"
  },
  "send_email": false
}'
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"
  },
  "commission_override": {
    "type": "fixed",
    "value": 50
  },
  "send_email": false
}'

Legacy names

These older spellings still work. The answer carries a deprecated_field warning that names the spelling to use (see Legacy names).

Older nameUse instead
partner_order_id, partnerOrderId, bookingIdbooking_id
bookHashbook_hash (do not send it: FX-Port books the pre-booked rate)
hotelIdhotel_id (do not send it: FX-Port books the pre-booked hotel)

Response: the booking is pending

A real answer of the sandbox, in 3 to 5 seconds:

{
  "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
}
FieldDescription
statusAlways pending. The supplier confirms in the background.
bookingIdThe booking id: the same as partnerOrderId and as the booking_id you sent. Use it for the booking detail, the voucher and the cancellation.
hotelId, checkinDate, checkoutDateThe hotel and the dates, from the locked offer.
pricingThe price as locked: totalPrice is what the traveler pays, agencyCommission is yours, and you are debited totalPrice - agencyCommission. There is no b2bPrice here. See Hotel pricing.
postProcessing.bucketLinknull here: the voucher is made after the call. Read it with get_voucher. The other keys of postProcessing are internal.
balanceDebited, transactionCreated, payoutCreatedSandbox only, always false: nothing is debited. They are absent on live.
processingTimeSeconds the call took.

The keys also come as snake_case twins (booking_id, partner_order_id, hotel_id, checkin, checkout, pricing.total_price, pricing.agency_commission, pricing.agency_currency, pricing.supplier_currency, voucher_url, processing_time, balance_debited ...). See Legacy names.

From pending to confirmed

pending becomes confirmed or failed a few seconds later: 4 to 14 seconds in our measurements (7 to 11 seconds in the last runs). There are no webhooks: you poll.

book_hotel → pending → (4 to 14 s) → confirmed → voucher links (at the same moment, or up to 10 s later)
                                   → failed

Call GET /api/v1/bookings/{booking_id}?product=hotels every 2 seconds (the booking changes within seconds: polling faster only uses your rate limits) until status is no longer pending, and stop after about 90 seconds.

async function waitForBooking(bookingId, headers) {
  const started = Date.now();
  while (Date.now() - started < 90_000) {
    const url = `https://api.fx-port.com/api/v1/bookings/${bookingId}?product=hotels`;
    const { data } = await (await fetch(url, { headers })).json();
    if (data.status !== "pending") return data; // "confirmed" or "failed"
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
  throw new Error(`Booking ${bookingId} is still pending: keep polling it, do not book again`);
}
import time
import requests


def wait_for_booking(booking_id, headers, timeout=90):
    started = time.time()
    while time.time() - started < timeout:
        response = requests.get(
            f"https://api.fx-port.com/api/v1/bookings/{booking_id}",
            params={"product": "hotels"},
            headers=headers,
            timeout=30,
        )
        record = response.json()["data"]
        if record["status"] != "pending":
            return record  # "confirmed" or "failed"
        time.sleep(2)
    raise TimeoutError(f"Booking {booking_id} is still pending: keep polling it, do not book again")
StatusMeaning
pendingAccepted. The supplier has not confirmed yet.
confirmedBooked. The voucher links follow within seconds. The booking can be cancelled.
failedThe booking did not go through. Lock a rate again with pre_book (new booking id) and book again.
refunded, partial_refund, cancelledAfter a cancellation: Cancel a hotel booking.

When something goes wrong

The supplier documents that a timeout or a 5xx during a booking does not mean that the booking failed: it may exist. So the rule is: never call book_hotel again with the same booking id to see what happens. Read it.

What you gotWhat to do
200 pendingPoll the booking.
A timeout, a network error, 500 (post_booking_failed, internal_error) or 502 supplier_invalid_responseRead GET /bookings/{booking_id}?product=hotels. pending or confirmed: carry on, the booking exists. 404 booking_not_found: the booking never started, you may call book_hotel once more. failed: pre_book again.
503 supplier_busy or balance_lock_timeoutRefused before anything was sent: nothing was booked at the supplier, no booking exists, nothing is debited. Call book_hotel again with the same booking id after the seconds in Retry-After (a few seconds for balance_lock_timeout, which means that another booking of your agency in the same currency is being processed).
409 duplicate_bookingA booking with this id exists already (errors[0].details has its booking_id and, for your own booking, its status). No second call reached the supplier. Poll it.
409 cached_offer_requiredThe offer of pre_book was not found: it is older than one hour (live), or another agency locked it. pre_book again with your own key, then book the new booking id.
400 invalid_requestThe body names two booking ids: partner.partner_order_id differs from booking_id. Send the booking id once, as booking_id.
400 incorrect_guests_numberA child without is_child and age, or guests that do not match the searched rooms. Fix the list and book again.
400 order_not_foundThe supplier has no order for this booking id: the hotel was not bookable (in the sandbox, not a test hotel) or the order expired. pre_book again.
402 insufficient_balanceLive: top up the balance of your agency, then book again.

Commission override

commission_override sets your commission for this booking instead of the default one of your agency:

FieldDescription
typefixed: value is your commission per room, in your agency currency. percentage: value is a percentage of what you pay (totalPrice - agencyCommission before the override).
value0 or more. A negative value, or a type that is not one of the two, is ignored.

The traveler price becomes what you pay plus the new commission. What you are debited does not change. The answer shows the new pricing.totalPrice and pricing.agencyCommission. Sandbox example: { "type": "fixed", "value": 50 } on a one-room booking costing 1,185.60 DZD gives totalPrice 1,235.60.

Errors

HTTPCodeCause
400missing_fieldA required field is missing: rooms or user.email. errors[0].field names it (Missing required field: rooms), and errors[0].details.missing_fields lists all of them when several are missing.
400invalid_rooms, too_many_rooms, invalid_child_age, invalid_is_child, invalid_send_email, invalid_booking_id, invalid_languageThe body breaks a rule: rooms must be a non-empty list of at most 9 rooms with 1 to 10 guests each, names at most 100 characters, booking_id 1 to 256 characters, is_child and send_email booleans.
400invalid_requestThe body names two booking ids: partner.partner_order_id differs from booking_id. A book_hash or hotel_id that is not the offer's is not an error: it is ignored.
400booking_mode_not_availableThe body has a booking_mode other than standard. A booking made with an API key always books and confirms the rate: do not send the field.
400incorrect_guests_number, order_not_foundSee above. Both are refusals of the supplier: errors[0].type is supplier_error.
402insufficient_balanceLive only.
403read_write_permission_required, agency_not_activeA read key, or an agency that is not approved for this environment.
409duplicate_booking, cached_offer_required, trusted_offer_pricing_missingSee above.
500post_booking_failed, internal_errorRead the booking before you repeat the call.
502supplier_invalid_responseThe supplier's answer to the booking could not be read. Read the booking before you repeat the call.
503supplier_busy, balance_lock_timeoutRefused before anything was sent: retry after the seconds in Retry-After.

The 409 hotel_not_bookable_in_sandbox belongs to pre_book; this is what it carries:

{
  "test_hotels": [
    {
      "id": "test_hotel_do_not_book",
      "hid": 8473727,
      "residency": "any"
    },
    {
      "id": "test_hotel",
      "hid": 8526976,
      "residency": [
        "dz",
        "ru",
        "us"
      ]
    }
  ]
}

Next step

Poll the booking until it is confirmed, then fetch the voucher. To cancel, see Cancel a hotel booking.

Related

Pre-book a rateWhere the booking id comes from.Get a hotel bookingPoll the status, with the price and the voucher links.Get the hotel voucherThe PDF to hand to the traveler.Cancel a hotel bookingFree until 21:00 UTC on the check-in day in the sandbox.API reference: Book a locked rateEvery field and error.
PreviousPre-book a rateNext Get a hotel booking

On this page

  • Request body
  • Legacy names
  • Response: the booking is pending
  • From pending to confirmed
  • When something goes wrong
  • Commission override
  • Errors
  • Next step
  • Related

© 2026 FX-Port

TermsPrivacy[email protected]