Hotel guides
Following search best practices
A good search integration asks precise questions, shows results as they arrive, and books inside the quote's lifetime. These practices get travellers accurate prices quickly and keep bookings from failing at checkout.
Resolve the destination before you search
Free text is slow and ambiguous. “Springfield” matches dozens of cities, and a search that has to guess either returns the wrong one or returns nothing. Resolve the place with GET /catalog/suggest as the traveller types, then search with IDs.
{
"productType": "hotel",
"destination": "Springfield"
}{
"productType": "hotel",
"destination": "Dubai",
"destinationCountryCode": "AE",
"destinationCityRegionId": 6053839
}- A city: send its
cityRegionIdasdestinationCityRegionId, plusdestinationCountryCode. Some cities come back with several region IDs incityRegionIds(a city and its surrounding area); send them all asdestinationCityRegionIdsto cover the whole area. - One hotel: send its
canonicalIdashotelId. Only that property is priced, so the search is much faster. - Near a point: send
nearwithlat,lngand aradiusKmbetween 0.5 and 50.
{
"productType": "hotel",
"destination": "Dubai",
"hotelId": "c9b1e2f4a7d34b0e",
"checkIn": "2026-11-10",
"checkOut": "2026-11-13",
"occupancy": [{ "adults": 2, "childrenAges": [] }],
"currency": "USD",
"market": "GB",
"residency": "GB"
}Search in the currency you'll pay in
Bookings are paid from your agency wallet, and the wallet doesn't convert between currencies. A credit booking whose currency differs from your wallet fails with 409 wallet_currency_mismatch. Search doesn't convert prices: hotel rates that aren't in the currency you searched in are left out of your results, while flight, activity and transfer results come back in the inventory's own currency — so check each row's sellCurrency and hide the ones that don't match.
Set currency to your wallet currency on every search. If you show travellers another currency, convert for display on your side and keep settling in the wallet currency.
Describe the real party
Price depends on who is travelling, and the booking must match the search exactly. Get these right up front rather than discovering the problem at checkout:
- One
occupancyentry per room. Two rooms is two entries, never one entry with four adults. - Every child's age in
childrenAges, as of the check-in date. Send[]when there are none. Many hotels price children by age band, and a wrong age can change the room. residencyis the lead guest's passport country — not where your agency is, and not where the traveller lives. Rates can differ by nationality, and the hotel may check the passport at check-in.marketis your point of sale — the country you're selling in.
"occupancy": [
{ "adults": 2, "childrenAges": [] },
{ "adults": 1, "childrenAges": [4, 9] }
]| Hotel search limit | Value |
|---|---|
| Rooms per search | 9 |
| Adults per room | 6 |
| Children per room | 4, aged 0–17 |
| Length of stay | 30 nights |
| How far ahead | 730 days |
Show results as they arrive
A hotel search fans out to many sources at once, and they answer at different speeds. POST /search returns as soon as the first batch is priced, or after about 4 seconds if that takes longer; the rest arrives in the background. Don't make travellers wait for the slowest source.
- Call
GET /search/:id/resultsstraight after the search and render what's there. - While
meta.session.statusispartial, poll every 1–2 seconds.meta.session.batchProgresstells you how many batches are done if you want a progress bar. - When
meta.session.lateArrivalsgoes up, rates from a slower source have been added. Re-read from the first page: a cursor never revisits earlier rows. - Stop at
completedorfailed, or after 90 seconds, whichever comes first. A search can still bepartialat 90 seconds; show what you have.
async function waitForResults(searchId) {
const deadline = Date.now() + 90_000;
let seenLate = 0;
while (Date.now() < deadline) {
const { data, meta } = await call('GET', '/search/' + searchId + '/results?groupByProduct=true&limit=50');
const late = meta.session.lateArrivals ?? 0;
render(data, { reset: late > seenLate }); // late arrivals: re-read from page one
seenLate = late;
if (meta.session.status === 'completed' || meta.session.status === 'failed') return;
await sleep(1500);
}
}Results are limited to 300 requests a minute per key — plenty for one poll every 1.5 seconds across several concurrent searches. Flight, activity and transfer searches finish before POST /search returns, so their first read is already complete.
Treat listing prices as “from” prices
Results carry each hotel's cheapest rates, which is right for a listing page but isn't every room. Group result rows by canonicalProductId and show the first row you meet for each hotel as its “from” price. When a traveller opens a hotel, fetch its full rate sheet with POST /search/:id/hotels/:productId/rates and let them choose from that.
- Only fetch a rate sheet when a hotel is actually opened. It queries live inventory for that hotel, can take up to about 25 seconds, and is limited to 60 a minute per key.
- Rates are ranked for you: cheapest first, and when two rates are within 2% of each other the refundable one ranks higher. In search results this ranking applies within each batch of hotels; rows from later batches and slower sources come after.
- Show the room name, board, cancellation terms and any
excludedTaxes(taxes paid at the hotel) for every rate you list.
Page through results; don't search again
A new search is slower, counts against a tighter limit (30 a minute), and can return different prices. For the next page, sorting or filtering, work from the search you already have:
- Page with
limit(up to 500) andmeta.nextCursor. - Results arrive ranked. If you need a different order or filters — star rating, board, refundable only — fetch the rows once and sort or filter them on your side.
- Only search again when the question changes: new dates, destination or party.
Mind the quote clock
Prices are only good for a short time. Two clocks run, and a booking must beat both:
- Searches and their quotes live 20 minutes. After that, rate sheets return
410 search_expiredand price-checks410 quote_expired. Opening a rate sheet extends the search and returns fresh quotes with their own 20 minutes — but quote IDs you already had keep their original expiry, so always price-check a rate from the latest sheet. - A price-check resets that quote's expiry to 20 minutes from the check and opens a 10-minute window to book. Book later and you get
409 quote_not_price_checked; price-check again first.
If the traveller comes back after a break, run the search again rather than trying to revive an old one.
Price-check once, at checkout
A price-check asks the inventory source for the live price, so it is slower than reading results and it is the step that catches sold-out rooms. Run it once, when the traveller commits to a rate — not on every page view, and not for every rate on the sheet. Then book straight away. If the price or terms moved, follow Handling price changes.
Cache content, not prices
- Don't cache prices or availability beyond the 20-minute search. They move constantly, and a stale price is a failed booking waiting to happen.
- Do cache hotel content — descriptions, photos, amenities — from
GET /products/hotel/:id. It changes rarely; refreshing it daily is plenty. UseGET /products/hotel-thumbnailsto fetch up to 60 listing images in one call.
Check what's on sale in each market
Products can be switched on or off per market. Ask once per market when your app starts, and cache the answer for the session, instead of letting a search fail.
curl "https://api.roaveagents.com/api/v1/search/capabilities?market=AE" \
-H "X-API-Key: $ROAVE_API_KEY"Checklist
- Destinations are resolved with suggest, and searches send region or hotel IDs.
currencyis always your wallet currency, and non-hotel rows in any othersellCurrencyare hidden.- One
occupancyentry per room, with every child's age. residencyis the lead guest's passport country.- Results render immediately and update while the search is
partial. - Rate sheets load only when a hotel is opened.
- Paging uses cursors; sorting and filtering happen client-side.
- Price-check runs once, at checkout, and booking follows within 10 minutes.
- Prices are never cached beyond the search; content is.