Destination Search
Enumerate every bookable property in a destination for one stay, each with its cheapest room's price — the other axis from Booking.com per-hotel pricing, which prices one hotel you already have. Here you name a place and get back the whole market. Available for Booking.com, and for MakeMyTrip on request.
/api/v1/scraper/destination-search/— submit/api/v1/scraper/destination-search/{job_id}/— pollBase URL: https://api.scrapeguys.com · Auth: X-API-Key: gsk_… with the destination:booking scope (destination:makemytrip for "ota": "makemytrip") · See the Overview.
destination:booking is a separate scope from ota:booking, not implied by it. Pricing one hotel and enumerating an entire city are different products with unit costs orders of magnitude apart, so a key granted ota:booking (for the per-hotel guide) does not thereby get city sweeps. This scope post-dates every key issued so far and is not on any existing key, including full-access ones — ask your ScrapeGuys contact to add it before your first call.Submit a job
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
destination | string | Required | — | ≤ 200 chars. Free text — a city, region ("Goa"), district or landmark. Booking's own search box resolves it, and what it resolved to comes back on the job. An area query ("Indiranagar, Bengaluru") resolves to the area, not to a single hotel inside it; a query that names a hotel stays on the hotel. |
check_in | date | Required | — | YYYY-MM-DD. Not in the past; ≤ 1 year ahead. |
check_out | date | Required | — | After check_in; ≤ 30 nights. |
ota | string | Optional | booking | booking, or makemytrip (alias mmt) where enabled for your account — see MakeMyTrip, which differs in several fields. |
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. [5, 9] — not a count. Ages 0–17, max 10 children. Booking prices on the child's age: one child in a test city returned 383 / 405 / 417 available properties at ages 2 / 8 / 15, against 465 with none. |
currency | string | Optional | INR | One of INR, USD, GBP, EUR, AED. |
star_ratings | int[] | Optional | [] | Narrow the sweep to these property ratings, e.g. [4, 5]. Each value 1–5, at most 5 entries; anything else is rejected with 400. Omit for the whole destination — see Cost control below. |
max_properties | int | Optional | 500 | How many properties to return — a multiple of 100 from 100 to 1,000; anything else is rejected with 400. Omitting it still caps the search at 500 — there is no uncapped search. This is what makes the cost fixed — see Fixed cost below. |
sort | string | Optional | distance_from_search | Which properties the first max_properties are: distance_from_search (nearest the centre first), bayesian_review_score (best reviewed), class_asc (star class) or upsort_bh. |
children (a bare count) is rejected with 400. Use children_ages instead — a count with no ages would silently price a child at an age nobody stated. Booking.com per-hotel pricing now supports children_ages too — the age bounds differ slightly (max 10 children here, vs 8 on the per-hotel endpoint).max_properties / 100 pages from one walk and stops, so the default (500) is 5 pages. Work is bounded by construction rather than by a timeout, so a search cannot run away, but it can come back incomplete when the destination holds more than max_properties — which the result says explicitly (see Response schema below)./usage/'s in_flight and counts against a Rate Shop's up-front quota check; it is billed when the job finishes — a failed sweep is billed too, because the proxy bandwidth was spent either way. The per-minute rate limit is charged once, at submit, and only after the request has passed validation and the scope check.Errors — 400 (validation, an unsupported ota, a bare children count, a star_ratings value outside 1–5, a max_properties that is not a multiple of 100 from 100 to 1,000, a sort outside the four supported orders, a currency the OTA cannot be asked for, a stay over 30 nights, or "destination search is not currently enabled" when the feature is switched off for the environment), 401 (missing, unknown or deactivated key), 403 (key lacks destination:booking, or destination:makemytrip for a MakeMyTrip request), 429 (per-minute rate limit, or daily / monthly quota exhausted). No job is created on any of these.
Fixed cost — max_properties
A search reads max_properties / 100 pages from one walk of Booking's result list, and stops. It never cuts the destination into price bands and never re-reads Booking's own count, so the amount of work is bounded by the number you asked for, not by how big the destination turns out to be.
max_properties | Pages | Typical result |
|---|---|---|
| 100 | 1 | Goa: 100 of ~3,100 properties |
| 500 (default) | 5 | Goa: ~480 of ~3,100 properties |
| 1,000 | 10 | Dubai: ~1,000 of ~9,400 properties |
Up to max_properties, not exactly. On a large destination a few properties reappear on a later page and duplicates are removed, so a cap of 500 can come back with slightly fewer distinct properties. The search does not fetch another page to make up the difference — that page is exactly the variable cost the cap exists to remove. property_count says how many you got.
Always capped, default 500. There is no uncapped option: a request without max_properties runs as "max_properties": 500, so every request's cost is known before it runs.
Whole pages only. Booking serves 100 rows a page, and a page is what crosses the wire, so a cap of 250 would be billed as 300 with 50 thrown away. It is refused rather than rounded, and so is anything above 1,000, which one walk cannot reach.
The answer is the first N in sort order, not a sample of the city. The default returns the properties nearest the centre; bayesian_review_score returns the best reviewed. Price is not offered as a sort because Booking's own price ordering drops properties between pages — the returned list is still ordered cheapest first, so sorting the fetched properties by price costs nothing.
When the destination holds more than you asked for, the job completes with complete: false and result.truncated_at_limit: true, and result.declared_total still carries the full size — so "Goa has 3,100 properties, here are 500" is distinguishable from a search that broke.
max_properties combines with star_ratings. "star_ratings": [4, 5], "max_properties": 300, "sort": "bayesian_review_score" is the 300 best-reviewed 4- and 5-star properties, for a bounded cost.Cost control — star_ratings
star_ratings narrows a sweep at the origin — the excluded properties are never fetched, so this is not a filter applied to a list you have already paid for. It combines with max_properties: on a destination smaller than the cap, narrowing by rating means the search exhausts the destination in fewer pages and stops there rather than reading pages it doesn't need. On a destination at or above the cap, max_properties is already the bound — narrowing by rating then changes which properties come back within that same page budget, not how many pages are read. In a city with 480 properties (below the default cap), [4, 5] narrowed the result to 89 and [5] to 28, each in a single page instead of five.
max_properties tops out at 1,000, below the point where Booking's own pagination would otherwise force the destination to be cut into overlapping price bands (roughly 1,100 matches) — so every capped search is one straight walk of the result list, however large the destination. Price-band slicing, and the partitions[] / incomplete_partitions[] fields it populates, is a thing only a job created before this cap existed can show.class=0, class=6, class=-1, class=3.5 and class=abc each returned Ahmedabad's full 480 — so passing one through would sweep and bill the entire destination while looking like a filtered answer. A value outside 1–5, or more than five of them, is a 400 before any job is created.class=1 … class=5 sum to 389 of Ahmedabad's 480, and the remaining 91 are unreachable through this field at all. So "star_ratings": [1, 2, 3, 4, 5] is not the same request as omitting the field — it is a narrower one, and it silently drops every unrated property. To sweep the destination, omit the field.
Under a filter, property_count counts the slice and never the destination. declared_total and coverage sit on that same filtered footing — they say how completely the slice was retrieved, not how much of the market it represents. Nothing in the property list distinguishes a narrow sweep from a full one, which is why the ratings you asked for are echoed back on the job.
The filter matches the rating value, not who awarded it. Booking carries official star ratings alongside a property's own self-assessment, and class= selects on the number either way — one class=5 sweep returned 27 officially rated properties and 1 self-assessed one. Every property carries star_rating_type (STARS vs TILES) — read it if that distinction matters to you, since the filter will not make it for you.
Poll for results
Poll the poll_url every ~2 seconds until status is completed or failed. Statuses: pending → processing → completed | failed.
/api/v1/scraper/destination-search/{job_id}/status: "failed", result.properties is empty unless the sweep failed partway, in which case it holds everything collected before the failure — those properties are real data, and the job was billed for them.Response schema
Job envelope fields: id, status, ota, destination, destination_label, dest_id, dest_type, check_in, check_out, adults, rooms, children_ages, currency, star_ratings, max_properties, sort, complete, property_count, result, error_message, created_at, started_at, completed_at, duration_seconds, bandwidth_kb. A job created before max_properties shipped reads null for it — it swept the whole destination uncapped (possibly price-sliced, see partitions[]). Only such a job that was still queued when the cap shipped ran under the default cap of 500; check result.max_properties on those.
complete before treating result.properties as the destination. It answers exactly one question — was anything left out of this search?
| Value | Meaning |
|---|---|
true | Nothing was left unreachable — the walk ran until Booking stopped returning new rows, within max_properties. |
false | The search stopped at max_properties while the destination holds more (result.truncated_at_limit is true in exactly that case) — or, for a job that predates the cap, a price slice could not be served in full. Either way the list is real data about the properties it holds, but it is not the destination's property list — result.completeness_note says why. This is not an error and the job still reads completed. |
null | The search failed before this could be established. For "ota": "makemytrip", null can also mean a finished walk whose count is a lower bound — read completeness_note (see MakeMyTrip). |
result.coverage is the separate question of how many of Booking's own declared total came back — the two differ because that declared total is itself approximate. Branch on complete to decide whether the enumeration held everything within the cap; read coverage to see how much of the whole destination the returned properties represent.
Fields — result (the sweep)
| Field | Notes |
|---|---|
properties[] | One entry per property, cheapest first. For "ota": "makemytrip": in MakeMyTrip's listing order, each with a rank (see MakeMyTrip). |
property_count | Number of entries. |
declared_total | Booking's own claim of how many match. Approximate — consecutive identical requests have returned different totals for the same city. |
coverage | property_count / declared_total, or null. |
complete, completeness_note | As above. |
max_properties | The cap the search ran under, echoed (also at the top level of the job, beside sort). |
truncated_at_limit | true when the search stopped at max_properties while the destination holds more — the reason complete is false. declared_total still reports the full size. |
partitions[], incomplete_partitions[] | The price slices a destination had to be cut into, and any that still could not be served. Only ever populated on a job created before max_properties shipped — a capped search's ceiling sits below Booking's own pagination limit, so it never needs to slice. |
destination | What the free text resolved to (dest_id, dest_type, label, name, country_code, latitude, longitude, alternatives, resolved_from). resolved_from is top_match (Booking's first suggestion) or type_preference (an area was preferred over a property Booking ranked first; that property is alternatives[0]). Check dest_type before reading the list: an area query that resolved to a hotel answers a different question. catalogue_size is always null for Booking, which no longer reports a property count. |
within_destination_count | How many returned properties Booking places inside the resolved area rather than merely near it. null when Booking's wording could not be read, or the search was anchored on a single hotel. A destination search is a radius from the area's centre, never a clip to its outline, so a neighbourhood sweep can be complete and still mostly be the surrounding city. To get the neighbourhood itself, filter on per-property within_destination or on distance_km against your own radius; a small max_properties caps the count, not the reach. |
currency / currency_requested | What Booking said it quoted vs what was asked for — compare, never assume. |
star_ratings | The property ratings the sweep was narrowed to, echoed (also at the top level of the job). Empty means the whole destination. Non-empty makes every count above a count of the slice, and excludes unrated properties entirely — see Cost control above. |
nights, adults_per_room, children_per_room | Derived, so a price never has to be re-interpreted. |
ota, check_in, check_out, adults, rooms, children_ages | The request, echoed. |
pages_fetched, requests_made, source, scraped_at | How the sweep ran (source is http or browser) and when it was stamped (ISO 8601). |
error | null unless the sweep failed; the same text lands in the job's error_message. |
malformed_rows | Result cards the parser could not read and skipped. Non-zero is not a failure, but it is the only sign rows were dropped. |
Fields — result.properties[] (one property)
Field notes below describe Booking results. For "ota": "makemytrip", hotel_id and property_id are both MakeMyTrip's own hotel id, and rank is set — see MakeMyTrip.
| Field | Notes |
|---|---|
hotel_id | Booking's own internal id (slug) for the property. It's not a request field — Booking.com per-hotel pricing is submitted by hotel_name + city, not by id — but a Booking per-hotel job's response echoes this same id back as its own hotel_id, so it's how you correlate a card here with that job's result. |
name | The property's current name. |
property_id | Booking's numeric id. Stable across a rebrand, unlike hotel_id. |
name_matches_id | false when the id no longer describes the name — see the warning below. |
price, currency | One room, one night, before taxes (same price_basis / price_scope semantics as the per-hotel guide). Each card also carries them: price_basis is always per_night_before_taxes here and price_scope is per_room. |
taxes_fees | Same per-night basis. |
stay_total, taxes_fees_total | Booking's own whole-stay figures, unmodified. |
strikethrough_price, discounts[] | Pre-discount price and the lines already subtracted from price, all on the same per-night scale. Do not subtract discounts from price again. |
offer_occupancy, fits_occupancy | The party the card's block was priced for, and whether it seats the party you asked about. |
board, meal_plan, free_cancellation, cancel_by | Board is a closed vocabulary; meal_plan is Booking's own wording. |
star_rating, star_rating_type | STARS is an official rating, TILES a self-assessment — do not flatten them. |
review_score, review_count | |
latitude, longitude, address, city, country_code, distance_text, accommodation_type_id | distance_text is Booking's own wording, e.g. "2.3 km from downtown"; accommodation_type_id is Booking's numeric property-type id. |
distance_km | distance_text read as kilometres, or null when Booking stated none or used wording that is not recognised. |
within_destination | Tri-state: true Booking places it inside the area, false Booking states a distance from the area (outside), null unknown — including every row of a search anchored on a single hotel. Unknown is never defaulted to true. |
sold_out, is_closed, is_ad | |
url | Direct link to the property page. |
price is divided by the night count so it means the same thing as everywhere else in this API, with Booking's own figures kept in stay_total.fits_occupancy is false on those, and null where Booking stated no occupancy at all.name, not by hotel_id. Booking freezes a property's URL identifier at first listing and lets the name change underneath — in one measured city, 14% of properties were listed under an identifier that now names a different brand (a Courtyard-branded slug that had become a Le Meridien). The failure is asymmetric: matching by identifier and missing just returns nothing, but a name lookup can match the stale slug with high confidence and return the wrong hotel. Use name for matching and property_id for identity; hotel_id is Booking's own address for the property, not a name to match on.Errors — 404 (unknown job_id, or another client's job — its existence is not disclosed), 403 (key lacks the scope).
Next: Google Flights →
MakeMyTrip ("ota": "makemytrip")
Send "ota": "makemytrip" (alias "mmt") to walk a city's MakeMyTrip hotel listing. Auth needs the destination:makemytrip scope, which is separate from destination:booking and is not on existing keys. It is not enabled by default — ask your ScrapeGuys contact. While it is off for your account the request is a 400 ("MakeMyTrip destination search is not currently enabled").
The search pages MakeMyTrip's own listing, 10 hotels a page, and returns the hotels in MakeMyTrip's listing order, not sorted by price. Each property carries rank, its 1-based position in that order (a hotel MakeMyTrip repeats keeps the rank of its first appearance). On Booking results rank is null.
| Field | MakeMyTrip |
|---|---|
destination | Resolved to a MakeMyTrip city through its autosuggest. Regions and landmarks are not supported. |
currency | INR only; anything else is 400. |
children_ages | Not supported yet; non-empty is 400. |
star_ratings | Not supported; non-empty is 400. |
sort | Not accepted; sending it is 400. The job's sort is "". |
check_in | One day nearer than Booking's horizon; a later date is 400 naming the limit. |
max_properties | Same rule as Booking: default 500, 100–1,000 in steps of 100. |
Reading complete
MakeMyTrip gives no reliable total, so declared_total and coverage are always null. result.completeness_note says why the walk stopped:
complete | completeness_note | Meaning |
|---|---|---|
true | MakeMyTrip reports no more hotels | MakeMyTrip itself said the list ended. |
null | empty page N without MakeMyTrip's end-of-list flag; … | Not proof the list ended; the count is a lower bound. |
false | reached the cap of N properties | Stopped at max_properties (truncated_at_limit: true). |
null | cursor loop: … | MakeMyTrip kept serving hotels already sent; the count is a lower bound. |
false | blocked on page N after M hotels | One page stayed blocked through repeated retries. Hotels fetched so far are kept. |
false | stopped at the byte ceiling | The job reached its traffic ceiling (8 MB). |
false | stopped at the time limit after M hotels | The walk used its time budget (15 minutes by default). |
false | MakeMyTrip error on page N: … | MakeMyTrip answered with its own error. Hotels fetched so far are kept. |
A block, a MakeMyTrip error, an empty first page without the end-of-list flag, or the time limit on the first page fails the job (status: failed). Throttles and server errors are retried and do not end the walk by themselves.
bandwidth_kb on the job counts every attempt, including blocked retries.