Cheapest Room
Get the cheapest room at one hotel on one OTA — its name, its price including taxes and fees, its meal plan and whether it is refundable — instead of every room type and every rate. It is the same scrape as per-hotel OTA pricing with a much smaller answer.
/api/v1/scraper/cheapest-room/— submit/api/v1/scraper/cheapest-room/{job_id}/— pollBase URL: https://api.scrapeguys.com · Auth: X-API-Key: gsk_… with the ota:<slug> scope of the OTA you name · See the Overview.
/cheapest-room/ is /ota-price/ with a smaller response, so it needs the same ota:<slug> scope, validates exactly as that endpoint does, and costs 1 unit per submit (billed even if the scrape fails; polling is free). Because it is the same job, the two endpoints read each other's work: a job submitted to /ota-price/ can be polled here, and /ota-price/{job_id}/ returns the full room matrix of a job submitted here, whose result.cheapest_room is this same block.Submit a job
The body is the /ota-price/ body: one hotel on one OTA for one stay. The fields that matter here are below; the Agoda guide is the full reference, and the Booking.com, MakeMyTrip and Expedia guides cover what differs per OTA.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
ota | string | Optional | agoda | agoda / booking / makemytrip (alias mmt) / expedia / goibibo, or the OTA's numeric channel code (Addressing an OTA by number; the live list is GET /otas/). An OTA switched off for the environment is rejected with 400. |
hotel_name | string | Conditional | — | ≤ 300 chars. Required, with city, unless place_id or ota_hotel_id is given. |
city | string | Conditional | — | ≤ 200 chars. Same rule as hotel_name. |
place_id | string | Optional | "" | Google Place ID — one key that identifies the property on every OTA. ≤ 200 chars. Don't have it? Look it up. |
ota_hotel_id | string | Optional | "" | That OTA's own property id (≤ 128 chars); skips resolution. A malformed id for the OTA is a 400. |
check_in | date | Required | — | YYYY-MM-DD. Not in the past; ≤ 1 year ahead. |
check_out | date | Required | — | After check_in; ≤ 30 nights. |
adults | int | Optional | 2 | 1–10. Party total across all rooms, not per room. |
rooms | int | Optional | 1 | 1–8. Must not exceed adults. |
children_ages | int[] | Optional | [] | One age per child, e.g. [6]. Ages 0–17, max 8. Accepted on Agoda, Booking.com, MakeMyTrip and Expedia; refused with 400 on an OTA that cannot price children (Goibibo). A bare children count is refused on every OTA. |
currency | string | Optional | INR | 3-letter ISO code, supported by the chosen OTA (MakeMyTrip is INR-only). The envelope's currency echoes your request; read cheapest_room.currency for the currency the price is actually quoted in. Unsupported codes are rejected with 400. |
device_type | string | Optional | desktop | desktop or mobile. Booking.com's app_android route (Booking.com guide) is also accepted. |
Errors — 400 (validation, an unsupported ota or currency, a bare children count, children_ages on an OTA that cannot price them, a malformed ota_hotel_id, rooms above adults, a stay over 30 nights, or "ota '…' is not currently enabled"), 401 (missing, unknown or deactivated key), 403 (key lacks ota:<slug> for the OTA named), 429 (per-minute limit, or daily / monthly quota exhausted). No job is created on any of these.
Poll for results
Poll the poll_url every ~2 seconds until status is completed or failed. Statuses: pending → processing → completed | failed.
/api/v1/scraper/cheapest-room/{job_id}/Errors — 404 ("Job not found." — unknown id, another client's job, or a Google Hotels scrape job, which polls on /scrape/), 401, 403 (key lacks the scope of the job's OTA).
Response schema
| Field | Type | Description |
|---|---|---|
id, status | string | The job and its state. |
ota | string | The canonical OTA name — never the number you may have sent. |
hotel_name | string | What the OTA itself calls the property; the name you submitted when the OTA named none (empty on a place_id-only job that never got an answer). |
hotel_id | string / null | The OTA's own property id; null until it is known. |
place_id, ota_hotel_id, check_in, check_out, adults, children_ages, rooms, currency | Your request, echoed back. | |
cheapest_room | object / null | The cheapest rate that seats your party, or null — reason says why. |
cheapest_room.room_name | string | The OTA's name for the room type. |
cheapest_room.price | number | One room, one night, including taxes and fees. A 2-room, 3-night stay at ₹5,000 + ₹900 tax a night reads 5900.0. On Booking.com, which quotes the whole stay, it is the stay's average night. Multiply by rooms and the number of nights for the booking total. |
cheapest_room.price_includes_taxes | bool | true whenever the OTA stated its taxes. false only when no rate that fits the party stated them; price is then that rate's own pre-tax figure — we never add a tax of our own. |
cheapest_room.currency | string | ISO code the price is in, read from the price itself. It is read from the price itself, so compare it with the envelope's currency if you need to confirm it matches what you asked for. |
cheapest_room.meal_plan, meal_plan_code | string / null | "Room only" EP, "Breakfast included" CP, "Half board" MAP, "Full board" AP, "All inclusive" AI. null when the OTA said nothing about meals. |
cheapest_room.refundable | bool / null | true refundable, false non-refundable, null when the OTA stated no policy. |
cheapest_room.free_cancellation | bool / null | true free cancellation (the deadline is in cancellation_text or free_cancellation_until); false with refundable: true means refundable with a penalty; null when no policy was stated. |
cheapest_room.cancellation_text | string / null | The OTA's own wording of the policy. |
cheapest_room.free_cancellation_until | string / null | When free cancellation ends, as an ISO 8601 UTC timestamp (e.g. "2026-11-25T18:30:00Z"). Set only where the OTA states a timestamp (Booking.com answered from search data); null otherwise. |
cheapest_room.price_before_taxes | number / null | The same rate before taxes and fees, on the same footing as price. null when the OTA quoted only a tax-inclusive price with no figure for its tax. Equals price when price_includes_taxes is false. |
cheapest_room.taxes_and_fees | number / null | What the OTA adds on top of price_before_taxes: taxes and fees together, so price_before_taxes + taxes_and_fees = price. Read from the same answer as the price, so it costs no extra request. null when the OTA stated no figure. |
cheapest_room.tax_amount, fee_amount | number / null | The tax alone and the charge beside it (extra bed, service fee), one room, one night — only when the job sent tax_required and the OTA itemized the rate (Agoda, Expedia, Goibibo); tax_amount + fee_amount = taxes_and_fees. null otherwise. |
cheapest_room.tax_percentage | number / null | The OTA's own stated tax rate for this rate (Agoda with tax_required); never computed by us. null otherwise. |
cheapest_room.tax_basis | string / null | tax_only — taxes_and_fees is only tax; tax_and_fees — something else is inside it; unknown — a figure was stated but not its parts; null — no tax stated. |
unavailable_reason | string | The OTA's own words for why there is nothing to sell, when it gave any (e.g. "Property requires a minimum stay of 2 nights"). "" when it stated none, and on jobs priced before 27 October 2026. |
sold_out | bool | true when the property resolved and has no availability for the dates. It reads false in every other state — including a job that is still running or has failed — so read status and reason first. |
reason | string / null | Why cheapest_room is null; null whenever it is not. See below. |
error_message | string | Empty unless the job failed. |
Which room
Of the rates the OTA quoted for your search, the one that costs least after taxes and fees among those that seat your party: ceil(adults / rooms) adults and ceil(children / rooms) children in one room, the same fit test as cheapest_fitting_price on the full matrix.
- Ranked on the all-in, not the pre-tax price. Tax is not a flat share of the price (India's hotel GST steps up above ₹7,500 a night), so the pre-tax order can name a rate that costs more once tax is on. It can therefore be a different rate from
cheapest_fitting_price. - Only stated taxes are added. A rate whose tax the OTA did not state cannot be compared, so it is not ranked against the rest. Only when no fitting rate states one does the cheapest of them stand in — by its pre-tax price, with
price_includes_taxes: false. - A bundle of rooms is never one room. Agoda can offer a package ("2 X Suite") to a party one room cannot seat; its price covers all of them, so it never counts as the room asked for.
- MakeMyTrip answers a party that no single room type matches with whole-booking combinations. A combination is priced per room as its share of the booking, and counts only when it was priced for exactly your adults, rooms and child ages — MakeMyTrip often re-splits a childful party into more rooms or re-prices the children as 12-year-olds, and such a combination is never used.
- Booking.com prices any number of rooms as that many of the one-room rate.
Faster answers from search data
For common searches, Cheapest Room answers from a lighter source than the OTA's full room list and falls back to the full route whenever the lighter one cannot give the same answer. The response shape is the same. Eligible jobs are one room, adults only, desktop; MakeMyTrip and Agoda also need INR, and Agoda jobs must not send tax_required.
- Booking.com — answered from Booking's search data.
cancellation_textis worded as the hotel page words it, from the free-cancellation deadline in the property's local time. When search names no meal,meal_planis"Room only"(EP). - MakeMyTrip — answered from the hotel's card on the search listing.
cancellation_textmay omit the deadline ("Free Cancellation"), and on stays of several nightspriceis rounded to the whole rupee. - Agoda — answered from Agoda's search results for the hotel. When two room types tie on price, the room named may be the other of the two.
The first request for a hotel on MakeMyTrip or Agoda is always answered from the full room list. In these cases the job's /ota-price/<job_id>/ view has result.source: "search" and holds only the one rate, not the full room matrix.
When cheapest_room is null
reason | Meaning |
|---|---|
pending | The job has not finished — poll again. |
failed | The scrape failed; see error_message. The unit is still charged. |
sold_out | The property has no availability for the dates. unavailable_reason adds the OTA's own wording when it gave any; a minimum-stay or date-window refusal means these dates will not price, where a plain sold-out may. |
no_room_fits_party | The job completed and is not sold out, but no rate is confirmed to seat the party with a usable price — including when no rates came back at all. |
GET /api/v1/scraper/ota-price/{job_id}/ (see Agoda for the schema). To price many hotels across many OTAs and dates in one request, use Rate Shop. For Google's multi-OTA comparison, see Google Hotels.Next: Destination Search →