# Pre-book a rate

> Lock in the price of a chosen rate and get a partner_order_id and the booking form ahead of book_hotel. Requires Read+Write permission.

Endpoint: `POST /api/v1/hotels/pre_book`

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

Lock the price of the rate the user chose. `pre_book` asks the supplier to confirm the rate and the price, opens an order at the supplier, and keeps the priced offer on FX-Port's side. It answers a `partnerOrderId`: the **booking id** you send to [`book_hotel`](https://fx-port.com/docs/hotels/book.md) next.

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


> [!CAUTION]
> **In the sandbox only the supplier test hotels can be booked**
>
> Pre-book `test_hotel_do_not_book` (every residency) or `test_hotel` (residency `dz`, `ru` or `us`). Any other hotel **answers `409 hotel_not_bookable_in_sandbox`** and lists the test hotels in `errors[0].details.test_hotels`: the supplier opens no order for it, `formData.orderId` is `null`, and `book_hotel` would fail with `order_not_found`. Searching and pricing work for every hotel. See [Sandbox testing](https://fx-port.com/docs/hotels/sandbox-testing.md).

> [!NOTE]
> `pre_book` stores the **priced offer** (with your commission) on FX-Port's side, under the `partnerOrderId`. `book_hotel` reads the price, the hotel, the dates and the residency from that offer, not from your request, so the amount debited is the one you were shown here. Call `pre_book` right before `book_hotel`.


## Request body

`POST /api/v1/hotels/pre_book`

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `book_hash` | string | Yes | | The `bookHash` of the rate, from [`get_rates`](https://fx-port.com/docs/hotels/rates.md). It starts with `h-` and is valid for **30 minutes**. |
| `price_increase_percent` | number | No | `0` | The price rise, in percent, you accept if the supplier raised the price since `get_rates`: 0 to 100. With `0` the call fails if the price moved. `null` means not set (0); a number written as text (`"5"`) is read, with a `coerced_value` warning. |
| `language` | string | No | `en` | Language of the hotel name in the answer. |

The `matchHash` of a rate is **not** a booking token: `pre_book` does not read it, and a call that sends only a `match_hash` fails with `400 missing_field`. Send the `bookHash` as `book_hash`.


**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
}'
```


### Legacy names

These older spellings still work. The answer carries a `deprecated_field` warning that names the spelling to use (see [Legacy names](https://fx-port.com/docs/legacy-names.md)).

| Older name | Use instead |
|---|---|
| `hash`, `bookHash` | `book_hash` |

## Response

A real answer of the sandbox, in 4 to 5 seconds (the supplier does the work):

~~~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"
      }
    ]
  }
}
~~~

| Field | Description |
|---|---|
| `partnerOrderId` | **The booking id.** Send it as `booking_id` to [`book_hotel`](https://fx-port.com/docs/hotels/book.md), and later to the booking detail, [`get_voucher`](https://fx-port.com/docs/hotels/voucher.md) and [`cancel_hotel`](https://fx-port.com/docs/hotels/cancel.md). It comes twice more as `booking_id` and `partner_order_id`. |
| `priceChanged` | `true` when the supplier's price moved since `get_rates`, within `price_increase_percent`. |
| `data.rates[0]` | The rate you locked, with the **price as it stands now** (`pricing.totalPrice`). Its `bookHash` is a new one that starts with `p-`: do not reuse it, `book_hotel` needs only the booking id. |
| `formData.orderId`, `itemId` | The order opened at the supplier. **`orderId: null` means the supplier opened no order**: the hotel cannot be booked, do not call `book_hotel`. |
| `formData.paymentTypes` | The ways the supplier offers to pay: one `deposit` and about 40 card options, one per currency. Informational: FX-Port always pays with `deposit`, from your agency balance. |

## Check the price

When `priceChanged` is `true`, or whenever you want to be sure, compare `data.rates[0].pricing.totalPrice` with the price the traveler accepted. If it is higher, show the new price and let the traveler decide before you call `book_hotel`. With `price_increase_percent` at `0` the call fails instead of locking a higher price (see below).

## What lives how long

| What | How long |
|---|---|
| The `bookHash` from `get_rates` | 30 minutes |
| The priced offer behind `partnerOrderId` | 1 hour: after that `book_hotel` answers `409 cached_offer_required` on live and you must `pre_book` again |
| The order at the supplier | 60 minutes; an order you never book costs nothing and expires |

Calling `pre_book` again **with the same rate and the same `price_increase_percent` within a minute** returns the same answer (the same `partnerOrderId`), so a retry after a timeout does not open a second order. The key of the replay is your agency, the rate and the percentage as read: `"5"`, `5` and `5.0` are one request, another `price_increase_percent` is a new request. The supplier limits all `pre_book` calls of FX-Port to about 30 a minute: do not pre-book for many rates "just in case".

## Errors

| HTTP | Code | Cause |
|---|---|---|
| `400` | `missing_field`, `invalid_rate_hash`, `invalid_price_increase` | `book_hash` is missing (`Missing required field: hash`, `errors[0].field` is `book_hash`) or malformed, or `price_increase_percent` is not a number from 0 to 100. |
| `400` | `rate_not_found` | The supplier does not know the rate any more: it expired (30 minutes), or the price rose by more than `price_increase_percent`. Call [`get_rates`](https://fx-port.com/docs/hotels/rates.md) again and choose again. `errors[0].type` is `supplier_error`: the refusal comes from the supplier, and any other refusal of the supplier comes the same way, with its own code. |
| `400` | `supplier_invalid_response` | The supplier priced the rate but returned no usable offer (the message says `No rates in pre-book response`). Call [`get_rates`](https://fx-port.com/docs/hotels/rates.md) again. |
| `403` | `read_write_permission_required` | The key has read permission only. |
| `409` | `hotel_not_bookable_in_sandbox` | Sandbox only: the hotel is not a supplier test hotel. |
| `500` | `internal_error` | An unexpected failure, for example the supplier did not answer. Nothing is booked and you can repeat the call: within a minute it replays the answer of the first call if that one succeeded, otherwise it opens a new order (an order you never book costs nothing). |
| `502` | `supplier_invalid_response` | The supplier answered in a form that could not be used. Nothing is locked. Retry in a few minutes. |
| `503` | `supplier_busy` | The supplier is at capacity. The call was refused **before anything was sent**: nothing is locked, nothing is debited. Retry after the seconds in `Retry-After`. |

The `409` carries the test hotels:

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

## Next step

Call [`book_hotel`](https://fx-port.com/docs/hotels/book.md) with the `partnerOrderId` and the guests of each room.

## Related


- [Get hotel rates](https://fx-port.com/docs/hotels/rates.md): Where the `bookHash` comes from.
- [Book a hotel](https://fx-port.com/docs/hotels/book.md): Finish the booking.
- [Hotel pricing](https://fx-port.com/docs/hotels/pricing.md): What `totalPrice` and what you are debited mean.
- [API reference: Lock a rate before booking](https://fx-port.com/docs/api/pre-book-hotel.md): Every field and error.
