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

GET/api/v1/catalog/suggest

Turn what the traveller typed into an unambiguous place. Suggest returns cities, hotels and airports as they type.

Request · cURL
curl "https://api.roaveagents.com/api/v1/catalog/suggest?q=dubai" \
  -H "X-API-Key: $ROAVE_API_KEY"
Response · 200
{
  "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.

POST/api/v1/search

Describe the stay: dates, one occupancy entry per room, the currency you'll pay in, and the lead guest's passport country as residency.

Request · cURL
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"
  }'
Response · 201
{
  "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

GET/api/v1/search/:searchId/results

With 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.

Request · cURL
curl "https://api.roaveagents.com/api/v1/search/66f1c0a2e4b0c1d2e3f4a5b6/results?groupByProduct=true&limit=20" \
  -H "X-API-Key: $ROAVE_API_KEY"
Response · 200 (abridged)
{
  "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

POST/api/v1/search/:searchId/hotels/:productId/rates

When 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.

Request · cURL
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

POST/api/v1/quotes/:rateQuoteId/price-check

Run 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.

Request · cURL
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 '{}'
Response · 200 (abridged)
{
  "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

POST/api/v1/bookingsIdempotency-Key required

Send 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.

Request · cURL
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 }
    ]
  }'
Response · 201
{
  "data": {
    "bookingId": "66f1d2c4e4b0c1d2e3f4b010",
    "orderRef": "RV-2026-4CD5ADB2",
    "state": "confirmed"
  },
  "meta": {},
  "error": null
}

7. Confirm and fetch the voucher

POST/api/v1/bookings/:bookingId/refresh-status

If 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
// 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);