Hotel catalogue
Build and sync a local copy of the hotel catalogue for hotel pages, maps, filters, autocomplete and SEO. Prices never come from the catalogue, only from Quote.
The static content endpoints give you the hotels your microsite can sell: names, locations, images, facilities, ratings and destinations. Use them for hotel pages, maps, filters, autocomplete and SEO. Never use them for prices or availability. Those come only from Quote.
Static content calls accept any valid token. They don't need the booking-flow token, so a catalogue job can keep its own.
The endpoints
| Endpoint | Returns | Notes |
|---|---|---|
GET /accommodations?first=0&limit=20000 | id, giataId, name, geolocation, countryCode, lastUpdate, plus pagination | first and limit are required. limit is at most 20000. |
GET /accommodations/{accommodationId}/datasheet?lang=EN | One full datasheet | |
GET /accommodations/datasheet?accommodationId=1000&accommodationId=1003&lang=EN | Up to 100 datasheets | Repeat the accommodationId parameter. Don't comma-join it. |
GET /facilites/accommodation?lang=EN | Hotel facility catalogue: id, icon, priority, translations | The path is spelled "facilites" (sic). lang is required. |
GET /facilites/room?lang=EN | Room facility catalogue, with a category per facility | lang is required. |
GET /mealplan/{micrositeId}?lang=EN | The microsite's meal plans: id, type, description | Use it to label mealPlan.id codes. |
GET /accommodations/preferred/{micrositeId} | The microsite's preferred (promoted) hotels as full datasheets | Query destinationId, countryCode, first, limit, lang. Good for landing pages and "recommended" rails. |
GET /destination/{micrositeId} | Destinations. code is what Quote's destinationId expects. | Query countryCode, iata, lang. Also /destination/{micrositeId}/{destinationId} and /destination/countries/{micrositeId}. |
Limits and parameters come from the spec. Which hotels the list contains depends on the providers connected to your credential's microsite. The documentation's own sample reported 86,768 hotels, so expect several pages.
{
"auditData": { "authToken": "[REDACTED]", "traceId": "D4AE54C6-…" },
"accommodations": [
{
"id": "1000",
"giataId": 314272,
"name": "Hotel Palas Pineda",
"geolocation": { "latitude": 41.06788, "longitude": 1.17602 },
"countryCode": "ES",
"lastUpdate": "2022-10-30 18:36"
}
],
"pagination": { "firstResult": 0, "pageResults": 1, "totalResults": 86768 }
}Sync pipeline
Download the list weekly, as the documentation recommends, off-peak, and fetch a datasheet again only when its lastUpdate changed.
weekly, off-peak:
1. page through GET /accommodations (limit 20000)
upsert id, giataId, name, geolocation, countryCode, lastUpdate
2. mark ids not seen in this run as inactive (soft delete: bookings may reference them)
3. changed = new ids + ids whose lastUpdate is newer than the stored one
4. for each language you serve:
GET /accommodations/datasheet for the changed ids, in batches of 100
5. refresh the facility catalogues and meal plans for each language
daily (optional):
GET /accommodations/preferred/{micrositeId} for merchandising railsPage through the list. Call GET /accommodations with limit 20000, moving first on until you have totalResults rows. Upsert id, giataId, name, geolocation, countryCode and lastUpdate.
Soft-delete what disappeared. Mark ids you did not see in this run as inactive and stop quoting them. Don't hard-delete them: bookings may still reference them.
Find what changed. New ids, plus ids whose lastUpdate is newer than the one you stored. If lastUpdate is older than your last download, skip the datasheet.
Fetch datasheets. For each language you serve, call GET /accommodations/datasheet for the changed ids, 100 per call, repeating the accommodationId parameter.
Refresh the small catalogues. Facility catalogues and meal plans, for each language. They are small, so you can fetch all of them every run.
- No rate limits are documented. We recommend 2 to 4 parallel requests at most, with back-off on 429 and 5xx.
- Store the raw datasheet JSON next to your normalised columns. Shapes have changed before (see facilities).
- Keep content per language.
descriptionand facilitytranslationsare language-specific.langtakes theLanguageenum (EN,ES,ARand others, see Enums).
Code sketch
Pages through /accommodations, then fetches datasheets for the changed ids in batches of 100. catalogue stands for your own storage.
const BASE = process.env.NAVA_BASE_URL!;
type Query = Record<string, string | number | (string | number)[]>;
async function get(token: string, path: string, query: Query) {
const url = new URL(BASE + path);
for (const [key, value] of Object.entries(query)) {
// Arrays become repeated parameters: accommodationId=1000&accommodationId=1003
for (const item of Array.isArray(value) ? value : [value]) url.searchParams.append(key, String(item));
}
const res = await fetch(url, {
headers: { 'auth-token': token, 'Accept-Encoding': 'gzip' },
signal: AbortSignal.timeout(180_000),
});
const body = await res.json();
if (body?.auditData?.authToken) body.auditData.authToken = '[REDACTED]';
if (!res.ok) {
// Validation errors have no auditData, and static GETs send no Travelc-Trace-Id: log x-request-id too.
const traceId = body?.auditData?.traceId;
const requestId = res.headers.get('x-request-id');
throw new Error(`GET ${path} failed with HTTP ${res.status} (traceId ${traceId}, x-request-id ${requestId})`);
}
return body;
}
// 1. The hotel list, up to 20000 per page.
export async function* allAccommodations(token: string, pageSize = 20_000) {
for (let first = 0; ; first += pageSize) {
const page = await get(token, '/accommodations', { first, limit: pageSize });
yield* page.accommodations ?? [];
const p = page.pagination;
if (!p || !p.pageResults || first + p.pageResults >= p.totalResults) return;
}
}
// 2. Datasheets, at most 100 ids per call.
export async function datasheets(token: string, ids: string[], lang = 'EN') {
const out: any[] = [];
for (let i = 0; i < ids.length; i += 100) {
const batch = await get(token, '/accommodations/datasheet', { accommodationId: ids.slice(i, i + 100), lang });
out.push(...(batch.accommodations ?? []));
}
return out;
}
export async function weeklySync(token: string, languages = ['EN', 'AR']) {
const seen = new Set<string>();
const changed: string[] = [];
for await (const hotel of allAccommodations(token)) {
seen.add(hotel.id);
const stored = await catalogue.get(hotel.id);
// lastUpdate looks like "2022-10-30 18:36", so string comparison orders it.
if (!stored || hotel.lastUpdate > stored.lastUpdate) changed.push(hotel.id);
await catalogue.upsertSummary(hotel);
}
await catalogue.markInactiveExcept(seen); // soft delete, never hard delete
for (const lang of languages) {
await catalogue.saveDatasheets(lang, await datasheets(token, changed, lang)); // keep the raw JSON too
}
}Data model
countryCodein the list is ISO 3166-1 alpha-2.- The datasheet fields are defined in the spec as
IdeaHotelDataVO.
Images
- There is no thumbnail. Use
images[0], or choose by classification, for example a high-confidenceBuildingorPoolimage. - Images are full-size originals and can be more than 5000 px wide. Serve them through a resizing CDN instead of hot-linking the originals.
classification.typevalues differ between the spec and the documentation samples. The spec usesAerial ViewandHallway-Staircase; the samples showAERIAL_VIEWandHALLWAY_OR_STAIRCASE.
Normalise both sides before you compare: upper-case, replace every run of non-alphanumeric characters with _, then drop _OR_. Hallway-Staircase and HALLWAY_OR_STAIRCASE both become HALLWAY_STAIRCASE.
export const normaliseClassification = (type: string) =>
type.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/_OR_/g, '_');
normaliseClassification('Hallway-Staircase'); // "HALLWAY_STAIRCASE"
normaliseClassification('HALLWAY_OR_STAIRCASE'); // "HALLWAY_STAIRCASE"
normaliseClassification('Aerial View'); // "AERIAL_VIEW"Facilities
accommodationFacilities[] and roomFacilities[] each hold { id, icon, priority, translations }.
priorityis an integer: higher means more prominent. Sort by it, descending, for a "top amenities" list.iconis a CSS class, either Font Awesome (fa-regular fa-grill) or the platform's own (ico-tc-SPA). Map it to your own icon set.translationsmaps a language to the text. Pick the language you render.- Room facilities add a
category:GENERAL,ACTIVITIES,BATHROOM,MEDIA_AND_TECHNOLOGY,FOOD_AND_DRINK,INTERNET,KITCHEN,OUTDOORS,VIEW,LIVING_AREA,BEDROOMorEXCLUSIVE_SERVICES. - Whether a facility costs extra is not specified.
- The full catalogues come from
/facilites/accommodationand/facilites/room(sic), withlangrequired.
Categories
| Prefix | Meaning | Codes |
|---|---|---|
S | Stars | S1 to S6 |
L | Keys | L1 to L5 |
H | Suns | H1 to H5 |
IN | None or indeterminate | IN |
The codes are documented and defined in the spec's HotelCategory enum.
The datasheet gives category as a string ("S4"). The booking flow gives an object, { "code": "S4", "name": "4 STARS" }, with names such as "4 STARS" or "0 KEYS". Normalise both into one type.
type CategoryCode = string; // S1..S6, L1..L5, H1..H5, IN
export const categoryCode = (c: string | { code: string } | undefined): CategoryCode | undefined =>
typeof c === 'string' ? c : c?.code;Accommodation types and subtypes
accommodationTypeisHOTELorAPARTMENTin the spec. The documentation FAQ writes "Hotel or Apartement", so compare case-insensitively.accommodationSubtypehas 33 values, for exampleAPARTHOTELS,VILLAS,HOSTEL,RIADSandGUEST_HOUSES. Use it for finer filters. The full list is in Enums.
Ratings
Ratings come from three sources only: Booking.com, Tripadvisor and Expedia. Each entry is { source, numReviews, score }, and score is a string.
source | Scale |
|---|---|
| Booking.com | Out of 10 |
| Tripadvisor | Out of 5 |
| Expedia | Out of 5 |
Parse score to a number, keep the scale with it, and hide entries with numReviews: 0.
Destinations and search entry points
| The guest starts from | Do this |
|---|---|
| A destination | List destinations with GET /destination/{micrositeId}, then Quote with destinationId. |
| A map or radius | Filter your local catalogue by geolocation, then Quote with accommodations (at most 3000 codes within about 200 km of each other). |
| A hotel page | Quote single with the hotel's id. |
| An airport | GET /destination/{micrositeId}?iata=MAD, then Quote with destinationId. |
Pitfalls
- Hotels disappear. A hotel can drop out of
/accommodationswhen a provider disconnects. Soft-delete it and stop quoting it: quoting an id that vanished from the list returns nothing. Refresh weekly. - Catalogues are per microsite. Content depends on the microsite and credential, so two microsites can see different hotel sets. If a destination returns nothing, check the supplier connections with
GET /providers/configurations/{micrositeId}. - Don't cache quote prices in the catalogue. Rates are valid for about 1 hour, and
combinationKeys expire 40 to 60 minutes after the response that issued them. - Don't depend on legacy shapes.
facilitieswithincludedFacilities[]andincludedServices[]still appear in old samples. See facilities.
Related
- Accommodations and Datasheets: the list and datasheet references.
- Facilities and Meal plans: the lookup catalogues.
- Destinations: destination codes for Quote.
- The booking flow: where prices actually come from.
Reading bookings
Fetch a whole trip with getBookings, find its hotel services, passengers and money, and design a downstream sync that survives cancellations, rebookings and repricing.
Limits
Hard limits on rooms, guests, stay length and search size, the field rules the API validates, the clocks that run during a booking flow, and what the API does not support.