# 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).

Endpoint: `POST /hotels/book_hotel`

Source: https://fx-port.com/docs/api/book-hotel

`POST /api/v1/hotels/book_hotel`

Requires an API key with **Read+Write** permission: a Read key is refused with `read_write_permission_required`.

Send 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 /bookings/{booking_id}?product=hotels` 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.

## Request

### Body

Required, JSON.

- `booking_id` (string, required): The id of the booking: the `partnerOrderId` that `pre_book` returned. Book it once.
- `rooms` (array of object, required): The 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.
  - `guests` (array of object, required): Every guest of the room, adults and children, as many as you searched (one object per adult and per child).
    - `first_name` (string, required): Given name of the guest. Letters of any script, spaces, `-`, `,`, `.` and `'` are kept; digits and other symbols are replaced by a space.
    - `last_name` (string, required): Family name of the guest, same rules as `first_name`.
    - `is_child` (boolean, optional): `true` 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`.
    - `age` (integer, optional): Age 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.
- `user` (object, required): The person who makes the booking.
  - `email` (string, required): E-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.
  - `phone` (string, optional): Phone number of the booker, with country code.
  - `comment` (string, optional): Free text for the hotel, forwarded to the supplier.
- `language` (string, optional): Language of the confirmation e-mail and of the voucher. Default `en`.
- `send_email` (boolean, optional): `false` 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_override` (object, optional): Sets 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.
  - `type` (string, required): `fixed`: `value` is your commission per room, in your agency currency. `percentage`: `value` is a percentage of the price you pay. One of `fixed`, `percentage`.
  - `value` (number, required): The amount or the percentage, `0` or more. A negative value is ignored. At least 0.
- `book_hash` (string, optional): Do 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_id` (string, optional): Do 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_id` (string, deprecated, optional): Legacy name: use `booking_id`. The booking id, in its older spelling. Use `booking_id`.
- `partnerOrderId` (string, deprecated, optional): Legacy name: use `booking_id`. The booking id, in its older spelling. Use `booking_id`.
- `bookingId` (string, deprecated, optional): Legacy name: use `booking_id`. The booking id, in its older spelling. Use `booking_id`.
- `bookHash` (string, deprecated, optional): Legacy name: use `book_hash`. The rate token, in its older spelling. Do not send it: see `book_hash`.
- `hotelId` (string, deprecated, optional): Legacy name: use `hotel_id`. The hotel id, in its older spelling. Do not send it: see `hotel_id`.

### Example request

```bash
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": "traveler@example.com",
    "phone": "+15555550123"
  },
  "send_email": false
}'
```

## Response

### 200: The supplier accepted the booking: it is `pending`.

- `environment` (string): The 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_id` (string): The id of the request, also in the `X-Request-Id` header.
- `warnings` (array of object): Present only when the request used a previous spelling of a field or something the endpoint ignored.
  - `code` (string): What the warning is about. One of `deprecated_field`, `ignored_field`, `coerced_value`, `deprecated_value`, `deprecated_endpoint`.
  - `field` (string): The request field the warning is about.
  - `replacement` (string): The name to use instead, for `deprecated_*` warnings.
  - `message` (string): A sentence that says what happened and what to do.
  - `documentation_url` (string): Where the naming rules are explained.
- `success` (boolean): `true` when the supplier accepted the booking.
- `status` (string): Always `pending`: the supplier confirms in the background. Poll the booking until it is `confirmed`. Always `pending`.
- `bookingId` (string, deprecated): Legacy name: use `booking_id`. The id of the booking. The same value as `partnerOrderId` and as the `booking_id` you sent.
- `booking_id` (string): The 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.
- `partnerOrderId` (string, deprecated): Legacy name: use `partner_order_id`. The id of the booking, as the supplier knows it.
- `partner_order_id` (string): The id of the booking, as the supplier knows it. The snake_case twin of `partnerOrderId`, with the same value: use this name.
- `hotelId` (string, deprecated): Legacy name: use `hotel_id`. The id of the hotel that was booked.
- `hotel_id` (string): The id of the hotel that was booked. The snake_case twin of `hotelId`, with the same value: use this name.
- `checkinDate` (string, deprecated): Legacy name: use `checkin`. Check-in date, `YYYY-MM-DD`, taken from the locked offer.
- `checkin` (string): Check-in date, `YYYY-MM-DD`, taken from the locked offer. The snake_case twin of `checkinDate`, with the same value: use this name.
- `checkoutDate` (string, deprecated): Legacy name: use `checkout`. Check-out date, `YYYY-MM-DD`.
- `checkout` (string): Check-out date, `YYYY-MM-DD`. The snake_case twin of `checkoutDate`, with the same value: use this name.
- `pricing` (object): The price of the booking as it was locked by `pre_book`.
  - `totalPrice` (number, deprecated): Legacy name: use `total_price`. What the traveler pays, in `currency`.
  - `total_price` (number): What the traveler pays, in `currency`. The snake_case twin of `totalPrice`, with the same value: use this name.
  - `agencyCommission` (number, deprecated): Legacy name: use `agency_commission`. Your commission, in `currency`. You are debited `totalPrice - agencyCommission`.
  - `agency_commission` (number): Your commission, in `currency`. You are debited `totalPrice - agencyCommission`. The snake_case twin of `agencyCommission`, with the same value: use this name.
  - `currency` (string, deprecated): Legacy name: use `agency_currency`. The currency of your agency.
  - `agency_currency` (string): The currency of your agency. The snake_case twin of `currency`, with the same value: use this name.
  - `supplierCurrency` (string, deprecated): Legacy name: use `supplier_currency`. The currency the supplier priced in: `EUR`.
  - `supplier_currency` (string): The currency the supplier priced in: `EUR`. The snake_case twin of `supplierCurrency`, with the same value: use this name.
  - `originalPrice` (number): The supplier's price in `supplierCurrency`, for information.
- `postProcessing` (object): Internal progress of the booking. Poll the booking instead of reading it.
  - `bookingDbId` (string): The internal id of the booking row (a UUID). The booking detail calls it `id`.
  - `webhookSent` (boolean): Describes an internal step of FX-Port (the voucher rendering), not a message to you: FX-Port sends no webhooks to customers.
  - `bucketLink` (string, nullable, deprecated): Legacy name: use `voucher_url`. The voucher link. Always `null` here: the voucher is made after the call; read it with `get_voucher`.
  - `voucher_url` (string, nullable): The 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.
- `processingTime` (number, deprecated): Legacy name: use `processing_time`. Seconds the call took.
- `processing_time` (number): Seconds the call took. The snake_case twin of `processingTime`, with the same value: use this name.
- `balanceDebited` (boolean, deprecated): Legacy name: use `balance_debited`. Sandbox only: `false`, nothing was debited. The key is absent on live.
- `balance_debited` (boolean): Sandbox only: `false`, nothing was debited. The key is absent on live. The snake_case twin of `balanceDebited`, with the same value: use this name.
- `transactionCreated` (boolean, deprecated): Legacy name: use `transaction_created`. Sandbox only: `false`. Absent on live.
- `transaction_created` (boolean): Sandbox only: `false`. Absent on live. The snake_case twin of `transactionCreated`, with the same value: use this name.
- `payoutCreated` (boolean, deprecated): Legacy name: use `payout_created`. Sandbox only: `false`. Absent on live.
- `payout_created` (boolean): Sandbox only: `false`. Absent on live. The snake_case twin of `payoutCreated`, with the same value: use this name.

```json
{
  "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
}
```

## 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. |

See [Errors](https://fx-port.com/docs/errors.md) for every code and how to react.

## Related guides

- [Book a hotel](https://fx-port.com/docs/hotels/book.md)
- [Guests and rooms](https://fx-port.com/docs/hotels/guests-and-rooms.md)
- [Get a hotel booking](https://fx-port.com/docs/hotels/get-booking.md)
