Compare Travel Options on Expedia
Purpose
Shop and compare travel inventory on Expedia the way a traveler would by hand — search Stays (hotels and Vrbo vacation rentals), Flights, Cars, Packages, and Things to do; apply filters (price, dates, ratings, stops, amenities, cancellation, baggage); read each option's price, rating, fees, refundability, and loyalty/member pricing; then summarize tradeoffs and build a shortlist. Read-only and shopping-only: open and compare options, but never advance past a results/detail page into checkout or place a booking without explicit user confirmation. All dynamic results render in the browser from a single internal GraphQL endpoint; there is no usable public API, so a real (stealthed) browser session is the working surface.
When to Use
- "Find me a romantic weekend stay near Seattle under $350/night" → Stays search + price/rating filters + shortlist.
- "Compare hotel vs Vrbo for this trip" → a single Stays search returns both; filter by
Vacation rentals/Hotelsproperty type. - "Does changing my dates save money?" → re-run the same search across candidate date ranges and diff the totals (flights expose a flexible-date price strip directly).
- "Find the best flight with a carry-on/checked bag and a reasonable layover" → Flights search + the
Carry-on bag included/ fare-feature filters + Stops/Layover filters. - "Build me a full itinerary from Expedia options" → run Stays + Flights (+ Cars/Activities) searches and assemble a combined summary.
Workflow
Recommended method: browser (stealth + residential proxy). Every result list (hotels, flights, cars, activities) is hydrated client-side by POSTs to a single internal endpoint https://www.expedia.com/graphql (persisted queries, behind Akamai Bot Manager). There is no documented or cookieless-usable public API — treat GraphQL as internal-only and drive the rendered pages. Deep-link search URLs are the fast path: most search state (destination, dates, occupancy, sort, and price band) is controllable from the URL, so you rarely need to operate the search form.
1. Stealth + residential-proxy session
sid=$(browse cloud sessions create --keep-alive --proxies --verified \
| node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>process.stdout.write(JSON.parse(s).id))")
--verified --proxies is the configuration that loaded every search page cleanly during testing. Expedia sets Akamai Bot Manager cookies (ak_bmsc, bm_ss, bm_so) on every response and guards the search-results routes; a bare datacenter session is liable to Akamai challenges. A US residential proxy also fixes IP-geolocation (currency/market) for the result set.
2. Stays (hotels + Vrbo) — deep-link search
https://www.expedia.com/Hotel-Search
?destination=<URL-encoded "City, State, Country">
®ionId=<market id> # optional but locks the market; e.g. Seattle = 3121
&startDate=YYYY-MM-DD
&endDate=YYYY-MM-DD
&adults=2&rooms=1 # &children=age,age for kids
&sort=PRICE_LOW_TO_HIGH # RECOMMENDED | PRICE_LOW_TO_HIGH | REVIEW | DISTANCE | PROPERTY_CLASS
&price=0,350 # min,max — see gotcha: this is TOTAL stay price, not per-night
Open it, then dismiss the fee-inclusive interstitial that auto-opens (pwaDialog=fee-inclusive-pricing-sheet) by clicking the Got it button before reading anything.
export BROWSE_SESSION="$sid"
browse open "<hotel-search-url>" --remote
browse click "@<ref-of-Got-it-button>" --remote # close the "We include taxes and fees" dialog
3. Force the full result list to load, then extract
The result list is lazy/virtualized — only ~3 main cards exist in the DOM right after load. Scroll to hydrate the rest, then read the rendered text:
for i in 1 2 3 4; do browse eval "window.scrollBy(0,2200)" --remote; sleep 1.2; done
browse get markdown body --remote # then parse
Parse the main list (not the promoted carousels — see gotcha). Each property card carries: name, guest rating (9.4 out of 10 + review count), $NNN nightly, $NNN total, an optional struck-through "was" price, refundability (Fully refundable), Reserve now, pay later, and badges (VIP Access, Member Prices). The detail link is /{City}-Hotels-{Name}.h{HOTELID}.Hotel-Information?... — h{HOTELID} is the stable property id.
4. Flights — deep-link search
https://www.expedia.com/Flights-Search
?leg1=from:City%20(SEA),to:City%20(JFK),departure:MM/DD/YYYYTANYT
&leg2=from:City%20(JFK),to:City%20(SEA),departure:MM/DD/YYYYTANYT
&passengers=adults:1
&trip=roundtrip # oneway | roundtrip
&mode=search
Results render progressively (wait ~5s after load). Each option shows airline, depart/arrive times, total duration, stop count, and the roundtrip total price. The left rail exposes filters that map directly to common asks: Stops (Nonstop / 1 Stop / 2+ Stops), Airlines, cabin class (Basic economy / Economy / Premium / Business / First), fare features (Carry-on bag included, Refundable fare, Changes included, Seat choice included), Layover airport, and a total-travel-time slider. A price-prediction widget and a 7-day flexible-date price strip (e.g. Fri, Jun 12 $416) are rendered inline — use the strip to answer "would different dates be cheaper?" without re-searching.
5. Other verticals (same pattern)
Navigate from the top nav (Stays → /Hotels, Flights → /Flights, Cars → /Cars, Packages → /Vacation-Packages, Things to do → /Activities, Cruises → /Cruises) or deep-link the search route (/carsearch, /things-to-do/search). All are JS-rendered lists that follow the same load → scroll → read-markdown extraction pattern.
6. Compare & shortlist, then stop
Aggregate the extracted options into a comparison (price, rating, refundability, fees, location/stops, loyalty pricing), surface tradeoffs, and return a shortlist with detail URLs. Do not click Reserve / Select / Continue into checkout unless the user explicitly confirms a booking — that is a separate, write-action skill.
browse cloud sessions update "$sid" --status REQUEST_RELEASE
Site-Specific Gotchas
- Stealth + residential proxy recommended. Akamai Bot Manager is active on every route (
ak_bmsc/bm_ss/bm_socookies,X-Akamai-Reference-Idheader).--verified --proxiesloaded all search pages cleanly in testing with no challenge. The host pre-run probe reported "no antibots" on the homepage301, but that only reflects the bare homepage — the value-bearing/Hotel-Searchand/Flights-Searchroutes are the guarded ones, so do not start bare. - The price filter is TOTAL stay price, not per-night. The
price=MIN,MAXURL param (which Expedia rewrites toprice=MIN&price=MAX) drives the slider labeled "Total price". Soprice=0,350on a 2-night stay means total ≤ $350 (≈ ≤ $175/night), and the filter chip reads "Less than $350". For a "under $X per night" request, multiply by the number of nights before setting the cap, or read the per-night value off each card and filter client-side. - Cards show both nightly and total prices. Each card renders
$NNN nightlyand$NNN total; don't conflate them. Expedia displays taxes-and-fees-included pricing by default (the reason for the "We include taxes and fees" interstitial). - A fee-inclusive interstitial dialog auto-opens on first results load (
pwaDialog=fee-inclusive-pricing-sheet). It covers the results — dismiss theGot itbutton before snapshotting/reading or you'll capture the dialog instead of the list (screenshot01shows this state). - Results are lazy/virtualized. Only ~3 main cards are in the DOM immediately after load; a single
get markdown bodythen will undercount drastically (saw 3 of 15). Scroll several times (window.scrollBy) to hydrate, then extract — yielded the full list (3 → 23 cards) after ~4 scrolls. - Promoted carousels pollute naive extraction. A horizontally-scrolling
VIP Access properties (N)carousel and sponsored modules appear above/within the results and carry their own prices/ratings. A naive "grab every$NNN nightly" pulls these in and corrupts sort/order assumptions. Scope extraction to the main list, which begins after the## Search resultsheading; main cards are titled### Photo gallery for {name}. - Vrbo / vacation rentals are mixed into Stays results.
/Hotel-Searchreturns hotels and Vrbo rentals (treehouses, cabins, guest houses, glamping) interleaved — convenient for "hotel vs Vrbo" comparisons. Use theProperty typefilter (HotelsvsVacation rentals) to separate them. regionIdlocks the market; plain destination text also works. Hotel detail links expose the resolvedregionId(Seattle market =3121,destType=MARKET),neighborhoodId, andlatLong. PassingregionIdavoids any typeahead-resolution ambiguity. To discover an unknown region, type the city into theWhere to?typeahead and read the resolved link, or inspect a returned detail URL.- The GraphQL endpoint is internal — don't try to call it directly. All result data comes from POSTs to
https://www.expedia.com/graphql(persisted-query hashes, client headers, valid Akamai/bm_*cookies, behind Bot Manager) plus telemetry to/api/uisprime/track. There is no public/documented API and no cookieless path; the rendered pages are the reliable surface. Treat it like OpenTable's GraphQL — a trap, not a shortcut. robots.txtdisallows the search routes for generic bots (/Hotel-Search,/search?,/Flights-Search,/carsearch,/things-to-do/search?,/api/v4/typeahead/,/api/ucs/shortlist/). This is policy, not a hard server block — a real stealthed browser session still renders them — but it confirms there's no bot-friendly data feed.- Member Prices / One Key loyalty require sign-in. Logged-out sessions see public rates plus "Sign in to unlock Member Prices" prompts; the discounted member rate is only realized after authentication. Report public pricing unless the user supplies credentials. Flights/Stays also surface "VIP Access" and bundle-savings ("save when you book stay + car") badges that are conditional.
- Flight prices are roundtrip totals (for
trip=roundtrip), and the inline flexible-date strip already prices ±a few days — read it instead of re-searching to answer date-flexibility questions. - READ-ONLY shopping. Stop at results/detail. Never click
Reserve,Select,Continue, or reach/Checkout,/HotelCheckout,/FlightCheckout, or/MultiItemCheckout.
Expected Output
A shortlist/comparison object per vertical. Distinct shapes:
// Stays (hotels + Vrbo), filtered & sorted
{
"vertical": "stays",
"query": { "destination": "Seattle, Washington, United States of America", "regionId": "3121",
"startDate": "2026-06-12", "endDate": "2026-06-14", "adults": 2, "rooms": 1,
"sort": "PRICE_LOW_TO_HIGH", "priceFilter": { "type": "total_stay", "min": 0, "max": 350 } },
"totalMatches": 15,
"results": [
{
"name": "Ace Hotel Seattle",
"propertyType": "hotel",
"hotelId": "h2330513",
"rating": 9.0,
"reviewCount": 1007,
"nightly": 156,
"totalStay": 312,
"currency": "USD",
"taxesFeesIncluded": true,
"refundable": true,
"reserveNowPayLater": true,
"badges": ["Member Prices"],
"detailUrl": "https://www.expedia.com/Seattle-Hotels-Ace-Hotel-Seattle.h2330513.Hotel-Information?chkin=2026-06-12&chkout=2026-06-14&adults=2"
}
]
}
// Vrbo / vacation rental within the same Stays search
{ "name": "Alki Beach Courtyard Cabin", "propertyType": "vacation_rental",
"rating": 9.8, "reviewCount": 12, "nightly": 189, "totalStay": 378, "currency": "USD",
"refundable": false, "detailUrl": "https://www.expedia.com/...h*.Hotel-Information?..." }
// Flights, roundtrip
{
"vertical": "flights",
"query": { "origin": "SEA", "destination": "JFK", "depart": "2026-06-12", "return": "2026-06-15",
"trip": "roundtrip", "adults": 1, "cabin": "ECONOMY" },
"lowestPrice": 477,
"currency": "USD",
"stopCounts": { "nonstop": 9, "oneStop": 73, "twoPlus": 42 },
"results": [
{ "airline": "American Airlines", "depart": "09:25 AM", "arrive": "05:55 PM",
"duration": "5h 30m", "stops": 0, "priceRoundtrip": 537,
"fareFeatures": ["Carry-on bag included"] }
],
"flexibleDates": [ { "date": "2026-06-12", "price": 416 }, { "date": "2026-06-13", "price": 467 } ]
}
// Comparison / itinerary roll-up across verticals
{
"vertical": "comparison",
"summary": "Cheapest sub-$350-total Seattle stay is the Green Tortoise Hostel ($73/nt); best value hotel is Ace Hotel Seattle (9.0, $156/nt, refundable). Cheapest reasonable SEA-JFK roundtrip is a nonstop at $477.",
"shortlist": [ { "vertical": "stays", "name": "Ace Hotel Seattle", "tradeoff": "Best rating-to-price; refundable" },
{ "vertical": "flights", "name": "American 09:25-17:55 nonstop", "tradeoff": "$537, carry-on included, 5h30m" } ]
}