Expedia Hotel & Flight Scraper – Prices, Reviews & Fares avatar

Expedia Hotel & Flight Scraper – Prices, Reviews & Fares

Pricing

from $1.00 / 1,000 hotels

Go to Apify Store
Expedia Hotel & Flight Scraper – Prices, Reviews & Fares

Expedia Hotel & Flight Scraper – Prices, Reviews & Fares

Scrape Expedia hotel search and flight search in one Actor. Hotel rows carry price, 0-10 guest rating, review count, amenities, photo gallery and mandatory fees; flight rows carry fare, airlines, route, times, duration and stops. No Expedia account or API key needed.

Pricing

from $1.00 / 1,000 hotels

Rating

0.0

(0)

Developer

R.L.

R.L.

Maintained by Community

Actor stats

0

Bookmarked

10

Total users

5

Monthly active users

3 days ago

Last modified

Categories

Share

Expedia Hotel & Flight Scraper — hotel prices, ratings, amenities, photos, guest reviews and flight fares

Expedia Hotel & Flight Scraper — Scrapes Expedia hotel search with prices, ratings, amenities, photos and guest reviews, plus flight search with fares and journeys, from the Expedia mobile GraphQL API. Read-only public travel data. It reads Expedia's own API directly, so it's fast, reliable and complete — no flaky HTML parsing, no headless browser. Export to JSON, CSV, Excel, or an API.


What does this Expedia scraper do?

It runs in one of four modes, chosen with searchType:

  • hotels — give it a place (Copenhagen, Vilnius Old Town, an airport code) and a date range. You get one row per property: name, Expedia hotel id and URL, the price as Expedia shows it plus a parsed number and currency, a lead image, and — on request — the 0–10 guest rating, review count, amenities, the photo gallery, mandatory fees and recent guest reviews.
  • flights — give it two IATA codes and a date. You get one row per offer: fare, airlines, the route, departure and arrival times, duration in minutes, stop count, trip type and Expedia's offer identifier.
  • hotel-details — give it hotel IDs instead of a destination. You get the amenities, photo gallery, 0–10 guest rating and mandatory fees for exactly those hotels, with no search in front of it and no hotel search rows on the bill.
  • reviews — give it hotel IDs and how many reviews you want. You get one row per review — score, title, text, date, author and stay length — each carrying the hotel_id to join back on. Charged per review and nothing else.

Each mode reads its own input fields and ignores the others'. The two by-ID modes take IDs from an earlier run of this Actor (the url or hotel_id column) or from your own data.

  • No code required — set your input, click Start, download your data.
  • Fast & accurate — reads Expedia's own private API and returns structured JSON, not scraped HTML.
  • Rich, structured data — every field below, clean and ready to use.
  • Deep hotel detail on demand — amenities, the full photo gallery, rating and mandatory fees, fetched only when you ask for them.
  • Guest reviews — text, score, date, author and stay length per review.
  • Flights too — switch searchType to flights for fares, airlines and journey times.
  • Already know the hotels? hotel-details and reviews take hotel IDs and skip the search entirely, so you are not charged for search rows you did not ask for.
  • Transparent pay-per-result pricing — pay for the rows you get, with no run minimum and no platform-usage surcharge.
  • Export anywhere — JSON, CSV, Excel, XML, or pull it live via the Apify API.

What can you do with Expedia data?

  • Rate-parity and price monitoring — re-run a city and date range on a schedule and diff price_total.
  • Competitive set analysis — pull every property in a neighbourhood with its rating and review count.
  • Amenity and fee research — see which properties advertise what, and what mandatory fees they disclose.
  • Review mining — collect guest review text, score and date for sentiment work.
  • Airfare tracking — watch fares, airlines and stop counts on a route over time.

What do the fields actually mean?

  • Hotel prices are display strings. Expedia returns a price as text ("$1,277 total"), never a number. Rows keep that original string in price_display and a parsed numeric price_total with currency, so they sort correctly without losing the "total" / per-night wording — which changes what the number means. Flight fares come back the same way but without the suffix ("$189").
  • rating is out of 10. It is parsed from Expedia's own label, which rating_text keeps verbatim: "8.6 out of 10". Some properties genuinely have no rating summary; those rows carry rating: null rather than a fabricated value.
  • destination is Expedia's resolved region, not your input. Type Vilnius Old Town and rows come back labelled Vilnius Old Town, Vilnius, Vilnius County, Lithuania, with region_id naming the region Expedia matched. In flights mode destination is just the IATA code you asked for and region_id is null.
  • fees is Expedia's mandatory-fee disclosure, flattened from HTML to plain text — the "Fee for buffet breakfast…" kind of copy, not a number.
  • amenities and images are deduplicated and capped at 40 entries each. Amenities are Expedia's own strings, which mix facilities with property facts (24-hour front desk, 2 restaurants, 10 floors, 1,453 square feet of conference space).
  • route / itinerary / trip_type / operated_by are flight-only display strings, e.g. Seattle (SEA) - New York (JFK), 11:50am - 8:28pm, One way per traveler, Alaska Airlines operated by ALASKA. duration_minutes and stops are parsed out of Expedia's "5h 36m (Nonstop)" wording so they are usable as numbers.
  • offer_id is Expedia's opaque fare token for a flight offer. It identifies the offer within the search; it is not a stable long-term id.
  • Unused fields are null, not absent. Hotel rows carry the flight columns as nulls and vice versa, so a mixed export keeps one consistent shape. The dataset also ships separate Hotels and Flights views if you'd rather see only the relevant columns.

How do I scrape Expedia? (3 steps)

  1. Set your mode — hotels with a destination, or flights with two IATA codes.
  2. Pick options — dates, adults, includeDetails, includeReviews, and a maxItems cap. Leave the proxy alone; see Do I need a proxy? below.
  3. Run & export — click Start, then download the dataset as JSON/CSV/Excel or fetch it via the API.

What input does it take?

FieldTypeDefaultDescription
searchTypeenumhotelsWhich mode to run — one of hotels, flights, hotel-details, reviews. Each mode reads its own fields; the others are ignored.
Hotels
destinationstring–A place name: a city, area or neighbourhood, e.g. Copenhagen or Vilnius Old Town. An airport code works too. Required in hotels mode.
checkInstring–Check-in date. A date, or a relative value like 30 days. Empty = 30 days from today.
checkOutstring–Check-out date. Empty = 3 nights after check-in. Must be after checkIn.
includeDetailsbooleanfalseAmenities, photo gallery, rating and fees per hotel. Four extra API calls per hotel — one per surface — so noticeably slower.
includeReviewsbooleanfalseAttach recent guest reviews to each hotel. One extra API call per hotel.
reviewsPerHotelinteger5Reviews per hotel when reviews are on, and in reviews mode. 0 fetches none and charges nothing for them (hotels mode only). Expedia returns at most 50, whatever is asked for.
Flights
flightOriginstring–Origin airport IATA code, e.g. SEA. Required in flights mode.
flightDestinationstring–Destination airport IATA code, e.g. JFK. Required in flights mode.
departureDatestring–Outbound date, absolute or relative. Empty = 30 days from today.
returnDatestring–Return date. Empty = one-way.
Hotel details / Reviews (by hotel ID)
hotelIdsarray–Hotel IDs or Expedia hotel URLs. Required by hotel-details and reviews. Accepts a bare ID (45263901), the url this Actor outputs, or any Expedia hotel URL. Up to 200 per run.
Both
adultsinteger2Adult guests or passengers, 1–14. Expedia returns the same results for every occupancy of 14 or more, so larger values are rejected rather than billed as something they are not.
maxItemsinteger1000 = no limit, but see the 200-per-search ceiling under Limits.
proxyConfigurationobjectApify residentialSee Do I need a proxy? below — residential is enforced, not merely the default.

Example input

{
"searchType": "hotels",
"destination": "Copenhagen",
"checkIn": "2026-12-05",
"checkOut": "2026-12-08",
"adults": 2,
"includeDetails": false,
"includeReviews": false,
"reviewsPerHotel": 5,
"maxItems": 100,
"proxyConfiguration": {
"useApifyProxy": true,
"apifyProxyGroups": [
"RESIDENTIAL"
]
}
}

What does a row look like?

A hotel row:

{
"type": "hotel",
"destination": "Copenhagen City Centre, Copenhagen, Hovedstaden, Denmark",
"region_id": "6051779",
"checkin": "2026-12-05",
"checkout": "2026-12-08",
"hotel_id": "45263901",
"hotel_name": "WIDE Hotel",
"url": "https://www.expedia.com/h45263901.Hotel-Information",
"price_display": "$707 total",
"price_total": 707.0,
"currency": "USD",
"image": "https://images.trvl-media.com/lodging/46000000/45270000/45264000/45263901/49f6b716.jpg?impolicy=resizecrop&ra=fit&rw=455&rh=455",
"rating": 9.0,
"rating_text": "9.0 out of 10",
"review_count": 1989,
"amenities": [
"24-hour business center",
"24-hour front desk",
"24-hour gym",
"24-hour roundtrip airport shuttle on request for a surcharge",
"…+35 more"
],
"images": [
"https://images.trvl-media.com/lodging/46000000/45270000/45264000/45263901/49f6b716.jpg?impolicy=resizecrop&rw=1200&ra=fit",
"…+39 more"
],
"fees": "Fee for buffet breakfast: approximately DKK 255 for adults and DKK 160 for children\n\nAirport shuttle fee: DKK 1500 per vehicle (one-way, maximum occupancy 4)…",
"reviews": [
{
"title": "Excellent",
"text": "Beautiful hotel in a great location.",
"rating": 10.0,
"date": "Aug 20, 2026",
"author": "Konrad",
"stay_info": "Stayed 4 nights in Aug 2026"
}
]
}

A flight row:

{
"type": "flight",
"origin": "SEA",
"destination": "JFK",
"departure_date": "2026-10-27",
"return_date": null,
"trip_type": "One way per traveler",
"route": "Seattle (SEA) - New York (JFK)",
"itinerary": "11:50am - 8:28pm",
"airlines": ["Delta"],
"operated_by": null,
"duration_minutes": 338,
"stops": 0,
"price_display": "$189",
"price_total": 189.0,
"currency": "USD",
"offer_id": "v5-sos-89d379265ca94de8accd7c6e6feeede1-134-1~8.S.1~…"
}

Which fields are in the dataset?

Hotel rows (type: "hotel"), the dataset's Hotels view:

FieldTypeDescription
typetextRecord type
destinationtextExpedia's resolved region label
region_idtextRegion ID Expedia matched
checkintextCheck-in
checkouttextCheck-out
hotel_idtextHotel ID
hotel_nametextHotel
urllinkURL
price_displaytextPrice (as shown)
price_totalnumberPrice
currencytextCurrency
imageimageLead image
ratingnumberGuest rating out of 10
rating_texttextRating as Expedia labels it
review_countnumberReviews
amenitiesarrayAmenities (max 40)
imagesarrayPhoto gallery (max 40)
feestextMandatory-fee disclosure
reviewsarrayGuest reviews

Flight rows (type: "flight"), the dataset's Flights view:

FieldTypeDescription
typetextRecord type
origintextOrigin IATA code
destinationtextDestination IATA code
departure_datetextDeparture date
return_datetextReturn date, null for one-way
trip_typetextTrip type
routetextRoute
itinerarytextTimes
airlinesarrayAirlines
operated_bytextOperating carrier
duration_minutesnumberDuration (min)
stopsnumberStops
price_displaytextPrice (as shown)
price_totalnumberPrice
currencytextCurrency
offer_idtextOffer ID

Which mode should I use?

You wantModeWhat it charges
Hotels for a place and some dateshotelsone hotel row per result
…with amenities, photos, rating, feeshotels + includeDetailshotel row + hotel details
…with guest reviews attachedhotels + includeReviewshotel row + per review
Details for hotels you already knowhotel-detailshotel details only
Reviews for hotels you already knowreviewsper review only
Flight fares for a routeflightsone flight row per offer

The two by-ID modes exist so you do not pay twice. If you already have hotel IDs — from an earlier run of this Actor, or from your own data — they fetch only what you asked for, with no destination search and no hotel search rows on the bill. The data and the price per unit are identical either way; only the search row differs.

Both take IDs in whatever form you have them:

{
"searchType": "reviews",
"hotelIds": ["45263901", "https://www.expedia.com/h9536.Hotel-Information"],
"reviewsPerHotel": 10
}

Rows carry a type column saying which mode produced them — hotel, flight, hotel_detail or review — so a mixed dataset can be filtered and a bill line reconciled against the rows that earned it. The two by-ID modes emit only the columns they can fill, so a CSV export is not padded with empty hotel or flight columns:

ModeColumns
hotel-detailstype, hotel_id, hotel_name, url, rating, rating_text, amenities, images, fees
reviewstype, review_id, hotel_id, url, hotel_review_count, review_rating, review_title, review_text, review_date, review_author, review_stay_info

In reviews mode maxItems counts review rows, and reviewsPerHotel bounds how many are taken from each hotel — so maxItems: 10 stops after ten reviews rather than paying to fetch every hotel you listed.


How much does it cost?

This actor uses pay-per-event pricing. You pay for the rows it delivers and nothing else — there is no run minimum, and platform usage (compute, proxy traffic) is not billed on top. The optional enrichments are priced separately because each one costs extra API calls.

Prices per 1,000, by your Apify plan — in effect from 31 October 2026. Until then every event is the previous flat rate ($1.00 per 1,000 rows, details and $0.10 per 1,000 reviews); the Pricing tab on the Actor page always shows what is live today.

You scrapeFreeBronzeSilverGold+
Hotel search result$3.33$2.50$2.30$2.12
Flight offer$3.33$2.50$2.30$2.12
Hotel details (amenities, photos, rating, fees)$3.99$3.00$2.76$2.54
Guest review$2.00$1.50$1.38$1.27

A plain hotel search costs $2.50 per 1,000 rows on Bronze. Turning both enrichments on, with the default five reviews a hotel, costs $13.00 per 1,000 hotels — the hotel row, the details, and five reviews.

Rows are delivered as they are collected, so a run stopped by its cost cap keeps everything it had already found. Details and reviews are charged only when they actually come back, so a hotel whose enrichment Expedia refused costs you the listing row alone. maxItems bounds the listing rows, and reviewsPerHotel bounds the review count per hotel.


How do I run it from my own code?

Run it programmatically with the Apify API:

curl -X POST "https://api.apify.com/v2/acts/expedia-api-scraper/runs?token=YOUR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"searchType": "hotels",
"destination": "Copenhagen",
"adults": 2,
"includeDetails": false,
"includeReviews": false,
"reviewsPerHotel": 5,
"maxItems": 100,
"proxyConfiguration": {
"useApifyProxy": true,
"apifyProxyGroups": [
"RESIDENTIAL"
]
}
}'

Or with the Apify CLI: apify call expedia-api-scraper -i input.json -o.


What are the limits?

200 properties per search. Expedia's own search returns at most 200 properties for a region however large it is — measured identically for Berlin, Paris, London and New York. maxItems above that cannot be met by one run, and the log says so rather than leaving you to guess whether you hit a ceiling or a small region. To go deeper, search neighbourhoods rather than the whole city (Vilnius Old Town rather than Vilnius); each is its own region with its own 200.

Occupancy is meaningful to 13. From 14 adults up, Expedia returns the same result set whatever the number, so adults is capped at 14.

Up to 50 reviews per hotel. reviewsPerHotel above that is clamped — Expedia will not return more.


FAQ

Do I need an Expedia account or API key?

No. The actor talks to Expedia's mobile API for you — you only need an Apify account.

Do I need a proxy?

You get one whether you ask or not. Expedia's edge throttles by exit IP and throttles Apify's own egress hard, so a run with no proxy is answered with Provisioned request rate has been exceeded almost immediately. The actor therefore treats Apify residential as a floor: if proxyConfiguration is missing, or selects Apify Proxy without the RESIDENTIAL group, it is corrected to residential and the substitution is logged. A custom proxyUrls list is used exactly as given.

One residential exit is held for as long as it keeps working, and replaced the moment Expedia rate-limits it (403/429/503) or the tunnel through it fails. That is deliberate: re-minting a session per request means a cold TLS handshake every time and throws away good exits along with bad ones.

What if Expedia is throttling?

The run finishes SUCCEEDED with a status message saying so, rather than failing — throttling is environmental and not a defect in the actor. Rows already collected are kept. Optional enrichment (details, reviews) is dropped for the rest of the run if it fails on three hotels running; the listing rows, which are the part worth having, still land, and the log names the surface it gave up on.

What happens with bad input?

The run finishes SUCCEEDED with a status message naming the field and the problem — a run that could never have worked is yours to fix, not a failure of the actor. Rejected before any request: a missing destination or IATA code, a malformed IATA code, a checkOut on or before checkIn, a date in the past, adults outside 1–14, a negative maxItems, and a destination carrying characters no place name has. That last one matters because Expedia answers those with 403 Access Denied rather than a result.

How do I get more than 200 hotels for a city?

Run one search per neighbourhood instead of one per city. Expedia treats Vilnius Old Town and Vilnius as different regions, each with its own 200-property ceiling, so several neighbourhood searches reach deeper into a city than one city-wide search ever can.

Which currency are the prices in?

Whatever Expedia serves the run, parsed out of the display string into currency — USD on the runs measured here. price_display always keeps the original text, so you can see exactly what Expedia said even if the symbol is one the parser doesn't map.

Can I schedule it?

Yes — use Apify's scheduler for recurring runs and a webhook to push each finished dataset onward. For hotels, join runs on hotel_id plus checkin/checkout, since the price only means anything for a specific stay.

Is scraping this legal?

You are responsible for how you use the data. Scrape only public product data and comply with Expedia's Terms and applicable law (e.g. GDPR/CCPA for personal data).


How does it work under the hood?

The actor impersonates Expedia's current Android app — app version and an okhttp TLS fingerprint, via impit — and calls the same private GraphQL surfaces the app does: region resolution, property search, and one call each for amenities, gallery, rating summary and fees when details are on, plus one for reviews. Resolved region ids are cached so a retry does not re-resolve them. Responses are normalised into the flat records above. No browser, no HTML scraping — just clean, structured data.


Good to know

  • Scrape only public product data and respect Expedia's Terms of Service and robots policy.
  • For personal data, comply with GDPR/CCPA and applicable law — you are the data controller.
  • Fields can be null when Expedia doesn't expose them for a given record.

Found this useful?

Give the Expedia Hotel & Flight Scraper a star on Apify and check out my other apify-*-api-scraper actors.