# Geo search

> Search and price every hotel around a point (latitude, longitude and radius), or one hotel by id: a compact card and the cheapest price per hotel.

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

Source: https://fx-port.com/docs/hotels/search

**Geo search** lists the hotels **around a point** with their prices. Send a `latitude`, a `longitude` and a `radius` with the dates and the guests: the answer has a compact **mini card** for each hotel and the **price of its cheapest rate** for the stay. The same call, `POST /api/v1/hotels/search_advanced`, also searches **one hotel by id**. It returns no rooms, meals or cancellation terms: call [`get_rates`](https://fx-port.com/docs/hotels/rates.md) for the hotel the user picks.

**Permission:** Read


> [!WARNING]
> **Sandbox: any hotel can be searched, only the test hotels can be booked**
>
> The search runs on real hotels and real prices in the sandbox. Booking is possible only on `test_hotel_do_not_book` and `test_hotel`: see [Sandbox testing](https://fx-port.com/docs/hotels/sandbox-testing.md).


## Search hotels around a point

`POST /api/v1/hotels/search_advanced`


**Request**

```bash
curl https://api.fx-port.com/api/v1/hotels/search_advanced \
  -H "Authorization: Bearer fxp_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "latitude": 48.8566,
  "longitude": 2.3522,
  "radius": 1000,
  "checkin": "2026-12-01",
  "checkout": "2026-12-04",
  "residency": "gb",
  "guests": [
    {
      "adults": 2,
      "children": []
    }
  ]
}'
```


These seven fields are all it needs. `radius` is in **meters** (100 to 70,000) and has **no default**. A big city is a big answer: 5 km around the center of Paris measured 1,818 hotels and 8.5 MB. Keep the radius to what your map shows ([Size and speed](#size-and-speed)).

The answer is one JSON body with two parts that you **join by hotel id**:

~~~text
status              complete | partial | cache_miss | no_hotels_found
hotels_details[]    the mini card of each hotel: id, name, address, latitude, longitude, star_rating, images, amenities (no price)
hotels_pricing      { "<hotel id>": { final_price, b2b_price, agency_commission, agency_currency, exchange_rate } }
                    the cheapest rate of each hotel for the whole stay
missing_hotel_ids   the hotels that have a price but no card yet
search_id, processing_status, queue_info, timing
~~~

Every field is described in [Response](#response), the cards in [Mini and full hotel data](https://fx-port.com/docs/hotels/hotel-data.md).

## How to get the coordinates

Ask [`locations/search`](https://fx-port.com/docs/hotels/locations.md) for the place the user typed, then take the `latitude` and `longitude` of the place they pick (for example `locations[0]`):


**Request**

```bash
curl 'https://api.fx-port.com/api/v1/hotels/locations/search?query=Paris' \
  -H "Authorization: Bearer fxp_test_YOUR_KEY"
```


[Search locations](https://fx-port.com/docs/hotels/locations.md) shows the answer and how to let the user choose between several places. To search around a hotel you know, use its own `latitude` and `longitude` (in its [mini card](https://fx-port.com/docs/hotels/hotel-data.md)).

## Search one hotel by id

To price **one hotel** whose id you know (from [`name-search`](https://fx-port.com/docs/hotels/name-search.md), an earlier search or a booking), send `hotel_id` instead of the coordinates and the radius:


**Request**

```bash
curl https://api.fx-port.com/api/v1/hotels/search_advanced \
  -H "Authorization: Bearer fxp_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "hotel_id": "test_hotel_do_not_book",
  "checkin": "2026-12-01",
  "checkout": "2026-12-04",
  "residency": "gb",
  "guests": [
    {
      "adults": 2,
      "children": []
    }
  ]
}'
```


The answer has the same shape with one card and one price, in under a second. If you send both a `hotel_id` and coordinates, `hotel_id` wins and the coordinates are ignored. A search of `test_hotel` with a residency other than `dz`, `ru` or `us` answers `no_hotels_found`.

## Request body

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `latitude` | number | For a geo search | | -90 to 90, from [`locations/search`](https://fx-port.com/docs/hotels/locations.md). |
| `longitude` | number | For a geo search | | -180 to 180. |
| `radius` | integer | For a geo search | none | Meters, 100 to 70,000. **There is no default**: a geo search without a radius cannot be priced. A value outside the range is moved to the nearest bound, and the answer carries a `coerced_value` warning. |
| `hotel_id` | string | For a search of one hotel | | The FX-Port id of the hotel, from `name-search` or from an earlier search. |
| `checkin` | string | Yes | | Check-in date, `YYYY-MM-DD`. From yesterday (for time zones) to 400 days ahead. |
| `checkout` | string | Yes | | Check-out date, after `checkin`. A stay is 1 to 30 nights. |
| `guests` | array | Yes | | One `{ adults, children }` object per room, 1 to 9 rooms. See [Guests and rooms](https://fx-port.com/docs/hotels/guests-and-rooms.md). |
| `residency` | string | No, but send it | none | The residency of the guests, a two-letter country code (`gb`, `dz`, `fr`). It decides which rates exist: some are for some residencies only. The search does not default it, unlike [`get_rates`](https://fx-port.com/docs/hotels/rates.md), which defaults to `gb`. |
| `language` | string | No | `en` | Language of the supplier's answer. The hotel content in the answer is English whatever you send. |


> [!CAUTION]
> Do not send a `currency` field: it is refused with `400`. Prices come in the currency of your agency, resolved from your key.


Several rooms: one `{ adults, children }` object per room, with the age of each child:


**Request**

```bash
curl https://api.fx-port.com/api/v1/hotels/search_advanced \
  -H "Authorization: Bearer fxp_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "latitude": 48.8566,
  "longitude": 2.3522,
  "radius": 1500,
  "checkin": "2026-12-10",
  "checkout": "2026-12-12",
  "residency": "gb",
  "guests": [
    {
      "adults": 2,
      "children": [
        8
      ]
    },
    {
      "adults": 1,
      "children": []
    }
  ]
}'
```


### Legacy names

These older spellings still work. The answer carries a `warnings` entry with the code `deprecated_field` that names the spelling to use (and the headers `Deprecation` and `X-FX-Deprecated-Fields`). See [Legacy names](https://fx-port.com/docs/legacy-names.md).

| Older name | Use instead |
|---|---|
| `id`, `hotelId` | `hotel_id` |
| `check_in`, `checkin_date`, `checkinDate` | `checkin` |
| `check_out`, `checkout_date`, `checkoutDate` | `checkout` |

## Values the API reads for you

A value that a real client writes in another well-defined way is **read, not refused**, and the answer carries a `coerced_value` warning that says what was read (see [Conventions](https://fx-port.com/docs/conventions.md#a-previous-spelling-still-works-and-says-so)). Send the documented form to avoid the warning.

| You send | The API reads |
|---|---|
| `"radius": 600000` or `50` | `70000` or `100`: a radius outside 100 to 70,000 meters is moved to the nearest bound |
| A number written as text: `"radius": "15000"`, `"latitude": "14.09"`, `"adults": "2"`, a child age `"8"` | The number. An integral float (`2.0`) is the same number and has no warning |
| `"checkin": "2026-12-01T00:00:00Z"`, a date with a **midnight** time | `2026-12-01`. Any other time of day is `400 invalid_date` |
| `"residency": "GB-SCT"`, `"USPR"`, `"ES-XCI"`, `"PT-MA"`, the sub-territories of the website's nationality list | The country: `gb`, `us`, `es`, `pt`. Send the 2-letter country |
| `"language": "zh-Hans-CN"`, `"es-419"`, `"sr-Latn-RS"` | `zh_CN`, `es`, `sr` |
| `"hotel_id": 8473727`, a JSON integer, or an id with spaces around it | The text `"8473727"`, or the trimmed id. A number is **not** looked up as the supplier's numeric `hid` here: send the FX-Port id (the [content calls](https://fx-port.com/docs/hotels/hotel-data.md#reading-hotel-content) do look a `hid` up) |
| `"hotel_id": null` next to `latitude` and `longitude` | A search around a point (a `null` alone is `400 missing_field`) |

What cannot be read keeps its error code: a boolean or `NaN` where a number is wanted, `1e3`, a fraction where a whole number is wanted, `usa`, `gbr`, `en-USA`, another time of day. [`get_rates`](https://fx-port.com/docs/hotels/rates.md) reads the same values.

## Response

The answer is **always one JSON body**. It is a real, trimmed answer of the sandbox (a geo search with a 4 km radius around Nuuk: five hotels were priced, the answer is cut to two here). A search of one hotel answers the same keys with one card:

~~~json
{
  "environment": "sandbox",
  "search_id": "71498fa9-d3a7-4b6c-81bd-76693be726d1",
  "status": "complete",
  "hotels_details": [
    {
      "id": "hotel_nordbo_2",
      "hid": 10048834,
      "name": "Hotel Nordbo",
      "kind": "Hotel",
      "address": "Vandsovej 13, Nuuk, 3900, Greenland, Nuuk",
      "latitude": 64.17923,
      "longitude": -51.73019,
      "postal_code": null,
      "region": {
        "id": 1359,
        "country_code": "GL",
        "iata": "GOH",
        "name": "Nuuk",
        "type": "City",
        "type_v2": "city"
      },
      "star_rating": 0,
      "images": [
        "https://cdn.worldota.net/t/{size}/content/16/be/16be5aea51c0060686286cf8c886e4e07e1e5dce.jpeg",
        "https://cdn.worldota.net/t/{size}/content/b1/98/b198dca0393650833ee449c3e460b2af85a04dda.jpeg"
      ],
      "serp_filters": [
        "has_parking",
        "has_meal",
        "has_pets"
      ],
      "amenity_groups": [
        {
          "amenities": [
            "24-hour reception",
            "Smoke-free property",
            "Ticket assistance"
          ],
          "non_free_amenities": [],
          "group_name": "General"
        },
        {
          "amenities": [
            "Breakfast",
            "Restaurant"
          ],
          "non_free_amenities": [],
          "group_name": "Meals"
        }
      ]
    },
    {
      "id": "hhe_express",
      "hid": 9168363,
      "name": "HHE Express Hotel",
      "kind": "Hotel",
      "address": "Aqqusinersuaq 14B, Nuuk",
      "latitude": 64.17208,
      "longitude": -51.73649,
      "postal_code": "3900",
      "region": {
        "id": 1359,
        "country_code": "GL",
        "iata": "GOH",
        "name": "Nuuk",
        "type": "City",
        "type_v2": "city"
      },
      "star_rating": 3,
      "images": [
        "https://cdn.worldota.net/t/{size}/content/b5/39/b539a20fd8349ef419e73802ced9a04215155205.JPEG",
        "https://cdn.worldota.net/t/{size}/content/41/97/4197c95a76a3ada2539a0fe169e7e3d7df3e80f9.jpeg"
      ],
      "serp_filters": [
        "has_internet",
        "has_parking"
      ],
      "amenity_groups": [
        {
          "amenities": [
            "24-hour reception",
            "Elevator/lift",
            "Smoke-free property"
          ],
          "non_free_amenities": [],
          "group_name": "General"
        },
        {
          "amenities": [
            "Free Wi-Fi"
          ],
          "non_free_amenities": [],
          "group_name": "Internet"
        }
      ]
    }
  ],
  "processing_status": {
    "total_hotels": 5,
    "cached_hotels": 5,
    "queued_hotels": 0
  },
  "queue_info": {
    "queued_hotel_ids": [],
    "estimated_completion": null
  },
  "timing": {
    "total_duration": 0.84,
    "search_duration": 0.82,
    "cache_check_duration": 0.02
  },
  "hotels_pricing": {
    "hotel_nordbo_2": {
      "exchange_rate": 285,
      "agency_currency": "DZD",
      "agency_commission": 438.68,
      "final_price": 219774.68,
      "b2b_price": 219336
    },
    "hhe_express": {
      "exchange_rate": 285,
      "agency_currency": "DZD",
      "agency_commission": 228.83,
      "final_price": 114639.23,
      "b2b_price": 114410.4
    }
  },
  "missing_hotel_ids": [],
  "missing_hotels_pricing": {}
}
~~~

| Field | Description |
|---|---|
| `search_id` | The id of the search, also in the `X-Search-Id` header. Quote it when you write to support. There is nothing to pass on to the next call. |
| `status` | `complete`: every priced hotel has a card. `partial`: some do not (see `missing_hotel_ids`). `cache_miss`: none has. `no_hotels_found`: nothing is available for the request. |
| `hotels_details[]` | The **mini card** of each hotel that has cached content: 13 fields (id, name, address, position, stars, photos, filters, amenities). No rate, no room, no price. See [Mini and full hotel data](https://fx-port.com/docs/hotels/hotel-data.md) and [The Hotel mini object](https://fx-port.com/docs/api/objects/hotel-mini.md). |
| `hotels_pricing` | The price of the **cheapest rate** of every priced hotel, by hotel id: `final_price` (what the traveler pays for the whole stay and all rooms), `agency_commission`, `b2b_price` (`final_price - agency_commission`, what your agency is debited), `agency_currency` and `exchange_rate`. See [Hotel pricing](https://fx-port.com/docs/hotels/pricing.md). |
| `missing_hotel_ids` | The hotels that are priced but have no card yet. |
| `missing_hotels_pricing` | The price entry of each of them, the same fields as `hotels_pricing`. |
| `processing_status` | `total_hotels`, `cached_hotels` and `queued_hotels`. |
| `queue_info` | The ids that FX-Port is loading in the background (`queued_hotel_ids`) and a rough delay (`estimated_completion`, `estimated_completion_seconds`). |
| `timing` | Seconds spent: `total_duration`, `search_duration` (the supplier) and `cache_check_duration`. |
| `truncated`, `max_hotels`, `total_hotels_found` | Present only when the supplier found more than 5,000 hotels and the rest was cut off. Narrow the search. |
| `note` | A hint about an empty answer, for example that `test_hotel` has no availability for the residency. |

The answer carries the headers `X-Search-Id`, `X-Search-Status`, `X-Search-Type` (`hotel_id` or `geo`), `X-Total-Hotels`, `X-Available-Hotels`, `X-Search-Time` and `X-Environment`.

### Join the cards and the prices

`hotels_details` and `hotels_pricing` are separate: join them by hotel id. A hotel that has a price and no card is in `missing_hotel_ids`:

~~~json
{
  "environment": "sandbox",
  "search_id": "4a6877dc-96da-4f32-8c1b-4aaed8b60fe4",
  "status": "partial",
  "hotels_details": [
    {
      "id": "hotel_honduras_maya_6",
      "hid": 6329711,
      "name": "Hotel Honduras Maya",
      "kind": "Hotel",
      "address": "Colonia Palmira 3era calle ave, Tegucigalpa",
      "latitude": 14.103492,
      "longitude": -87.19721,
      "postal_code": "11101",
      "region": {
        "id": 3421,
        "country_code": "HN",
        "iata": "TGU",
        "name": "Tegucigalpa",
        "type": "City",
        "type_v2": "city"
      },
      "star_rating": 3,
      "images": [
        "https://cdn.worldota.net/t/{size}/content/1d/67/1d67eefb8685fb4b809624bb4f6c437592722183.JPEG",
        "https://cdn.worldota.net/t/{size}/content/6a/0e/6a0ebfe39089a02e1e6da4a03eb6200047545ba1.JPEG"
      ],
      "serp_filters": [
        "has_internet",
        "has_airport_transfer",
        "has_parking",
        "has_kids",
        "has_pool"
      ],
      "amenity_groups": [
        {
          "amenities": [
            "ATM",
            "Air conditioning",
            "24-hour reception"
          ],
          "non_free_amenities": [],
          "group_name": "General"
        },
        {
          "amenities": [
            "Bridal suite",
            "Room service",
            "Interconnecting rooms available"
          ],
          "non_free_amenities": [],
          "group_name": "Rooms"
        }
      ]
    }
  ],
  "processing_status": {
    "total_hotels": 15,
    "cached_hotels": 14,
    "queued_hotels": 1
  },
  "queue_info": {
    "queued_hotel_ids": [
      "comfort_inn_tegucigalpa"
    ],
    "estimated_completion": "about 9 minutes",
    "estimated_completion_seconds": 541
  },
  "timing": {
    "total_duration": 4.38,
    "search_duration": 4.33,
    "cache_check_duration": 0.05
  },
  "hotels_pricing": {
    "hotel_honduras_maya_6": {
      "exchange_rate": 285,
      "agency_currency": "DZD",
      "agency_commission": 285.14,
      "final_price": 142853.54,
      "b2b_price": 142568.4
    },
    "comfort_inn_tegucigalpa": {
      "exchange_rate": 285,
      "agency_currency": "DZD",
      "agency_commission": 218.75,
      "final_price": 109590.36,
      "b2b_price": 109371.61
    }
  },
  "missing_hotel_ids": [
    "comfort_inn_tegucigalpa"
  ],
  "missing_hotels_pricing": {
    "comfort_inn_tegucigalpa": {
      "exchange_rate": 285,
      "agency_currency": "DZD",
      "agency_commission": 218.75,
      "final_price": 109590.36,
      "b2b_price": 109371.61
    }
  }
}
~~~

Show such a hotel with its price and a placeholder, or fetch its card with [`get_hotel_info`](https://fx-port.com/docs/hotels/hotel-details.md) (`detail_level` `minimal`, one id per call, five at a time). The recipe is in [Mini and full hotel data](https://fx-port.com/docs/hotels/hotel-data.md#hotels-without-content).

## Size and speed

There is **no pagination**, and the price list is part of the same answer. Measured on the sandbox (2026-11-01, the search call only):

| Search | Hotels | Answer | Time |
|---|---|---|---|
| One hotel | 1 | 2.9 KB (1.2 KB gzip) | 0.4 to 0.7 s |
| Nuuk, 4 km | 5 | 20 KB (5.7 KB gzip) | 1.5 s |
| Tegucigalpa, 15 km | 15 | 82 KB (17 KB gzip) | 4.1 s |
| Los Angeles, 5 km | 73 | 362 KB (79 KB gzip) | 9.6 s |
| Paris, 5 km | 1,818 | 8.5 MB (1.7 MB gzip) | 19.3 s |

About 93% of a large answer is `hotels_details`. So: **keep the radius to what the screen needs**, send `Accept-Encoding: gzip`, show the first hotels and read the rest in your own pages, and do not run the same search again for each page. One search answers at most 5,000 hotels. Identical searches of your agency that arrive at the same moment are merged into one supplier call; each caller still gets its own `request_id` and headers.

## No streaming for API keys

`?stream=true` and `Accept: text/event-stream` are **ignored** for API keys: the answer is always one JSON body (`Content-Type: application/json`). Server-sent events exist for FX-Port's own dashboard only. Streaming of flight searches is described in [Streaming](https://fx-port.com/docs/streaming.md).

## Errors

| HTTP | Code | Cause |
|---|---|---|
| `400` | `missing_field` | A required field is missing: `checkin`, `checkout` or `guests`; there is neither `hotel_id` nor `latitude` and `longitude`; or a search around a point has no `radius`. `errors[0].field` names the first missing field (always the current name: `hotel_id`, not `id`) and `errors[0].details.missing_fields` lists all of them when several are missing. The message is the older sentence (`Missing required fields for hotel_id search: ['guests']`). |
| `400` | `bad_request` | You sent a `currency` field. |
| `400` | `invalid_date`, `date_in_past`, `date_too_far`, `checkout_before_checkin`, `stay_too_long` | The dates are not a real stay: a wrong date, in the past, more than 400 days ahead (`errors[0].details.max_days_ahead` says 400), `checkout` not after `checkin`, or more than 30 nights. |
| `400` | `invalid_guests`, `invalid_adult_count`, `too_many_children`, `invalid_child_age`, `too_many_guests`, `too_many_rooms` | The rooms do not follow [the rules of guests and rooms](https://fx-port.com/docs/hotels/guests-and-rooms.md#constraints). |
| `400` | `invalid_latitude`, `invalid_longitude`, `invalid_radius` | A coordinate is out of range, or the radius is not a whole number. |
| `400` | `invalid_residency`, `invalid_language`, `invalid_hotel_id` | The code is not one the supplier knows. |
| `400` | `invalid_json`, `invalid_body` | The body is not a JSON object. |
| `403`, `401` | | The key is missing, wrong or not allowed: [Authentication](https://fx-port.com/docs/authentication.md). |
| `422` | `supplier_rejected_request` | The supplier refused well formed parameters (`errors[0].type` is `supplier_error`; `errors[0].details.supplier_code` has its code when it is a plain one). Change the dates, the guests, the residency or the radius. |
| `429` | `rate_limit_exceeded` | More than 6,000 searches a minute for the key: [Rate limits](https://fx-port.com/docs/rate-limits.md). With `errors[0].type` `supplier_error` the code says that the hotel supplier is receiving too many searches: retry in a minute. |
| `502` | `bad_gateway` | The supplier could not complete the search (`errors[0].type` is `supplier_error`). Retry in a few minutes with backoff. |
| `503` | `supplier_busy` | The supplier is at capacity for searches. The call was **refused before anything was sent**: retry after the seconds in `Retry-After`. |

A `200` with `status: "no_hotels_found"` is not an error: nothing is available for the request (also when a hotel exists but not for this residency).

## Gotchas

- **`hotels_pricing` is the cheapest rate**, not a quote. It has no room, meal or cancellation terms. The rates of the hotel come from [`get_rates`](https://fx-port.com/docs/hotels/rates.md), which returns prepaid rates only, so in rare cases the cheapest rate there is higher.
- **The prices are for the whole stay and all rooms**, in the currency of your agency. There is no price per night.
- **Send the same `residency` and `guests` to `get_rates`** as to the search, or the rates will differ.
- **A search is not a booking step.** `get_rates` makes its own search of the hotel; no id of the search is needed to book.

## Related


- [Mini and full hotel data](https://fx-port.com/docs/hotels/hotel-data.md): The cards of the answer, the full document and the hotels without a card.
- [Get hotel rates](https://fx-port.com/docs/hotels/rates.md): The rooms and rates of the hotel the user picked.
- [Search locations](https://fx-port.com/docs/hotels/locations.md): Coordinates for a typed destination, the input of the geo search.
- [API reference: Geo search](https://fx-port.com/docs/api/search-hotels.md): Every field and error.
