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.
POST/api/v1/hotels/search_advanced
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 /, also searches one hotel by id. It returns no rooms, meals or cancellation terms: call get_rates for the hotel the user picks.
Permission: Read
Search hotels around a point
POST /
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).
The answer is one JSON body with two parts that you join by hotel id:
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, timingEvery field is described in Response, the cards in Mini and full hotel data.
How to get the coordinates
Ask locations/ for the place the user typed, then take the latitude and longitude of the place they pick (for example locations[0]):
curl 'https://api.fx-port.com/api/v1/hotels/locations/search?query=Paris' \
-H "Authorization: Bearer fxp_test_YOUR_KEY"Search locations 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).
Search one hotel by id
To price one hotel whose id you know (from name-search, an earlier search or a booking), send hotel_id instead of the coordinates and the radius:
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/. | |
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. | |
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, 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. |
Several rooms: one { adults, children } object per room, with the age of each child:
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.
| 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). 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 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 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:
{
"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 and The Hotel mini object. |
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. |
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:
{
"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 (detail_level minimal, one id per call, five at a time). The recipe is in Mini and full hotel data.
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/ are ignored for API keys: the answer is always one JSON body (Content-Type: application/). Server-sent events exist for FX-Port's own dashboard only. Streaming of flight searches is described in Streaming.
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. |
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. | |
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. 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_pricingis the cheapest rate, not a quote. It has no room, meal or cancellation terms. The rates of the hotel come fromget_rates, 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
residencyandgueststoget_ratesas to the search, or the rates will differ. - A search is not a booking step.
get_ratesmakes its own search of the hotel; no id of the search is needed to book.