htl_* offer token you can hand to the trip tool to start a booking.
Tool description (what the LLM sees)
Tool description (what the LLM sees)
Search live hotel inventory and rates.
CONTINUATION:
- To load more from a prior result, send { next_handle } by itself: it continues the same search. Do not repeat destination, dates, occupancy, currency, filters, user_intent or trip_id.
FIRST SEARCH REQUIRED:
- destination: object, two distinct modes. Mode A (rate lookup): { hotel_name (+ optional country_code, city_name) } or { hotel_ids }. Mode B (hotel search): { query }, { city_name + country_code }, or { latitude + longitude (+ radius_km) }.
- checkin, checkout: the user's stay dates in YYYY-MM-DD. Check-in must be today or later; check-out must be after check-in.
OCCUPANCY (state it whenever the user gave a party):
- either { occupancies: [{ adults, children_ages? }, ...] } (one entry per room) OR shorthand { adults, children?, rooms? }, not both.
- When the user stated no party at all, you may search without one: the search then runs for 2 adults per room (1 room unless rooms says otherwise) and search_criteria.occupancy.source is "server_default". Tell the user the prices are for 2 adults in 1 room, and confirm the real party before trip(add_item), search again if it differs. A booking is priced for the party of the search.
TWO MODES, pick deliberately:
MODE A (rate lookup, the user named a specific hotel):
- { hotel_name }: free-text hotel name ("Hotel Calimala", "The St. Regis Rome", "Hôtel Costes"). The server matches the name against its hotel catalog. ALWAYS pair with country_code AND city_name when known, lookup precision drops sharply on common names without scope. Returns 422 HOTEL_NAME_LOW_CONFIDENCE if no candidate scores ≥ 0.7; see "ERROR HANDLING" below.
- { hotel_ids }: re-shop a known set (from a prior search result).
In Mode A: property filters (min_rating, min_star_rating, max_star_rating, min_reviews, hotel_type_ids, chain_ids, facility_ids) cannot be combined with hotel_name / hotel_ids: the request is rejected (the user named the property). filters.max_budget_per_night applies. filters.max_results does not limit Mode A: every named hotel that has rates is returned. The response includes nearby_alternatives, up to 40 hotels within ~3km of the matched property, in the same response shape so the user can compare.
MODE B (hotel search, the user is exploring a destination):
- { query }: a natural-language destination (city, POI, island or region). The platform parses it once into city + country or coordinates and applies shared location normalization; raw query text is never sent to a supplier.
- { city_name + country_code }: when the user named a city, even if ambiguous. Best when the destination has a primary city ("Mahón, ES" for Menorca; "Florence, IT" for Tuscany). Spell the city as an English-language booking site would, "Munich"/"Florence"/"Rome"/"Vienna", NOT "München"/"Firenze"/"Roma"/"Wien", but keep the local form where that IS the international one ("Regensburg", "Nürnberg", "Lyon"), and never an archaic exonym ("Leghorn", "Ratisbon"). When the user wrote the city in another language, translate it for this field only; keep their spelling when you talk back to them.
- { latitude + longitude + radius_km }: when exact coordinates and the desired area radius are already known. radius_km up to 50.
ERROR HANDLING, 422 HOTEL_NAME_LOW_CONFIDENCE (Mode A only):
When { hotel_name } fuzzy lookup finds no candidate ≥ 0.7, the response body is:
{ "error": { "code": "HOTEL_NAME_LOW_CONFIDENCE", "message": "...", "top_candidates": [{hotel_id, name, city, score}], "suggested_retry": { "destination": {...} } } }
This is ACTIONABLE, not fatal. Pick one:
1. Top candidate matches what the user meant (typo/spelling) → confirm with the user, then retry with { hotel_ids: ["<top.hotel_id>"] }.
2. None of the candidates fit → ask the user "I couldn't pin down 'X', want me to search all hotels in <city> instead?" then retry with suggested_retry.destination.
3. User likely meant a different city → ask to clarify, then retry hotel_name with corrected country_code/city_name.
Don't silently auto-pick a low-confidence candidate, booking the wrong hotel is worse than asking one clarifying question.
ERROR HANDLING, 422 DESTINATION_LOW_CONFIDENCE (Mode B city_name+country_code only):
When the city has no exact catalog match but close candidates exist, top_candidates are CITIES ({city, country_code, hotel_count, score}), not hotels: confirm an obvious typo and retry with that candidate's city_name + country_code (it resolves exactly), ask the user when several fit, and never report the city has no hotels.
destination_resolution comes back on every successful search and says HOW the destination resolved (exact / fuzzy / provider_matched / fallback_string); the text summary's opening "Searched: …" line, when present, means the match was fuzzy, fell back to a literal string, resolved to a different city than requested, or the platform located a place that is not a city (an island, region, district, landmark, station, address or airport). When destination_resolution.resolved_place is set, the platform located the place itself: tell the user that place, with its place_type when it is not a city, as where you searched. When destination_resolution.alternatives is set, the destination may name another place too (the same name in another country or state, the island rather than its town): name them, ask which one the user meant before booking, and search another by sending its latitude, longitude and radius_km.
SANITY-CHECK THE CITY GROUP (Mode B city_name, an "exact" match can still be the wrong group):
destination_resolution.catalog_hotel_count is how many properties the matched city group holds. A count in the single or low double digits for a city you would expect to be large does NOT mean the city is small, it means you matched a near-empty duplicate group. "Roma" holds 3 properties where "Rome" holds 20,706; "Firenze" holds 1 where "Florence" holds 6,986; "Wien" holds 7 where "Vienna" holds 4,580. Treat it as a DESTINATION problem: retry with the English-booking-site spelling, or with { latitude + longitude + radius_km }.
This overrides empty_reason. "no_availability_for_dates" is computed against the matched group alone, so a wrong-spelling match reports a sold-out city that actually has thousands of rooms, never tell the user a city is full, or that only N hotels exist there, on the strength of a small group.
FIRST SEARCH OPTIONAL:
- currency (ISO 4217), guest_nationality (ISO 3166-1 alpha-2)
- filters: { min_rating, min_star_rating, max_star_rating, min_reviews, hotel_type_ids, chain_ids, facility_ids, max_results, max_budget_per_night }
- filters.facility_ids / filters.hotel_type_ids take NAMES ("pool", "spa", "parking"; "hostel", "ryokan", "chalet"), the accepted names are listed on each parameter. For a facility the user requires, send that one name. A name outside the list is rejected with the accepted names. Every property filter is applied as sent.
- filters.max_budget_per_night: per-night per-room price cap (request currency) for "under $150/night" asks. Hotels whose CHEAPEST rate fits are kept with ALL their rates, on every page. Also applies in Mode A.
DESTINATION AND OCCUPANCY EXAMPLES:
Add checkin and checkout for the user's actual stay to each example below.
1. Free-text city search:
{ "destination": { "query": "Paris" }, "adults": 2 }
2. Two-room family with kids:
{ "destination": { "city_name": "Barcelona", "country_code": "es" },
"occupancies": [{ "adults": 2 }, { "adults": 1, "children_ages": [5, 7] }] }
3. Island/region with known coordinates:
{ "destination": { "latitude": 39.9496, "longitude": 4.1102, "radius_km": 30 }, "adults": 1 }
4. Geo-radius search around a POI:
{ "destination": { "latitude": 41.4036, "longitude": 2.1744, "radius_km": 3 }, "adults": 2 }
5. Mode A, user named a specific hotel:
{ "destination": { "hotel_name": "Hotel Calimala", "country_code": "it", "city_name": "Florence" }, "adults": 2, "currency": "EUR" }
RESPONSE:
- The text result is a summary, not JSON: hotels shown (with the total available when known), the price range and refundable split, then each hotel's stars, rating, cheapest price and hotel_id, and one line per rate ending in trip_item_token=<htl_* token>, with that rate's price, board and refund terms on the same line. Every rate token is listed.
- structuredContent carries the full payload: each hotel with basic metadata (name, address, star_rating, rating, main_photo) and a rooms[] list; each room has a rates[] list, and each rate has a trip_item_token (the same htl_* token), board type, refund policy, cancellation schedule, taxes, and total_amount_money. On a search of 2 or more rooms the rooms' rates[] are empty and the bookable offers are in the hotel's offers[]: each has room_rates[] naming the room it prices (occupancy_number, occupancy), one token per room, the same token repeated for identical rooms, or, when one offer covers every room, every room it books with its one token.
- When user_intent is given, hotels are ordered by how well each hotel matches the stated intent; a hotel may also carry recommendation_score (0 to 1; not a guest rating) and distance_km, its straight-line distance from the search center; the summary prints them on the hotel's "AI ranking" and distance lines. structuredContent also carries search metadata.
- structuredContent.search_criteria states what the prices were produced under, on every response (success, empty, error): destination (as sent, and resolved as the platform reported it), stay (checkin, checkout, nights), occupancy (rooms, adults, children_ages, source: request | server_default | session), currency (and whether it was defaulted), filters (requested, and as sent to the platform search, ids where the platform takes ids; every property filter is applied as sent), and on an empty page relax_first (what to change first, in order). The summary's "Priced for:" line repeats the party, currency and filters: read them back to the user with the prices.
- Empty page: do not repeat the same call. Change one thing per call in the order relax_first gives (the "Relax first:" line) before telling the user there is nothing.
- Short page: a "Coverage:" line says how many hotels have rates for the area and dates, e.g. 1 hotel. A short list is not a paging gap: widen the radius, shift the dates or try a nearby city. filters.offset is rejected once a search returned search_handle; page with next_handle instead.
- Error: the text names the retry policy, whether the SAME request is worth retrying once and after how many seconds (also structuredContent.retry_after_seconds when known), or whether the request has to change. One retry at most, never a loop; if it fails again, tell the user.
NEXT STEP: Use the trip tool with add_item and the trip_item_token (htl_* token) of the rate the user picks. Hotel items work alongside flight items in the same trip.
Flow: hotel_search → trip(add_item) → trip(upsert_travelers) → checkout
PRICES: rate prices, in the summary and in structuredContent, are totals for the stay.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
next_handle | string | null | No | Opaque continuation handle returned by a prior hotel_search. Send this field by itself, or with every other field null. A first request needs destination, checkin and checkout. |
destination | object | null | No | Destination, provide exactly one shape; unused fields may be null or left out. Two distinct modes: MODE A (rate lookup, you know which hotel): • { hotel_ids } to re-shop a known set. • { hotel_name, country_code?, city_name? } when the user named a specific property. Server resolves via fuzzy lookup; returns 422 HOTEL_NAME_LOW_CONFIDENCE if no candidate scores ≥ 0.7. MODE B (hotel search, you’re exploring): • { query } for a natural-language destination. The platform parses it once into city + country or coordinates and never forwards the raw query to a supplier. • { city_name + country_code } for ambiguous city names or when the user named a primary city (“Mahón, ES” for Menorca). • { latitude + longitude + radius_km } when exact coordinates and an area radius are already known. In Mode A, property filters (min_rating / star / min_reviews / facility / chain / hotel_type) cannot be combined with hotel_name / hotel_ids: the request is rejected, the user already named the property. filters.max_budget_per_night applies with hotel_ids or hotel_name. |
checkin | string | null | No | Check-in date (YYYY-MM-DD). |
checkout | unknown | null | No | Check-out date (YYYY-MM-DD). |
occupancies | array<object> | null | No | One entry per room (structured). PREFERRED whenever the party is larger than 2 adults or has children, it removes ambiguity about how guests are split across rooms. Example: [{ “adults”: 2 }, { “adults”: 1, “children_ages”: [5] }]. Do NOT combine with the shorthand fields (adults/children/rooms), use one or the other. |
adults | integer | null | No | Shorthand: total adults across all rooms. Use ONLY when there is no ambiguity (1 or 2 adults = single room). For 3+ adults, or odd splits, ask the user how they want to split rooms and pass occupancies[] instead, e.g. 4 adults → [{adults:2},{adults:2}] (double + double) vs a single 4-sleeper is a meaningfully different search. Without rooms or occupancies[], the server prices ONE room for all the adults (adults 4 alone is a single 4-sleeper, never two doubles) and search_criteria in the result says so; with rooms, it spreads the adults evenly across that many rooms. Do NOT combine with occupancies[], use one or the other. |
children | array<integer> | null | No | Shorthand: ages of all children across all rooms. If children are present, prefer occupancies[] so the caller controls which room each child goes in (ages affect pricing and some providers reject invalid age/room combinations). |
rooms | integer | null | No | Shorthand: number of rooms (the platform auto-distributes adults + children). Use when the user named a room count but not the per-room split. |
currency | string | null | No | ISO 4217 currency code (e.g. “EUR”, “USD”, “GBP”, “JPY”). ALWAYS set this, omitting it falls back to USD which is rarely what users actually want. Infer from the strongest signal available: (1) the user’s explicit ask (“show me prices in pounds”), (2) the conversation language/locale (French → EUR, Japanese → JPY, German → EUR, Spanish/Catalan/Italian/Portuguese → EUR, English UK → GBP, English US → USD), (3) the destination country (Italy/France/Germany → EUR, UK → GBP, Japan → JPY, US → USD). When in doubt between two plausible currencies, prefer the one matching the user’s likely home country. |
guest_nationality | string | null | No | Guest nationality (ISO 3166-1 alpha-2, uppercase, e.g. “FR”). Affects rate availability + tax handling at search time. Separate from traveler nationality used for booking documents. |
filters | object | null | No | Optional filter overrides applied on top of the tenant default filter set. Every property filter is applied as sent. |
trip_id | string | null | No | Existing trip_id. Accepted on every surface for schema consistency; this surface does not use it. |
user_intent | string | No | The end user’s current request in their own words, e.g. “help me with my travel plans”. Pass it as-is when short; otherwise condense the goal and constraints into 1-2 sentences. Strip personal identifiers (names, emails, phone numbers, addresses, payment details), replace them with placeholders like “[name]”. Optional. Jinko uses it to understand demand, and hotel_search uses it to rank hotel results against what the user asked for (e.g. “quiet, with a gym, near Times Square”), so keep the user’s stated preferences in it. |
Examples
Simple city search, 2 adults, 3 nights in Paris:{
"name": "hotel_search",
"arguments": {
"destination": { "query": "Paris" },
"checkin": "2027-07-15",
"checkout": "2027-07-18",
"adults": 2
}
}
{
"name": "hotel_search",
"arguments": {
"destination": { "city_name": "Barcelona", "country_code": "es" },
"checkin": "2027-08-01",
"checkout": "2027-08-05",
"occupancies": [
{ "adults": 2 },
{ "adults": 1, "children_ages": [5, 7] }
]
}
}
{
"name": "hotel_search",
"arguments": {
"destination": {
"latitude": 48.8566,
"longitude": 2.3522,
"radius_km": 3
},
"checkin": "2027-07-15",
"checkout": "2027-07-18",
"adults": 2,
"filters": {
"min_rating": 8,
"min_star_rating": 4,
"facility_ids": ["pool", "wifi"],
"max_results": 20
}
}
}
rooms[].rates[] includes a trip_item_token starting with htl_. Pass it to trip(add_item), hotels coexist with flight items in the same trip.
What a hotel result actually carries. Use these names, not the ones a generic hotel API would suggest:
{
"hotel_id": "lp1897c",
"name": "Hôtel Le Marais",
"address": "12 Rue de Rivoli",
"city": "Paris",
"country": "fr",
"star_rating": 4,
"rating": 8.6,
"review_count": 1243,
"main_photo": "https://…/large.jpg",
"thumbnail": "https://…/thumb.jpg",
"latitude": 48.8566,
"longitude": 2.3522,
"rooms": []
}
| Field | Note |
|---|---|
main_photo | The single hero image. There is no images[] array on a search result; use hotel_details for the full gallery. |
thumbnail | Small image. Not thumbnail_url. |
country | Lowercase ISO 3166-1 alpha-2, e.g. "fr". Not country_code, which is a request field on destination. |
