Mini and full hotel data
Use the compact mini data for search result lists and the full data only for the hotel page, with the size difference measured.
Hotel content comes in two sizes, and you can use both: the mini card for every hotel of a result list, and the full document for the one hotel the user opens. Mini is a compact card of 13 fields, about 5 KB a hotel. Full is the whole document, 35 to 45 KB a hotel on average and up to 150 KB: 7 to 9 times larger, and 92% of a mini card is photos and amenities. So build lists from mini data, fetch full data for one hotel when it is opened, and cache both.
Which one to use
| You are building | Use | Why |
|---|---|---|
| A list of results or map pins | hotels_details[] of search_advanced, joined by hotel id with its hotels_pricing | One call gives the cards and the prices. Already mini. |
| Cards for hotel ids you already have (favourites, name search hits, a saved list) | POST / with up to 200 ids | One call, from FX-Port's cache, no price. |
The card of a hotel in missing_hotel_ids | get_hotel_info with hotel_id and detail_level=minimal | The only call that asks the supplier when FX-Port does not hold the hotel yet. |
| The page of one hotel the user opened | get_hotel_info (full), next to get_rates | Description, every room type, policies, facts, contact details. |
| The pages of several hotels at once (a comparison) | POST / (full, snake_case) with their ids | One call, from the cache. |
| A copy of the hotel documents in your own catalogue | POST / | The stored document, with images_ext and policy_struct. |
Mini data
The mini card has 13 fields in snake_case. It is the same card wherever you get it:
hotels_details[]ofsearch_advanced(automatic, with every search);POST /(aliashotels/ search?detail_level=minimal mini);POST /;hotels/ details?detail_level=minimal get_hotel_infowithdetail_level=minimal.
| Field | Type | What it holds |
|---|---|---|
id | string | The FX-Port hotel id, a lower-case slug. Send it as hotel_id. |
hid | integer | The supplier's numeric hotel id. |
name | string | Name of the hotel. |
kind | string | Type of property: Hotel, Apartment, Resort, Hostel ... |
address | string | Street address, in English. |
latitude, longitude | number | Position, for the map. |
postal_code | string or null | Postal code; null for a minority of hotels. |
region | object | { id, country_code, iata, name, type, type_v2 }: the city. |
star_rating | integer | 0 to 5. 0 means the property is not rated. |
images | array or null | Photo URL templates, best first (see Images). null for a few hotels (62 of 1,818 around Paris). |
serp_filters | array of strings | Filter tags: has_internet, has_parking, has_pool, has_spa, has_fitness, has_pets, has_kids, has_meal ... |
amenity_groups | array | [{ group_name, amenities[], non_free_amenities[] }]. |
{
"id": "test_hotel_do_not_book",
"hid": 8473727,
"name": "Test Hotel (Do Not Book) test",
"kind": "Apartment",
"address": "Avenida Roble, Frente al Mall Multiplaza, Tegucigalpa",
"latitude": 14.079872,
"longitude": -87.21677,
"postal_code": null,
"region": { "id": 3421, "country_code": "HN", "iata": "TGU", "name": "Tegucigalpa", "type": "City", "type_v2": "city" },
"star_rating": 0,
"images": [
"https://cdn.worldota.net/t/{size}/extranet/b5/d3/b5d3d33394494c68321246882c1bd93a6832dcd5.jpeg",
"https://cdn.worldota.net/t/{size}/extranet/15/44/15440558c952ec4c3488730d1563dc9e8d25832c.JPEG"
],
"serp_filters": ["has_internet", "has_parking", "has_spa", "has_pets", "has_jacuzzi", "kitchen"],
"amenity_groups": [
{ "amenities": ["24-hour reception", "Early check-in", "Late check-out"], "non_free_amenities": [], "group_name": "General" }
]
}The reference describes every field: The Hotel mini object.
A mini card has no price, no room and no rate. It is made for a list: where the hotel is, what it looks like, what it offers. The price of the cheapest rate is in hotels_pricing, and the rooms and cancellation terms come from get_rates.
Full data
The full document adds what the hotel page needs: the description, every room type with photos and amenities, the policies, the facts, the contact details and the times of check-in and check-out. It comes in three spellings:
| Call | Keys | Notes |
|---|---|---|
POST / (default detail_level=full) | snake_case | 31 fields, from the cache. The Hotel full object. |
get_hotel_info (default) | camelCase | The same data with the room properties decoded into words. For a hotel that is not cached it asks the supplier. |
POST / | snake_case | The stored document: the 31 fields plus images_ext and policy_struct. About 27% larger. |
The fields of the full document (snake_case names; the camelCase document has the same data):
| Field | What it holds |
|---|---|
id, hid, name, kind, hotel_chain | Identity. hotel_chain is No chain for an independent hotel. |
address, postal_code, latitude, longitude, region | Place. |
star_rating, star_certificate | Stars, and the official certificate when there is one ({ certificate_id, valid_to }, else null). |
email, phone | Contact details of the hotel. |
check_in_time, check_out_time, check_in_time_end | Times of day (14:00:00, local time), not dates. check_in_time_end only for hotels that state it. |
front_desk_time_start, front_desk_time_end | Opening hours of the front desk, or null. |
images, amenity_groups, serp_filters | The same as in the mini card. |
description_struct | The description in titled blocks: [{ title, paragraphs[] }]. |
metapolicy_struct | The policies: internet, meal, children_meal, extra_bed, cot, pets, shuttle, parking, children, visa, deposit, no_show, add_fee, check_in_check_out. Read children before you assume a child stays free. |
metapolicy_extra_info | The house rules as free text, or null. |
room_groups | Every room type: room_group_id, name, name_struct, rg_ext (numeric codes), room_amenities, images. About 80% of the bytes of the document. |
facts | Floors, rooms, year built, year renovated, electricity, registration. Many are null. |
payment_methods | Payment methods accepted at the property. |
keys_pickup | How guests get the keys: type, is_contactless, phone, email, apartment_office_address, apartment_extra_information. |
is_closed, is_gender_specification_required, deleted | Flags. is_closed: true means get_rates finds no rate. |
distance_center, giata_code | Optional: present for some hotels only. |
get_hotel_info spells these keys in camelCase (starRating, amenityGroups, descriptionStruct, roomGroups), and differs in six places:
| In the snake_case document | In get_hotel_info |
|---|---|
amenity_groups[].non_free_amenities | amenityGroups[].paidAmenities |
check_in_time, check_out_time | checkIn, checkOut (the snake_case names are returned as twins) |
email, phone | contact: { email, phone } |
front_desk_time_start, front_desk_time_end | frontDeskHours: { start, end } |
room_groups[].rg_ext (12 numeric codes) | decoded: roomClass, quality, bathroom, hasPrivateBathroom, capacity, balcony, view, floor, bedrooms, beddingType, isFamily, isClub, genderRestriction |
description_struct[] | the same blocks plus html |
The entries inside metapolicyStruct keep their snake_case keys (age_start, price_unit, meal_type).
How much smaller is mini
Measured on the sandbox on 2026-11-01 (decoded JSON; "gzip" is what travels over the network):
| What | Mini | Full | Full / mini |
|---|---|---|---|
| One hotel, the test hotel (7 photos, 11 amenity groups, 9 room types) | 2.3 KB (0.9 KB gzip) | 11.9 KB (3.2 KB gzip), get_hotel_info 13.1 KB | 5.2x (5.7x) |
| 15 hotels around Tegucigalpa | 79 KB, 5.3 KB a hotel (16.7 KB gzip) | 654 KB, 43.6 KB a hotel (72.8 KB gzip); get_hotel_info 723 KB | 8.3x decoded, 4.4x gzip |
| 73 hotels around Los Angeles | 349 KB, 4.8 KB a hotel (75.7 KB gzip) | 2,493 KB, 34.2 KB a hotel (309 KB gzip); get_hotel_info 2,784 KB, 38.1 KB a hotel | 7.1x decoded, 4.1x gzip |
A mini card is also predictable: in the 73 hotels it runs from 0.4 KB to 8.3 KB, 4.6 KB in the middle. A full document runs from 3.8 KB to 131 KB, 23 KB in the middle.
Where the bytes go (73 hotels): in a mini card, photos are 58% and amenities 34%, so 92% of it is photos and amenities. In a full document the room types are 79%, the photos 8% and the amenities 5%.
A mini card is still larger than what a list shows. What a result card needs is the id, name, kind, address, position, stars, filters, the city name and one photo: about 0.5 KB, or 1.0 KB with six photos (computed on the 15 hotels above; 5.1 KB a hotel in mini form). Keep those fields when you store or forward the cards, and do not send the amenity groups to a phone.
Hotels without content
A search prices every hotel the supplier returns, but the content comes from FX-Port's own cache of hotel content. A hotel that is priced but not cached yet (a new property) is not in hotels_details. The search tells you:
{
"status": "partial",
"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 },
"missing_hotel_ids": ["comfort_inn_tegucigalpa"],
"hotels_pricing": { "comfort_inn_tegucigalpa": { "final_price": 109590.36, "b2b_price": 109371.61, "...": "..." } }
}missing_hotel_idslists the hotels that have a price and no card. The price is inhotels_pricing(andmissing_hotels_pricing) as for any other hotel.- FX-Port queues them for loading in the background (
queue_info): at most 50 ids a search. They are usually in the cache a few minutes later and then appear inhotels_detailsof the next search. statusiscompletewhen every hotel has content,partialwhen some do not,cache_misswhen none does.
It is rare: 1 hotel of 15 in the example (and cached 9 minutes later), none of 73 around Los Angeles, none of 1,818 around Paris.
To get the card of a missing hotel now, call get_hotel_info with that one id:
{ "hotel_id": "comfort_inn_tegucigalpa", "detail_level": "minimal" }It is the only content call that asks the supplier when FX-Port does not hold the hotel (about 0.3 s), and it keeps the hotel for everybody afterwards. It does this for one id per call. The calls that read many ids (POST /, POST /, get_hotel_info with hotel_ids) read the cache only: they leave out the ids they do not hold and do not report them.
How to do it well:
- Fill only the hotels the user can see, not all of them. Render the others with their price and a placeholder, or leave them out until their content is cached.
- Make at most five calls at a time. The supplier answers about 30 hotel content calls a minute for the whole of FX-Port, so a
429,502or503answer means you went too fast or the supplier is busy: back off. A503 supplier_busywas refused before anything was sent: wait the seconds ofRetry-Afterand ask again. - Send
"language": "en"when you fetch a hotel that is not cached. The content that is fetched is stored for every customer in the language of that call. - Remember the result for the hotel id, and remember a failure for a few minutes, so that the next search does not repeat the same calls.
Images
images holds URL templates. Replace {size} to get a picture:
https://cdn.worldota.net/t/{size}/extranet/b5/d3/b5d3d33394494c68321246882c1bd93a6832dcd5.jpeg{size} | Use | Note |
|---|---|---|
120x120 | Thumbnails, map pins | |
240x240 | Cards of a result list | 18 KB for the photo above |
640x400 | A hotel page, the first screen | |
1024x768 | A gallery, a full-width image | 228 KB for the photo above |
x500 | 500 px high, the width follows |
These sizes answer 200. orig does not (it redirects to a web page) and unknown sizes answer 400. Choose the size by what you display: one 1024x768 photo can weigh as much as 40 mini cards, so the size you ask the CDN for matters more than the card.
const photo = (template, size = "240x240") => template.replace("{size}", size);
const card = search.hotels_details[0];
const thumbnail = card.images ? photo(card.images[0]) : "/no-photo.svg"; // images can be nulldef photo(template, size="240x240"):
return template.replace("{size}", size)
card = search["hotels_details"][0]
thumbnail = photo(card["images"][0]) if card["images"] else "/no-photo.svg" # images can be nullimages_limit (0 to 30) on the content calls (POST /, POST / and get_hotel_info) keeps only the first N photos of each mini card: images_limit=1 removes about half of a mini card. It applies to the mini card only (detail_level=minimal, alias mini): with the full document it is ignored and the answer carries a coerced_value warning. It does not exist on search_advanced, which always returns every photo of every hotel.
Reading hotel content
Three calls read hotels by id. They read FX-Port's cache, they take up to 200 ids, and they answer a bare JSON array that does not follow the order of your ids; an id that is not cached is left out and queued for loading. An id can also be the supplier's numeric hid, as a JSON integer or as digits ([8473727]): POST / and POST / look an id up as an id and then as a hid, a hotel reached by both comes back once, and a digits-only id that matches nothing is not queued; get_hotel_info takes a hid too. Spaces around an id are trimmed.
| Call | Body | Answer |
|---|---|---|
POST / | { "hotel_ids": [...] } (a bare list still works) | Mini cards with ?detail_level=minimal, full documents otherwise |
POST / | the same | The stored documents (?detail_level=minimal gives mini cards) |
POST / | { "hotel_id": "..." } or { "hotel_ids": [...] } | { "data": [...] }, camelCase (mini cards with detail_level) |
curl 'https://api.fx-port.com/api/v1/hotels/search?detail_level=minimal&images_limit=1' \
-H "Authorization: Bearer fxp_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"hotel_ids": [
"test_hotel_do_not_book",
"hotel_honduras_maya_6"
]
}'| Query parameter | Default | Description |
|---|---|---|
detail_level | full | minimal (alias mini) or full. Anything else answers 400 invalid_detail_level. |
fields | none | A comma separated list of top-level fields (id,name,star_rating), at most 40. It replaces detail_level; id is always added. Nested paths are not supported. |
images_limit | none | With detail_level=minimal and no fields: the most photos of each hotel, 0 to 30 (another number answers 400 invalid_images_limit). In any other case it is ignored and the answer carries a coerced_value warning. POST / reads it too, with detail_level=minimal. |
The answer carries X-Results-Count, X-Detail-Level and X-Search-Time headers. A body that is not a list of ids, or that holds more than 200 ids, answers 400 invalid_body, and a malformed id 400 invalid_hotel_id. See the reference: POST /, POST / and POST /.
Legacy names
These older spellings still work. The answer carries a deprecated_field warning that names the spelling to use (see Legacy names).
| Older name | Use instead |
|---|---|
hotelIds, ids (body) | hotel_ids |
detailLevel (query) | detail_level |
imagesLimit (query) | images_limit |
Caching
- Cache content by hotel id. A hotel's content changes rarely: a day is a safe default for a card, and a result list can use content that is a week old. Do not put the language in the key: cached content is English whatever
languageyou send (languageonly applies to the live fetch of a hotel that is not cached, and to room names inget_rates). - Never cache prices beyond the search that returned them.
bookHashvalues live 30 minutes, and a price can change at any time. - Cache the card, not the answer. Keep the fields you show and drop
amenity_groupsand all but the photos you display. - Do not refetch on every search. Search again only for prices; read content for the ids you have not seen.
A results list in code
The answer of search_advanced has the cards in hotels_details and the prices in hotels_pricing. Join them by hotel id, and treat the ids of missing_hotel_ids apart:
search = call("POST", "/hotels/search_advanced", json=request)
cards = {card["id"]: card for card in search["hotels_details"]}
rows = []
for hotel_id, price in search["hotels_pricing"].items():
card = cards.get(hotel_id) # None for a hotel in missing_hotel_ids
rows.append({
"id": hotel_id,
"name": card["name"] if card else None,
"stars": card["star_rating"] if card else None,
"photo": card["images"][0].replace("{size}", "240x240") if card and card["images"] else None,
"price": price["final_price"],
"currency": price["agency_currency"],
})
rows.sort(key=lambda row: row["price"])
to_fill = [row["id"] for row in rows[:20] if row["name"] is None] # only what is visible, five at a timeconst search = await call("POST", "/hotels/search_advanced", request);
const cards = new Map(search.hotels_details.map((card) => [card.id, card]));
const rows = Object.entries(search.hotels_pricing).map(([id, price]) => {
const card = cards.get(id); // undefined for a hotel in missing_hotel_ids
return {
id,
name: card?.name ?? null,
stars: card?.star_rating ?? null,
photo: card?.images?.[0]?.replace("{size}", "240x240") ?? null,
price: price.final_price,
currency: price.agency_currency,
};
});
rows.sort((a, b) => a.price - b.price);
const toFill = rows.slice(0, 20).filter((row) => row.name === null).map((row) => row.id); // only what is visible