Hotel guides
Quickstart: book a hotel
Seven calls take you from a city name to a confirmed hotel booking. Each step shows the request, the response, and what to carry forward.
1. Find the destination
/api/v1/catalog/suggestTurn what the traveller typed into an unambiguous place. Suggest returns cities, hotels and airports as they type.
curl "https://api.roaveagents.com/api/v1/catalog/suggest?q=dubai" \
-H "X-API-Key: $ROAVE_API_KEY"{
"data": {
"query": "dubai",
"cities": [
{
"type": "city",
"name": "Dubai",
"countryCode": "AE",
"hotelCount": 4210,
"cityRegionId": 6053839,
"cityRegionIds": [6053839]
}
],
"hotels": [],
"pois": [
{ "type": "poi", "kind": "airport", "name": "Dubai International", "code": "DXB", "countryCode": "AE" }
]
},
"meta": {},
"error": null
}Carry forward the city's cityRegionId and countryCode. If the traveller picked a hotel instead, carry its canonicalId and send it as hotelId.
2. Search
/api/v1/searchDescribe the stay: dates, one occupancy entry per room, the currency you'll pay in, and the lead guest's passport country as residency.
curl -X POST "https://api.roaveagents.com/api/v1/search" \
-H "X-API-Key: $ROAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"productType": "hotel",
"destination": "Dubai",
"destinationCountryCode": "AE",
"destinationCityRegionId": 6053839,
"checkIn": "2026-11-10",
"checkOut": "2026-11-13",
"occupancy": [{ "adults": 2, "childrenAges": [7] }],
"currency": "USD",
"market": "GB",
"residency": "GB"
}'{
"data": { "searchId": "66f1c0a2e4b0c1d2e3f4a5b6" },
"meta": {},
"error": null
}For hotels the call returns once the first batch of properties is priced, usually within a few seconds. More results keep arriving in the background.
3. Read results
/api/v1/search/:searchId/resultsWith groupByProduct=true the page is counted in hotels: limit=20 gives you 20 hotels, and each can contribute several rate rows. Group rows by canonicalProductId; the first row you meet for each hotel is its top-ranked rate — the “from” price for your listing page.
curl "https://api.roaveagents.com/api/v1/search/66f1c0a2e4b0c1d2e3f4a5b6/results?groupByProduct=true&limit=20" \
-H "X-API-Key: $ROAVE_API_KEY"{
"data": [
{
"_id": "66f1c0b8e4b0c1d2e3f4a600",
"productType": "hotel",
"canonicalProductId": "c9b1e2f4a7d34b0e",
"rateQuoteId": "66f1c0b8e4b0c1d2e3f4a601",
"sellAmountMinor": 48230,
"sellCurrency": "USD",
"refundable": true,
"summary": {
"hotelName": "Example Marina Hotel",
"starRating": 5,
"cityName": "Dubai",
"roomName": "Deluxe King Room",
"boardBasis": "Breakfast included",
"cancellationPolicy": {
"refundable": true,
"freeCancelBefore": "2026-11-08T12:00:00Z",
"deadlines": [{ "before": "2026-11-08T12:00:00Z", "penaltyMinor": 16077, "currency": "USD" }]
},
"excludedTaxes": [],
"isPackage": false
}
}
],
"meta": {
"nextCursor": "MjA",
"session": { "status": "partial", "batchProgress": { "done": 1, "total": 3 } }
},
"error": null
}While meta.session.status is partial, more hotels are still being priced. Poll every one to two seconds until it is completed. Search best practices covers polling in depth.
4. Open the rate sheet
/api/v1/search/:searchId/hotels/:productId/ratesWhen the traveller opens a hotel, fetch every room and rate for it. This queries live inventory for that one hotel and returns an array of rows in the same shape as results.
curl -X POST "https://api.roaveagents.com/api/v1/search/66f1c0a2e4b0c1d2e3f4a5b6/hotels/c9b1e2f4a7d34b0e/rates" \
-H "X-API-Key: $ROAVE_API_KEY"Each row is one bookable room-and-rate combination. Carry forward the chosen row's rateQuoteId — that is the ID you price-check and book.
5. Price-check
/api/v1/quotes/:rateQuoteId/price-checkRun this once, when the traveller reaches checkout. It confirms the rate is still available, returns the final price and cancellation terms, and starts a 10-minute window to book.
curl -X POST "https://api.roaveagents.com/api/v1/quotes/66f1c0b8e4b0c1d2e3f4a7a2/price-check" \
-H "X-API-Key: $ROAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'{
"data": {
"quoteId": "66f1c0b8e4b0c1d2e3f4a7a2",
"priceChanged": false,
"policyChanged": false,
"breakdown": {
"sell": { "amountMinor": 51120, "currency": "USD" },
"taxesAndFees": { "amountMinor": 6240, "currency": "USD" }
},
"quote": {
"_id": "66f1c0b8e4b0c1d2e3f4a7a2",
"status": "price_checked",
"expiresAt": "2026-09-10T12:26:00Z",
"cancellationPolicy": { "refundable": true, "freeCancelBefore": "2026-11-08T12:00:00Z", "deadlines": [] },
"details": { "rateComments": "Check-in from 15:00. Photo ID required." }
},
"agencyCommission": null
},
"meta": {},
"error": null
}If priceChanged or policyChanged is true, show the traveller what changed before you book. See Handling price changes.
6. Book
/api/v1/bookingsSend one traveller per guest you searched for, grouped into rooms by roomIndex. The first traveller is the lead guest and needs an email and phone number. Send a title for every guest: it's required when the rate or quote has requiresGuestTitle: true, and the booking is refused with 400 traveler_title_required without one. See Titles.
curl -X POST "https://api.roaveagents.com/api/v1/bookings" \
-H "X-API-Key: $ROAVE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5c0e8a4e-2b7f-4f3e-9d61-0a8c7b1f2e34" \
-d '{
"quoteId": "66f1c0b8e4b0c1d2e3f4a7a2",
"paymentMethod": "credit",
"travelers": [
{ "title": "Ms", "firstName": "Anna", "lastName": "Smith", "email": "anna.smith@example.com", "phone": "+447700900123", "nationality": "GB", "roomIndex": 0 },
{ "title": "Mr", "firstName": "Ben", "lastName": "Smith", "roomIndex": 0 },
{ "title": "Miss", "firstName": "Cara", "lastName": "Smith", "type": "child", "age": 7, "roomIndex": 0 }
]
}'{
"data": {
"bookingId": "66f1d2c4e4b0c1d2e3f4b010",
"orderRef": "RV-2026-4CD5ADB2",
"state": "confirmed"
},
"meta": {},
"error": null
}7. Confirm and fetch the voucher
/api/v1/bookings/:bookingId/refresh-statusIf the state is supplier_pending, check again every 15 seconds. Once it is confirmed, read the full booking with GET /bookings/:bookingId and the voucher with GET /bookings/:bookingId/voucher. See Booking statuses and Vouchers.
The whole flow in one script
The same seven steps in plain Node.js with no dependencies. Change the dates, then run it with a key for the environment you're testing against.
// quickstart.mjs — Node 18 or later. Run: ROAVE_API_KEY=rk_live_... node quickstart.mjs
import { randomUUID } from 'node:crypto';
const BASE = 'https://api.roaveagents.com/api/v1';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function call(method, path, body, extraHeaders = {}) {
const res = await fetch(BASE + path, {
method,
headers: { 'X-API-Key': process.env.ROAVE_API_KEY, 'Content-Type': 'application/json', ...extraHeaders },
body: body === undefined ? undefined : JSON.stringify(body),
});
const { data, meta, error } = await res.json();
if (error) throw new Error(res.status + ' ' + error.code + ': ' + error.message);
return { data, meta };
}
// 1. Resolve the destination to a city region
const { data: suggest } = await call('GET', '/catalog/suggest?q=dubai');
const city = suggest.cities[0];
// 2. Search
const { data: search } = await call('POST', '/search', {
productType: 'hotel',
destination: city.name,
destinationCountryCode: city.countryCode,
destinationCityRegionId: city.cityRegionId,
checkIn: '2026-11-10',
checkOut: '2026-11-13',
occupancy: [{ adults: 2, childrenAges: [] }],
currency: 'USD', // your wallet currency
market: 'GB',
residency: 'GB', // lead guest's passport country
});
// 3. Poll the first page until every source has answered (or 90 s pass)
let page;
for (let waited = 0; waited < 90_000; waited += 1500) {
page = await call('GET', '/search/' + search.searchId + '/results?groupByProduct=true&limit=20');
if (page.meta.session.status !== 'partial' && page.meta.session.status !== 'pending') break;
await sleep(1500);
}
const hotel = page.data[0];
// 4. Open that hotel's full rate sheet and prefer a refundable room
const { data: rates } = await call('POST', '/search/' + search.searchId + '/hotels/' + hotel.canonicalProductId + '/rates');
const rate = rates.find((r) => r.refundable) ?? rates[0];
// 5. Price-check right before checkout
const { data: check } = await call('POST', '/quotes/' + rate.rateQuoteId + '/price-check', {});
if (check.priceChanged || check.policyChanged) {
// Never accept changed terms on the traveller's behalf. In a real app, show
// check.changes, and only book — with changesAcknowledged: true — once they accept.
console.log('Terms changed — ask the traveller before booking:', JSON.stringify(check.changes));
process.exit(1);
}
// 6. Book, paying from agency credit
const { data: booking } = await call('POST', '/bookings', {
quoteId: rate.rateQuoteId,
paymentMethod: 'credit',
travelers: [
{ title: 'Ms', firstName: 'Anna', lastName: 'Smith', email: 'anna.smith@example.com', phone: '+447700900123', roomIndex: 0 },
{ title: 'Mr', firstName: 'Ben', lastName: 'Smith', roomIndex: 0 },
],
}, { 'Idempotency-Key': randomUUID() });
// 7. Wait for the hotel to confirm if it hasn't already
let state = booking.state;
for (let tries = 0; state === 'supplier_pending' && tries < 40; tries++) {
await sleep(15_000);
({ data: { state } } = await call('POST', '/bookings/' + booking.bookingId + '/refresh-status'));
}
console.log(booking.orderRef, state);