Purpose
Search Booking.com accommodations by destination, property name, or known canonical hotel URL. Resolve unknown property names without guessing slugs, then inspect the property's date-specific room inventory to determine whether it appears bookable, including prices, room details, cancellation terms, review metrics, coordinates, and representative reviews. A property's own website being unavailable is not itself evidence of unavailability; Booking room-level availability for the requested dates is the relevant signal. This is strictly read-only: never reserve, save, sign in, or enter payment flows.
When to Use
Use for hotel-name resolution, accommodation discovery, date-specific availability and price comparison, enriching known Booking hotel URLs, optional currency/sort/filter selection, map searches, pagination, or checking whether a property has rooms bookable for supplied dates and occupancy. Validate similarly named properties using name, address, city, country, and canonical URL.
Workflow
- Use one real
browserless_agentsession with a residential proxy and retain the same proxy/session throughout. Booking may require AWS WAF clearance; usewaitUntil: 'load'or'domcontentloaded', wait 3–7 seconds for hydration, and never wait fornetworkidle. Report an uncleared challenge rather than treating it as zero availability. - If a canonical URL is supplied, navigate directly to it and append only requested parameters:
checkin={YYYY-MM-DD}&checkout={YYYY-MM-DD}&group_adults={adults}&group_children={children}&no_rooms={rooms}&selected_currency={ISO-4217-code}. Add oneage={child-age}per child when children are present. The durable detail form ishttps://www.booking.com/hotel/{country-code}/{slug}.en-gb.html(or.html) with these query parameters. Strip affiliatelabel,sid, and tracking parameters. - If given a property name or destination, first use
https://www.booking.com/searchresults.html?ss={url-encoded-query}. For destination searches, resolve(dest_id,dest_type)throughhttps://accommodations.booking.com/autocomplete.json?aid={aid}&query={url-encoded-query}&lang=en-us&size=10, obtaining{aid}fromwindow.utag_data.aidafter the Booking session is loaded. Select the result matching the requested name/address/country. Never invent an opaque ID or slug. - Compose one direct results URL with
ss, resolveddest_id/dest_type, dates, occupancy, currency, and optionalorder,offset, and URL-encodednflt. Useful sorts includeprice,bayesian_review_score,bayesian_review_score_and_price,class,class_asc,distance_from_search, andhomes_apartments_first. Useful filters includeht_id,class,review_score,mealplan,oos(free cancellation),fc(no prepayment),hotelfacility,roomfacility,chaincode,genius,sustainable_property,distance,pri,price, anddi. - On hydrated results, extract
[data-testid="property-card"]cards. Follow selected clean canonical/hotel/{country-code}/{slug}.htmllinks directly when room-level availability or richer reviews/coordinates are required. Do not click “See availability,” Reserve, Save-to-list, sign-in, or payment controls. - For a known or resolved property, use the direct detail URL with the requested dates and occupancy, then run the availability evaluator below. Treat visible room rows/offers with a price and a booking/reservation control as evidence of apparent bookability; treat explicit “no rooms,” “not available,” “sold out,” or “no availability” text as unavailable. A visible Reserve control may be detected but must never be clicked. If the page is blocked, return an anti-bot status rather than
bookable:false. - For more than 25 search results, repeat the same URL with
offset=25,50, etc., concatenate, and deduplicate by canonical URL. Booking normally returns 25 properties per page and caps pagination atoffset=1000. - Run this concrete evaluator on the loaded search-results or hotel-detail page:
(() => {
const clean=x=>(x||'').replace(/\s+/g,' ').trim();
const abs=h=>{try{return new URL(h,location.origin).href.split('#')[0]}catch{return null}};
const canonical=h=>{const u=abs(h);if(!u)return null;const m=new URL(u).pathname.match(/^\/hotel\/([^/]+)\/([^/?#]+?)(?:\.en(?:-[a-z]{2})?)?\.html$/i);return m?`https://www.booking.com/hotel/${m[1]}/${m[2]}.html`:u.split('?')[0]};
const body=clean(document.body.innerText), blocked=/awswaf|challenge\.js|captcha|verify you are human/i.test(body);
const cards=[...document.querySelectorAll('[data-testid="property-card"]')].map(card=>{const a=card.querySelector('a[data-testid="title-link"],h3 a,a[href*="/hotel/"]');return{name:clean(card.querySelector('[data-testid="title"],h3')?.textContent),url:abs(a?.getAttribute('href')),address:clean(card.querySelector('[data-testid="address"]')?.textContent)||null,price:clean(card.querySelector('[data-testid="price-and-discounted-price"],[data-testid="price-for-x-nights"],[data-testid="price"]')?.textContent)||null,roomDetails:clean(card.querySelector('[data-testid="recommended-units"],[data-testid="unit-configuration"],[data-testid="property-card-unit-configuration"]')?.textContent)||null,reviewScore:clean(card.querySelector('[data-testid="review-score"]')?.textContent)||null}}).filter(x=>x.name||x.url);
const rows=[...document.querySelectorAll('#hprt-table tr,[data-testid="property-section--content"] table tr,[data-testid*="room"]')].map(x=>clean(x.innerText)).filter(x=>x.length>10);
const reserve=[...document.querySelectorAll('button,input[type=submit],a')].map(e=>clean(e.value||e.textContent)).filter(x=>/reserve|book now|i['’]?ll reserve/i.test(x)).slice(0,10);
const soldOutLanguage=/no rooms|not available|sold out|no availability/i.test(body);
const link=document.querySelector('link[rel="canonical"]')?.getAttribute('href');
const path=location.pathname.match(/^\/hotel\/([^/]+)\/([^/?#]+?)(?:\.en(?:-[a-z]{2})?)?\.html$/i);
const name=clean(document.querySelector('h1,[data-testid="title"]')?.textContent);
const address=clean(document.querySelector('[data-testid="PropertyHeaderAddressDesktop-wrapper"],.hp_address_subtitle,[data-testid="address"],[data-testid="hotel-address"],[class*="address"]')?.textContent)||null;
const c=document.querySelector('[data-atlas-latlng]')?.getAttribute('data-atlas-latlng'), n=(c||'').match(/-?\d+(?:\.\d+)?/g);
return blocked?{ok:false,reason:'awswaf_challenge_or_captcha'}:cards.length?{page:'search-results',results:cards}:{page:'hotel-detail',name,url:canonical(link)||(path?`https://www.booking.com/hotel/${path[1]}/${path[2]}.html`:location.href.split('?')[0]),address,availability:{bookable:!soldOutLanguage&&rows.length>0,explicitlyUnavailable:soldOutLanguage,roomRows:rows.slice(0,30),reserveLabels:reserve},coordinates:n&&n.length>=2?{lat:Number(n[0]),lng:Number(n[1])}:null,reviewScore:clean(document.querySelector('[data-testid="review-score"],[class*="review-score"]')?.textContent)||null};
})()- When extracting richer detail data, prefer
[data-atlas-latlng]for coordinates,[data-testid="featuredreview-text"], .c-review__body, [data-testid="review-positive-text"]for excerpts,[data-testid="review-score"]for score, and[data-testid="price-and-discounted-price"],[data-testid="price-for-x-nights"]for room prices. Preserve raw price text and, when total and dates are known, emit both total-stay and per-night values.
Site-Specific Gotchas
- Booking's canonical search endpoint is
/searchresults.html; property identity uses clean/hotel/{two-letter-country-code}/{slug}.htmlpaths. Localized.en-gb.htmldetail URLs accept the same availability parameters. - A property name alone is not enough to construct a URL. Resolve the result and validate name, address, city, country, and href before opening the detail page.
- Booking may show AWS WAF, consent, sign-in, hCaptcha, or error overlays. Use a residential-proxy browser session, retain it throughout, wait for hydration, and report an uncleared challenge. A challenge is not evidence that the property has no rooms.
- The absence of the property's independent website does not determine Booking availability. Only room-level offers for the requested dates, occupancy, and room count establish apparent bookability; availability can change and a visible offer is not a completed reservation.
- Do not click reservation or availability CTAs. Detect their labels and inspect surrounding room rows instead.
roomRows.length === 0alone is inconclusive if the page is still hydrating, so wait before evaluating and distinguish explicit sold-out text from missing markup. - Child ages are mandatory whenever
group_childrenis nonzero. Prices may be nightly or total-stay in the UI; preserve raw text and normalize both when possible. [data-atlas-latlng]is the preferred coordinate source; return null rather than guessing. Review excerpts and coordinates are usually absent from search cards, so enrich selected properties through direct canonical detail URLs.nfltvalues are URL-encoded semicolon-delimited filters. Common observed values includeht_id=204hotel,201apartment,203hostel,review_score=90|80|70|60,oos=1free cancellation,fc=2no prepayment,mealplan=1breakfast,hotelfacility=2parking,17airport shuttle,54EV charging, andpopular_activities=2pool. Confirm drift by reading the resulting URL when needed.- Currency display can be per night or total stay; when possible set both
selected_currency={currency}and the same-origincur_currcookie after WAF clearance. Never use affiliate parameters as identity. - The in-page same-origin
/dml/graphqlFullSearch request may provide more reliable structured search data, but it is permissible only after WAF clearance and must be fetched from page context with credentials; fall back to the DOM evaluator when unavailable.
Expected Output
For a resolved property availability check, return {ok, query, matchedName, address, url, checkin, checkout, occupancy, availability:{bookable, explicitlyUnavailable, roomRows, reserveLabels}, price, roomDetails, reviewScore}. Use bookable:null with an explicit reason for a WAF/challenge or still-loading page. For searches, return request parameters, resolved destination or canonical URLs, pagination metadata, and rows shaped as {name,url,address,price,roomDetails,reviewScore}. For ambiguous names return {ok:false,reason:"ambiguous_or_not_found",candidates:[...]}; for an uncleared anti-bot wall return an explicit WAF failure rather than an empty result set.