Scraper · Agoda / Booking.com / MakeMyTrip / Expedia

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.

POST/api/v1/scraper/cheapest-room/— submit
GET/api/v1/scraper/cheapest-room/{job_id}/— poll

Base URL: https://api.scrapeguys.com · Auth: X-API-Key: gsk_… with the ota:<slug> scope of the OTA you name · See the Overview.

No scope of its own, and one unit like any OTA job. /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.

FieldTypeRequiredDefaultNotes
otastringOptionalagodaagoda / 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_namestringConditional—≤ 300 chars. Required, with city, unless place_id or ota_hotel_id is given.
citystringConditional—≤ 200 chars. Same rule as hotel_name.
place_idstringOptional""Google Place ID — one key that identifies the property on every OTA. ≤ 200 chars. Don't have it? Look it up.
ota_hotel_idstringOptional""That OTA's own property id (≤ 128 chars); skips resolution. A malformed id for the OTA is a 400.
check_indateRequired—YYYY-MM-DD. Not in the past; ≤ 1 year ahead.
check_outdateRequired—After check_in; ≤ 30 nights.
adultsintOptional21–10. Party total across all rooms, not per room.
roomsintOptional11–8. Must not exceed adults.
children_agesint[]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.
currencystringOptionalINR3-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_typestringOptionaldesktopdesktop or mobile. Booking.com's app_android route (Booking.com guide) is also accepted.
POST /api/v1/scraper/cheapest-room/
curl -X POST https://api.scrapeguys.com/api/v1/scraper/cheapest-room/ \ -H "X-API-Key: gsk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "ota": "agoda", "hotel_name": "The Taj Mahal Palace", "city": "Mumbai", "check_in": "2026-11-10", "check_out": "2026-11-12", "adults": 2, "rooms": 1 }'
202 Accepted
{ "job_id": "3f6c1a2b-...", "status": "pending", "ota": "agoda", "ota_hotel_id": "", "place_id": "", "poll_url": "/api/v1/scraper/cheapest-room/3f6c1a2b-.../" }

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.

GET/api/v1/scraper/cheapest-room/{job_id}/
cheapest.py
import requests, time API = "https://api.scrapeguys.com/api/v1/scraper" H = {"X-API-Key": "gsk_your_api_key"} job = requests.post(f"{API}/cheapest-room/", headers=H, json={ "ota": "agoda", "hotel_name": "The Taj Mahal Palace", "city": "Mumbai", "check_in": "2026-11-10", "check_out": "2026-11-12", }).json() while True: res = requests.get(f"{API}/cheapest-room/{job['job_id']}/", headers=H).json() if res["status"] in ("completed", "failed"): break time.sleep(2) room = res["cheapest_room"] if room: print(room["room_name"], room["price"], room["currency"]) else: print("no cheapest room:", res["reason"])

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

200 OK — GET /cheapest-room/{job_id}/
{ "id": "3f6c1a2b-...", "status": "completed", "ota": "agoda", "hotel_name": "The Taj Mahal Palace, Mumbai", "hotel_id": "16082488", "place_id": "", "ota_hotel_id": "", "check_in": "2026-11-10", "check_out": "2026-11-12", "adults": 2, "children_ages": [], "rooms": 1, "currency": "INR", "cheapest_room": { "room_name": "Luxury Room", "price": 24190.0, "price_includes_taxes": true, "currency": "INR", "meal_plan": "Breakfast included", "meal_plan_code": "CP", "refundable": true, "free_cancellation": true, "cancellation_text": "Free Cancellation before 8 Nov 01:59 PM", "free_cancellation_until": null, "price_before_taxes": 20500.0, "taxes_and_fees": 3690.0, "tax_amount": null, "fee_amount": null, "tax_percentage": null, "tax_basis": "unknown" }, "sold_out": false, "reason": null, "error_message": "" }
FieldTypeDescription
id, statusstringThe job and its state.
otastringThe canonical OTA name — never the number you may have sent.
hotel_namestringWhat 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_idstring / nullThe OTA's own property id; null until it is known.
place_id, ota_hotel_id, check_in, check_out, adults, children_ages, rooms, currencyYour request, echoed back.
cheapest_roomobject / nullThe cheapest rate that seats your party, or null — reason says why.
cheapest_room.room_namestringThe OTA's name for the room type.
cheapest_room.pricenumberOne 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_taxesbooltrue 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.currencystringISO 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_codestring / 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.refundablebool / nulltrue refundable, false non-refundable, null when the OTA stated no policy.
cheapest_room.free_cancellationbool / nulltrue 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_textstring / nullThe OTA's own wording of the policy.
cheapest_room.free_cancellation_untilstring / nullWhen 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_taxesnumber / nullThe 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_feesnumber / nullWhat 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_amountnumber / nullThe 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_percentagenumber / nullThe OTA's own stated tax rate for this rate (Agoda with tax_required); never computed by us. null otherwise.
cheapest_room.tax_basisstring / nulltax_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_reasonstringThe 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_outbooltrue 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.
reasonstring / nullWhy cheapest_room is null; null whenever it is not. See below.
error_messagestringEmpty 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.

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.

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

reasonMeaning
pendingThe job has not finished — poll again.
failedThe scrape failed; see error_message. The unit is still charged.
sold_outThe 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_partyThe 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.
Need every room, or many hotels? The full room-by-rate matrix of this very job is at 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 →