# Lock a rate before booking

> Locks the rate you chose: the supplier re-checks the price and opens an order, and FX-Port keeps the priced offer for one hour under the partnerOrderId of the answer.

Endpoint: `POST /hotels/pre_book`

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

`POST /api/v1/hotels/pre_book`

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

Book it with `book_hotel`, passing that id as `booking_id`. If the price rose since `get_rates` by more than `price_increase_percent` the call fails with `rate_not_found`: call `get_rates` again. If it rose by less, `priceChanged` is `true` and `data` holds the new price. Calling `pre_book` again with the same rate and the same `price_increase_percent` within a minute returns the same answer (the same `partnerOrderId`, no second order at the supplier); another percentage is a new request. Take 4 to 5 seconds. Always check `formData.orderId`: `null` means the supplier opened no order. In the sandbox a hotel other than a supplier test hotel answers `409 hotel_not_bookable_in_sandbox` with the list of test hotels.

## Request

### Body

Required, JSON.

- `book_hash` (string, required): The `bookHash` of the rate you chose, from `get_rates` (it starts with `h-`). Valid for 30 minutes. The `matchHash` of a rate is not a booking token and is not read as one: a call that sends only a `match_hash` fails with `missing_field`.
- `price_increase_percent` (number, optional): The price rise, in percent, you accept if the supplier raised the price since `get_rates`. `0` means none: the call fails if the price moved; `null` means not set (0). A repeated call within a minute with the same `book_hash` and the same percentage gets the same answer; another percentage is a new request. A number written as text (`"5"`) is read as the number, with a `coerced_value` warning. Between 0 and 100. Default `0`.
- `language` (string, optional): Language of the hotel name in the answer. Default `en`.
- `hash` (string, deprecated, optional): Legacy name: use `book_hash`. The rate token, in its older spelling. Use `book_hash`.
- `bookHash` (string, deprecated, optional): Legacy name: use `book_hash`. The rate token, in its older spelling. Use `book_hash`.

### Example request

```bash
curl https://api.fx-port.com/api/v1/hotels/pre_book \
  -H "Authorization: Bearer fxp_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "book_hash": "h-14574656-7ce9-5a21-b673-8654c5ebd416",
  "price_increase_percent": 0
}'
```

## Response

### 200: The rate is locked. Keep `partnerOrderId`.

- `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 rate was locked.
- `status` (string): Always `ok`. Always `ok`.
- `partnerOrderId` (string, deprecated): Legacy name: use `booking_id`. The id of the booking you are about to make. Send it as `booking_id` to `book_hotel`, then to `GET /bookings/{id}`, `get_voucher` and `cancel_hotel`.
- `booking_id` (string): The id of the booking you are about to make. Send it as `booking_id` to `book_hotel`, then to `GET /bookings/{id}`, `get_voucher` and `cancel_hotel`. The snake_case twin of `partnerOrderId`, with the same value: use this name.
- `partner_order_id` (string): The id of the booking you are about to make. Send it as `booking_id` to `book_hotel`, then to `GET /bookings/{id}`, `get_voucher` and `cancel_hotel`. The snake_case twin of `partnerOrderId`, with the same value: use this name.
- `priceChanged` (boolean, deprecated): Legacy name: use `price_changed`. `true` when the supplier's price moved since `get_rates` (within `price_increase_percent`). `data` holds the new price.
- `price_changed` (boolean): `true` when the supplier's price moved since `get_rates` (within `price_increase_percent`). `data` holds the new price. The snake_case twin of `priceChanged`, with the same value: use this name.
- `data` (object): The locked rate.
  - `hotelId` (string, deprecated): Legacy name: use `hotel_id`. The id of the hotel.
  - `hotel_id` (string): The id of the hotel. The snake_case twin of `hotelId`, with the same value: use this name.
  - `hid` (integer): The supplier's numeric hotel id.
  - `rates` (array of object): The one rate you locked, with a new `bookHash` that starts with `p-`. Do not reuse it.
    - `bookHash` (string, deprecated): Legacy name: use `book_hash`. The token of this rate. Send it as `book_hash` to `pre_book`. It stays valid for 30 minutes.
    - `book_hash` (string): The token of this rate. Send it as `book_hash` to `pre_book`. It stays valid for 30 minutes. The snake_case twin of `bookHash`, with the same value: use this name.
    - `matchHash` (string, deprecated): Legacy name: use `match_hash`. A second identifier of the rate. You do not need it to book: send `bookHash`.
    - `match_hash` (string): A second identifier of the rate. You do not need it to book: send `bookHash`. The snake_case twin of `matchHash`, with the same value: use this name.
    - `room` (object): The room of a rate. Match it with the hotel content by room name; the document of `get_hotel_info` lists every room type with photos and amenities.
      - `name` (string): Name of the room as the supplier sells it, in the `language` of the request.
      - `beddingType` (string, nullable): The bedding as a phrase: `single bed`, `full double bed`, `twin beds` ...
      - `miscRoomType` (string, nullable): Another description of the bed or room, when the supplier gives one (`king size bed`).
      - `capacity` (integer, nullable): Number of guests the room is made for; `null` when unknown.
      - `bathroom` (string, nullable): `private-bathroom` or `shared-bathroom`.
      - `hasPrivateBathroom` (boolean): `true` when the bathroom is private.
      - `balcony` (boolean): `true` when the room has a balcony.
      - `roomClass` (string, nullable): Class of the room: `room`, `suite`, `junior-suite`, `apartment`, `studio`, `cottage`, `dorm` ...
      - `quality` (string, nullable): Quality of the room: `economy`, `standard`, `classic`, `superior`, `deluxe`, `executive`, `business` ...
      - `view` (string, nullable): View of the room (`city-view`, `garden-view` ...); `null` when there is none.
      - `floor` (string, nullable): Floor of the room, when it is special (`attic`).
      - `bedrooms` (integer, nullable): Number of bedrooms; `null` when unknown.
      - `isFamily` (boolean): `true` for a family room.
      - `isClub` (boolean): `true` for a club room.
      - `genderRestriction` (string, nullable): Gender the room is reserved for (hostel dorms); `null` when there is no restriction.
      - `nonSmoking` (boolean): `true` when the room is non-smoking.
    - `meal` (object): The meal plan of a rate.
      - `type` (string): The meal included in the rate. `nomeal` is room only. One of `nomeal`, `breakfast`, `lunch`, `dinner`, `half-board`, `half-board-lunch`, `half-board-dinner`, `full-board`, `all-inclusive`, `ultra-all-inclusive`.
      - `included` (boolean): `true` when a meal is included (`type` is not `nomeal`).
      - `childMealIncluded` (boolean): `true` when the meal of the children is included too.
    - `dailyPrices` (array of number, deprecated): Legacy name: use `daily_prices`. The supplier's price of each night of the stay (first room), in `supplierCurrency`.
    - `daily_prices` (array of number): The supplier's price of each night of the stay (first room), in `supplierCurrency`. The snake_case twin of `dailyPrices`, with the same value: use this name.
    - `allotment` (integer): How many rooms of this type are left at this rate.
    - `isPackage` (boolean): `true` when the rate is part of a package.
    - `anyResidency` (boolean): `false` when the rate is reserved for some residencies: it exists for the `residency` of your request only.
    - `deposit` (object, nullable): Reserved for deposit terms of the rate. `null` today.
    - `noShow` (object, nullable): Reserved for no-show terms of the rate. `null` today.
    - `cancellationPolicy` (object): When and at what price a booking of the rate can be cancelled. The amounts are the supplier's (`originalAmount`, in `originalCurrency`); what is refunded to your agency is in the answer of `cancel_hotel`.
      - `freeCancellationBefore` (string, nullable, deprecated): Legacy name: use `free_cancellation_before`. UTC date and time (no zone suffix) until which the booking can be cancelled for free. `null` means the rate is non-refundable.
      - `free_cancellation_before` (string, nullable): UTC date and time (no zone suffix) until which the booking can be cancelled for free. `null` means the rate is non-refundable. The snake_case twin of `freeCancellationBefore`, with the same value: use this name.
      - `policies` (array of object): The schedule of penalties from now to check-in: the free window first, then the windows with a penalty.
        - `from` (string, nullable): Start of the window, a UTC date and time without zone suffix; `null` for the first window (from now).
        - `until` (string, nullable): End of the window, a UTC date and time without zone suffix; `null` for the last window (to check-in and after).
        - `originalAmount` (number): The penalty charged by the supplier in this window, in `originalCurrency`. `0` in the free window.
        - `originalCurrency` (string): Currency of `originalAmount`, the supplier's: `EUR`.
        - `currency` (string): Your agency currency, as a label. `originalAmount` is NOT in this currency.
    - `pricing` (object): The prices of a rate or of an offer. There is no `b2bPrice` in this block: the amount debited from your agency is `totalPrice - agencyCommission` (see Hotel pricing).
      - `supplierCurrency` (string, deprecated): Legacy name: use `supplier_currency`. Currency the supplier prices in: `EUR`.
      - `supplier_currency` (string): Currency the supplier prices in: `EUR`. The snake_case twin of `supplierCurrency`, with the same value: use this name.
      - `agencyCurrency` (string, deprecated): Legacy name: use `agency_currency`. The currency of your agency. Every amount below except `originalPrice` and `taxes` is in it.
      - `agency_currency` (string): The currency of your agency. Every amount below except `originalPrice` and `taxes` is in it. The snake_case twin of `agencyCurrency`, with the same value: use this name.
      - `originalPrice` (number): The supplier's price for the whole stay and all rooms, in `supplierCurrency`. For information: it is not the base of what you pay.
      - `agencyCommission` (number, deprecated): Legacy name: use `agency_commission`. Your commission, in `agencyCurrency`. The agency price is `totalPrice` minus this amount.
      - `agency_commission` (number): Your commission, in `agencyCurrency`. The agency price is `totalPrice` minus this amount. The snake_case twin of `agencyCommission`, with the same value: use this name.
      - `totalPrice` (number, deprecated): Legacy name: use `total_price`. What the traveler pays: the whole stay, all rooms, in `agencyCurrency`. You are debited `totalPrice - agencyCommission`.
      - `total_price` (number): What the traveler pays: the whole stay, all rooms, in `agencyCurrency`. You are debited `totalPrice - agencyCommission`. The snake_case twin of `totalPrice`, with the same value: use this name.
      - `taxes` (object): Taxes and fees by name (`cityTax`, `cleaningFee`, `electricityFee`, `occupancyTax`, `resortFee`, `serviceFee`, `vat` ...), only the ones the rate has.
    - `exchangeRate` (number, deprecated): Legacy name: use `exchange_rate`. The rate of exchange from `supplierCurrency` to `agencyCurrency` that was used (`285.0` in the sandbox).
    - `exchange_rate` (number): The rate of exchange from `supplierCurrency` to `agencyCurrency` that was used (`285.0` in the sandbox). The snake_case twin of `exchangeRate`, with the same value: use this name.
- `formData` (object): The supplier's booking form for the order that `book_hotel` finishes.
  - `partnerOrderId` (string, nullable, deprecated): Legacy name: use `partner_order_id`. The order id again. `null` when the supplier opened no order.
  - `partner_order_id` (string, nullable): The order id again. `null` when the supplier opened no order. The snake_case twin of `partnerOrderId`, with the same value: use this name.
  - `orderId` (integer, nullable): The supplier's order number. `null` means the supplier opened no order and the hotel cannot be booked.
  - `itemId` (integer, nullable): The supplier's item number inside the order.
  - `paymentTypes` (array of object): The ways to pay the supplier offers: one `deposit` and one `now` option per card currency (about 40 entries).
    - `amount` (string): The amount as a decimal string, in `currencyCode`.
    - `currencyCode` (string): ISO 4217 currency of `amount`.
    - `isNeedCreditCard` (boolean): `true` when the option needs a card. FX-Port never does: it pays with the `deposit` option.
    - `isNeedCvc` (boolean): `true` when the option needs a card security code.
    - `type` (string): `deposit` pays from the supplier deposit of FX-Port (the one FX-Port uses); `now` is a card payment. One of `deposit`, `now`.

```json
{
  "environment": "sandbox",
  "success": true,
  "status": "ok",
  "partnerOrderId": "HMVXX4ZQ3",
  "priceChanged": false,
  "data": {
    "hotelId": "test_hotel_do_not_book",
    "hid": 8473727,
    "rates": [
      {
        "bookHash": "p-58bfbfa0-fd1c-4be9-8a2a-54942cae6627",
        "matchHash": "m-a1f40b22-c086-57c9-b2b5-70c8e59cb27c",
        "room": {
          "name": "Standard room",
          "beddingType": "single bed",
          "miscRoomType": null,
          "capacity": null,
          "bathroom": "private-bathroom",
          "hasPrivateBathroom": true,
          "balcony": false,
          "roomClass": "room",
          "quality": "standard",
          "view": null,
          "floor": null,
          "bedrooms": null,
          "isFamily": false,
          "isClub": false,
          "genderRestriction": null,
          "nonSmoking": true
        },
        "meal": {
          "type": "nomeal",
          "included": false,
          "childMealIncluded": false
        },
        "dailyPrices": [
          1,
          1,
          1
        ],
        "allotment": 77,
        "isPackage": false,
        "anyResidency": true,
        "deposit": null,
        "noShow": null,
        "cancellationPolicy": {
          "freeCancellationBefore": "2026-12-01T21:00:00",
          "policies": [
            {
              "from": null,
              "until": "2026-12-01T21:00:00",
              "originalAmount": 0,
              "originalCurrency": "EUR",
              "currency": "DZD"
            },
            {
              "from": "2026-12-01T21:00:00",
              "until": null,
              "originalAmount": 3,
              "originalCurrency": "EUR",
              "currency": "DZD"
            }
          ]
        },
        "pricing": {
          "supplierCurrency": "EUR",
          "agencyCurrency": "DZD",
          "originalPrice": 3,
          "agencyCommission": 2.97,
          "totalPrice": 1484.97,
          "taxes": {
            "cityTax": {
              "includedInPrice": false,
              "amount": 23.1,
              "currency": "HNL"
            },
            "electricityFee": {
              "includedInPrice": true,
              "amount": 0.6,
              "currency": "EUR"
            },
            "resortFee": {
              "includedInPrice": true,
              "amount": 0.68,
              "currency": "EUR"
            }
          }
        },
        "exchangeRate": 285
      }
    ]
  },
  "formData": {
    "partnerOrderId": "HMVXX4ZQ3",
    "orderId": 757581298,
    "itemId": 232430616,
    "paymentTypes": [
      {
        "amount": "3.00",
        "currencyCode": "EUR",
        "isNeedCreditCard": false,
        "isNeedCvc": false,
        "type": "deposit"
      },
      {
        "amount": "3.00",
        "currencyCode": "USD",
        "isNeedCreditCard": true,
        "isNeedCvc": true,
        "type": "now"
      }
    ]
  }
}
```

## Errors

| Status | Code | Example message |
| --- | --- | --- |
| 400 | `missing_field` | Missing required field: hash |
| 400 | `invalid_rate_hash` | 'book_hash' must be a rate hash taken from get_rates (got 'abc'). |
| 400 | `rate_not_found` | Rate not found |
| 403 | `read_write_permission_required` | This endpoint requires an API key with read_write permission. |
| 409 | `hotel_not_bookable_in_sandbox` | Only the supplier test hotels can be booked in sandbox: test_hotel_do_not_book (any residency) or test_hotel (residency dz, ru or us). |
| 502 | `supplier_invalid_response` | The hotel supplier returned an answer that could not be used. Retry in a few minutes. |
| 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

- [Pre-book a rate](https://fx-port.com/docs/hotels/pre-book.md)
- [Sandbox testing](https://fx-port.com/docs/hotels/sandbox-testing.md)
