# Expedia Hotels Scraper - Prices, Ratings & Reviews (`abotapi/expedia-universal-scraper`) Actor

From $1/1K. Scrape Expedia.com hotel listings and reviews for any destination. Extract prices, ratings, review text, photos, location details, hotel information, and more in clean, structured data.

- **URL**: https://apify.com/abotapi/expedia-universal-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Travel, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 88 total users, 14 monthly users, 84.4% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Expedia Hotel Scraper: Prices, Ratings & Reviews

Expedia Hotel Scraper turns Expedia hotel listings and guest reviews into structured data. Search any destination or paste property and search URLs, and get back hotel cards with prices, star ratings, guest ratings and photos for your chosen dates, or flip a toggle to pull individual review records instead. Filter by price, star rating and sort order, cap review depth per hotel, and export to JSON, CSV or Excel, or read the results straight through the API.

### Why This Scraper?

- **Two ways in.** Search by a plain destination name, or paste any Expedia link: hotel pages (any Expedia country site or the mobile host), share links from the Expedia app (expe.onelink.me and similar, resolved automatically), search result pages and destination guide pages, or just a bare property id.
- **Two outputs from the same input.** Aggregate hotel cards by default, or turn on Scrape reviews only for individual review records, one row per review.
- **Every filter guests use.** Check in and check out dates, adults, rooms, children with ages, minimum and maximum nightly price, star rating, guest rating, amenities, property type, cancellation/payment options, meal plan, traveler experience, accessibility, neighbourhood, brand and property name, typed as run input or read straight out of a pasted Expedia search URL. Turn on Include property details (address, coordinates, amenities, policies, fees, nearby places) or Include rooms and rates (per-room rate plans, cancellation terms, meal plan, pay-later, scarcity) for a deeper crawl.
- **Review depth on demand.** Cap reviews per hotel (default 20, down from an earlier default of 25, the same fleet-standard cap used across other review-capable actors), embed a sample of reviews inside each hotel card, or pull every review as its own record with sentiment, stay details, helpful-vote counts, verified/translated flags and management replies. Filter and sort reviews by date, rating, and relevance/newest/highest/lowest.
- **30 locales and 36 currencies.** Cards and reviews come back in the guest's own language and currency.
- **Built for schedules.** Incremental mode returns only new, updated and reappeared hotels on repeat runs against the same search or URLs, and a blocked run fails loudly instead of quietly returning an empty dataset.
- **Resume support.** Continue an interrupted large destination pull without paying twice for hotels already collected in an earlier run.

### Use Cases

- **Rate and price monitoring:** track nightly prices, discounts and availability for a set of properties across a date range.
- **Reputation and sentiment analysis:** pull every review for a property to find recurring compliments, complaints and how management responds.
- **Scheduled market watching:** run the same destination or property list on a schedule with incremental mode and only pay for what changed.
- **Travel content and comparison sites:** build destination guides with hotel photos, star ratings and pricing pulled straight from search results.
- **Competitive benchmarking:** compare star rating, guest rating and price band for a shortlist of properties in one destination.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `mode` | `"search"` (also `"url"`, `"reviews"`) |
| `placeName` / `hotelId` / `hotelUrl` | `"Hotel Example Paris"` / `"11111"` / Expedia link to the property |
| `hotelOverallRating` / `hotelRatingLabel` / `hotelTotalReviews` | `9.2` (0 to 10 scale) / `"Wonderful"` / `1843` |
| `hotelCategoryRatings` / `hotelRatingDistribution` | `{"Cleanliness": 9.4, "Staff": 9.3}` / per-star-value review counts (reviews mode) |
| `provider` / `locale` / `scrapedAt` | `"expedia"` / `"en_US"` / `"2026-04-18T07:30:00.000Z"` |
| `extra.priceFrom` / `extra.priceFormatted` / `extra.priceStrikeout` / `extra.totalPrice` / `extra.totalPriceFormatted` / `extra.priceCurrency` | `420` / `"$420 nightly"` / `"$600"` (present when discounted) / stay total for the dates searched / `"USD"` |
| `extra.starRating` / `extra.vacationRental` / `extra.description` / `extra.images` / `extra.gallery` | `5` (the property's star class) / `false` / short property description / gallery image URLs / the same photos as `{id, url, description}` with the site's own caption |
| `extra.searchRank` / `extra.regionId` / `extra.badges` / `extra.priceMessages` | position in the site's own result list / the site's region id the card was listed under / every badge the card shows, e.g. `["VIP Access", "$58 off"]` / the price block's own lines, e.g. `["$237 nightly", "Total with taxes and fees", "for 2 nights", "Nov 5 - Nov 7"]` |
| `extra.neighborhood` / `extra.highlightedAmenities` / `extra.offerMessages` / `extra.dealBadge` / `extra.availabilityMessage` | `"Le Marais"` / `["Free WiFi", "Pool", "Spa"]` (often empty: the site does not always list amenities on the card; turn on Include property details for the full list) / site-shown promo text, e.g. `"Member deal"`, `"We have 1 left at this price"` |
| `extra.freeCancellation` / `extra.sponsored` | `true` / `false` (whether this card is a paid placement in this search) |
| `extra.address` / `extra.latitude` / `extra.longitude` / `extra.staticMapImage` | property details (when Include property details is on, always in URL mode): the one-line address the site shows, coordinates and the property's map image. `extra.city` / `extra.country` / `extra.postalCode` are declared for compatibility but stay empty: the site's property payload carries the address as one line, not split into parts |
| `extra.amenities` / `extra.amenityGroups` / `extra.highlights` / `extra.faqs` / `extra.policies` / `extra.importantInformation` / `extra.nearbyPlaces` / `extra.locationDescription` / `extra.vipAccess` | property details: amenities (flat and by category), policies, fees and rules, FAQs, nearby places, the neighbourhood write-up |
| `extra.rooms[]` | per-room rate plans: name, bed type, occupancy, price, cancellation policy, meal plan, pay-later, availability, scarcity badge (when Include rooms and rates is on) |
| `extra.searchDestination` / `extra.checkIn` / `extra.checkOut` / `extra.adults` / `extra.childrenAges` | the search and stay context the price was quoted for |
| `extra.reviews` | up to Max Reviews per Hotel inline review objects, when Include reviews in hotel cards is on |
| `reviewId` / `reviewTitle` / `reviewText` | review identity and body (reviews mode) |
| `reviewRating` / `reviewRatingLabel` | `10` / `"Excellent"` |
| `authorName` | `"Sarah"` |
| `reviewDate` / `stayDate` / `stayNights` / `stayedMonth` | `"Oct 14, 2025"` / `"Stayed 3 nights in Oct 2025"` / `3` / `"October 2025"` |
| `helpfulVoteCount` / `verified` / `verificationLabel` / `isTranslated` / `reviewPosition` / `reviewLocale` / `reviewBrand` | review-quality and provenance signals (reviews mode): votes, the site's own "Verified review" label, machine translation, rank in the chosen sort, the market it was written on, and the brand it was collected on when the site names one |
| `sentiment` | `{"liked": ["location", "staff"], "disliked": []}` |
| `hotelResponse` / `hotelResponseAuthor` / `hotelResponseDate` | management reply text, author and date |
| `photos` / `customData` | guest submitted photo URLs (reviews mode) / caller's own `userData` echoed back from a pasted start URL, when set |

Scrape reviews only mode returns one record per review using the review fields above. Incremental mode adds `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt` to hotel card records so you can tell what changed since the previous run.

#### Output views

The dataset's **Output** tab has one table per record kind, so you never have to open the raw JSON:

- **Results Overview**: one row per hotel card (or per review in Scrape reviews only runs) with stars, guest rating, nightly price, stay total, area, free cancellation, deal and link.
- **Property details**: address, coordinates, amenities by category, policies, fees and rules, nearby places, FAQs and the map image.
- **Rooms and rates**: each hotel's room types and rate plans for your dates.
- **Guest reviews**: review rows with rating, title, text, guest, stay, sentiment, helpful votes, verification and hotel reply, plus the reviews embedded in hotel cards.
- **Changes since the last run**: incremental mode's change type, changed fields, first/last seen and price.

The underlying dataset always holds every field above regardless of which view you look at.

### How to Use

1. Pick a **mode**: `search` (by destination) or `url` (paste hotel or search URLs).
2. Set **Check in** and **Check out** dates (required), plus adults, rooms, children, and any price, star rating or sort filters.
3. Turn on **Scrape reviews only** for individual review records, or leave it off for hotel cards, and set **Max items** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Search a destination:**

```json
{
  "mode": "search",
  "destination": "Paris",
  "checkIn": "2026-11-10",
  "checkOut": "2026-11-13",
  "adults": 2,
  "maxItems": 50
}
```

**Filter by price and star rating:**

```json
{
  "mode": "search",
  "destination": "Tokyo",
  "checkIn": "2026-11-10",
  "checkOut": "2026-11-13",
  "starRatings": ["4", "5"],
  "minPrice": 150,
  "maxPrice": 400,
  "sortBy": "REVIEW"
}
```

**Fetch specific hotels by URL:**

```json
{
  "mode": "url",
  "startUrls": [
    { "url": "https://www.expedia.com/Example-City-Hotels-Sample-Hotel.h11111.Hotel-Information" },
    { "url": "11112" },
    { "url": "https://www.expedia.com/Hotel-Search?destination=Barcelona&d1=2026-11-10&d2=2026-11-13&adults=2&price=0&price=150" },
    { "url": "https://www.expedia.com/Barcelona-Hotels.d178275.Travel-Guide-Hotels" }
  ],
  "checkIn": "2026-11-10",
  "checkOut": "2026-11-13"
}
```

**Reviews only for a destination:**

```json
{
  "mode": "search",
  "destination": "New York",
  "checkIn": "2026-11-10",
  "checkOut": "2026-11-13",
  "reviewsOnly": true,
  "maxItems": 500
}
```

**Supported URL shapes (URL mode, `startUrls`):**

| Shape | Example | Behavior |
|---|---|---|
| Property page | `expedia.com/Paris-Hotels-Sample.h11111.Hotel-Information` (also bare `/h11111.Hotel-Information` or `?HotelID=11111`) | Fetches that one property's full card. Priced for the Check in / Check out, adults, children and rooms inputs; when a pasted link carries its own stay (`chkin`, `chkout`), the log says so and the inputs still apply (set them to that stay to price it). |
| Bare property id | `11111` or `h11111` | Same as a property page. |
| Hotel-Search (listing) page | `expedia.com/Hotel-Search?destination=Barcelona&d1=...&d2=...&adults=...&rooms=...&sort=...` | Paginates the same search this URL would show on the site. `startDate`/`endDate`/`chkin`/`chkout` are also accepted as check-in/out aliases. |
| Destination / neighbourhood / theme landing page | `expedia.com/Paris-Hotels.d612707.Travel-Guide-Hotels` (including a themed variant with an extra `-t<theme>` token) | Converts the region id in the URL into the same paginated search as a Hotel-Search URL. |
| Expedia deeplink | `expe.onelink.me/...`, `expe.app.link/...`, `trip.expedia.com/...` | Resolved automatically to the matching property or search URL above before fetching. |
| Localized Expedia TLD or mobile host | `expedia.co.uk/...`, `expedia.de/...`, `m.expedia.com/...` | Accepted by any of the shapes above (classification doesn't check the host); note the fetch itself always goes through the `www.expedia.com` API, so a localized TLD's own market pricing isn't guaranteed. |

**Default start URLs:** if you run URL mode and leave the Hotel URLs field untouched (or omit `startUrls` from an API call), the actor scrapes one example of each main shape: a property page, a Hotel-Search link and a destination guide page. Replace the list with your own links, or use search mode, to avoid scraping the samples.

**Not yet supported:** Hotels.com (`hotels.com/ho<id>/...`) and Vrbo (`vrbo.com/<id>`) links are recognized but ride a separate catalog/ID space from Expedia hotels; the actor logs a clear warning and skips them rather than fetching the wrong property or silently returning nothing.

Filters embedded in a pasted Hotel-Search URL are applied exactly like the matching inputs: star rating (`star`), price range (`price=<min>&price=<max>`, the form the site's own price slider writes), guest rating, amenities, property type, payment/cancellation, meal plan, traveler experience, accessibility, neighbourhood, brand and property name (`hotelName`). A price parameter in any other form is not guessed: the actor logs a warning and Min/Max Price apply instead. Stay dates in a pasted Hotel-Search link that are already in the past are replaced by the run's Check in / Check out, with a warning. In URL mode the children ages and room count inputs now also price hotel-page cards (previously those cards were quoted for adults only, one room).

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/expedia-universal-scraper").call(run_input={
    "mode": "search",
    "destination": "Paris",
    "checkIn": "2026-11-10",
    "checkOut": "2026-11-13",
})
for hotel in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(hotel["placeName"], hotel["extra"].get("priceFrom"))
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/expedia-universal-scraper').call({
  mode: 'search',
  destination: 'Paris',
  checkIn: '2026-11-10',
  checkOut: '2026-11-13',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips hotels already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same destination search or URL list. Each hotel is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED`, or `EXPIRED` (only after a run that scanned the whole tracked scope, and only with `emitExpired`). `stateKey` names or shares the stored state. Scrape reviews only runs do not participate in incremental mode. With incremental mode off, output is exactly as before. These fields are ignored for change detection by default, and still returned in the output: `extra.checkIn` and `extra.checkOut` (leaving Check-in/Check-out empty makes the actor default to "tomorrow"/"the day after", which rolls forward every single day on a scheduled run even though nothing about the hotel itself changed), `extra.availabilityMessage` and `extra.rooms.availabilityMessage` (the site's own scarcity copy, e.g. "We have 1 left at this price", which moves with every other booking on the property), `extra.sponsored` (whether a search placed this card as an ad, a placement decision, not a property attribute), `extra.searchRank` (the card's position in this search, which moves whenever any other hotel's price does), `extra.priceMessages` (the price block's text lines, which carry the stay dates), `extra.highlightedAmenities` and `extra.offerMessages` (card display text the site does not always include) and `extra.staticMapImage` (a map-tile link; the location itself is compared through the coordinates). `customData` (the caller's own echoed-back `userData`) is likewise never treated as a change, and the card photo set (`extra.images`, `extra.gallery`) is never compared because the site rotates which photos a card shows. After an update of this actor, hotels tracked by an earlier version are not marked `UPDATED` just because new fields were added or because their star class (`extra.starRating`, previously always empty) is now filled in; those fields are compared from the following run on. List any other fields (or `extra.*` nested paths, e.g. `extra.priceFormatted`) you don't want treated as changes in `ignoreFieldsForChanges`.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (by destination) or `url` (paste hotel or search URLs). |
| `destination` | string | (none) | Place name for search mode, for example Paris, New York, Tokyo. Resolved automatically via Expedia's own lookup first, a geocoder fallback second; a name neither can place fails the run loudly (naming Destination ID and URL mode as the way out) instead of returning an empty dataset. A bare region id (`2734`) or `2734\|Paris` is taken as-is. |
| `destinationId` | string | (none) | Optional escape hatch, not needed for normal use. Numeric Expedia region ID, the number after `.d` in an Expedia URL. Set this to skip destination resolution entirely. |
| `startUrls` | array | (none) | Expedia property, share, search, or destination-landing links (or bare property ids) for url mode. See the URL-shape table above. The input is prefilled with one example of every shape. |
| `checkIn` | string | `1 day` | Check in date. Absolute (`YYYY-MM-DD`) or relative, e.g. `1 day`. Required. |
| `checkOut` | string | `2 days` | Check out date. Absolute or relative, e.g. `2 days`. Required. |
| `adults` | integer | `2` | Adult guests, 1 to 14. |
| `children` | array | `[]` | Ages of children, 0 to 17, one entry per child. |
| `rooms` | integer | `1` | Rooms to book, 1 to 8. |
| `minPrice` | integer | (none) | Minimum nightly price, compared with the nightly price each hotel card shows (`extra.priceFrom`, selected currency). Hotels without a price for your dates are skipped while a price filter is set; the log says how many were skipped. The site no longer narrows its own results by price (measured 2026-10-06), so the actor applies this filter to the cards it receives. |
| `maxPrice` | integer | (none) | Maximum nightly price, same rules as `minPrice`. If five result pages in a row hold no hotel in your range the search stops with a warning (widen the range or sort by price). |
| `starRatings` | array | `[]` | One or more of `1` to `5`. Empty means all star ratings. |
| `minGuestRating` | string | (none) | Minimum guest review score (site-native filter). |
| `amenities` | array | (none) | Filter by amenity (pool, free WiFi, parking, and similar). |
| `propertyTypes` | array | (none) | Filter by property type (hotel, apartment, resort, and similar). |
| `paymentOptions` | array | (none) | Filter by cancellation/payment option (free cancellation, pay later, and similar). |
| `mealPlans` | array | (none) | Filter by meal plan (breakfast included, all-inclusive, and similar). |
| `travelerExperience` | array | (none) | Filter by traveler-experience tag (business, family, romance, and similar). |
| `accessibilityFeatures` | array | (none) | Filter by accessibility feature. |
| `neighborhoodIds` | array | (none) | Filter by neighbourhood (site-native IDs). |
| `brandIds` | array | (none) | Filter by hotel brand (site-native IDs). |
| `propertyName` | string | (none) | Only hotels whose name matches this text, e.g. `Marriott` (the site's own Search by property name). Also read from a pasted Hotel-Search link's `hotelName` parameter. |
| `sortBy` | string | `RECOMMENDED` | `RECOMMENDED`, `PRICE_LOW_TO_HIGH`, `PRICE_HIGH_TO_LOW`, `DISTANCE`, `PROPERTY_CLASS`, `REVIEW`, or `REVIEW_RELEVANT`. |
| `includePropertyDetails` | boolean | `false` | Search mode only: deep-crawl each hotel for address, coordinates, amenities, policies, fees and nearby places. |
| `includeRooms` | boolean | `false` | Deep-crawl each hotel for room/rate-plan detail (cancellation, meal plan, pay later, scarcity). |
| `maxPropertyImages` | integer | (none) | Cap on gallery photos returned per hotel. |
| `reviewsOnly` | boolean | `false` | Output individual review records instead of hotel cards. |
| `includeReviews` | boolean | `false` | Embed up to Max Reviews per Hotel reviews inside each hotel card. Ignored when Scrape reviews only is on. |
| `maxReviewsPerHotel` | integer | `20` | Cap on reviews embedded per hotel card (default 20). Not used by Scrape reviews only, where each review row is billed as a normal result. |
| `reviewsFrom` | string | (none) | `YYYY-MM-DD`. Only keep reviews on or after this date. |
| `reviewsTo` | string | (none) | `YYYY-MM-DD`. Only keep reviews on or before this date. |
| `reviewSort` | string | (none) | Review order: relevant, newest, highest or lowest rated. |
| `minReviewRating` | integer | (none) | Keep only reviews at or above this rating. |
| `maxReviewRating` | integer | (none) | Keep only reviews at or below this rating. |
| `includeReviewPhotos` | boolean | `true` | Include guest-submitted photo URLs on review records. |
| `includeOwnerResponses` | boolean | `true` | Include the hotel's management reply on review records. |
| `includeCategoryRatings` | boolean | `true` | Attach aggregate category ratings (cleanliness, staff, and similar) to review records in Scrape reviews only mode. |
| `currency` | string | `USD` | 3 letter currency code for prices on hotel cards. |
| `locale` | string | `en_US` | Language and region for card content and review language. |
| `maxItems` | integer | `20` | Hard cap on total records saved to the dataset. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run; skips hotels already collected there. |
| `incrementalMode` | boolean | `false` | Return only new, updated and reappeared hotels on repeat runs against the same search or URLs. |
| `stateKey` | string | (none) | Name or share an incremental mode state. Left empty, the actor derives one from the search or URLs and filters. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) hotels unchanged since the last incremental run. |
| `emitExpired` | boolean | `false` | Also return (and bill) hotels no longer found, after a complete scan. |
| `ignoreFieldsForChanges` | string\[] | `[]` | Extra output fields (or `extra.*` nested paths) to ignore when deciding whether a hotel is `UPDATED`, on top of the defaults always ignored: `extra.checkIn`, `extra.checkOut`, `extra.availabilityMessage`, `extra.rooms.availabilityMessage`, `extra.sponsored`, `extra.searchRank`, `extra.priceMessages`, `extra.highlightedAmenities`, `extra.offerMessages`, `extra.staticMapImage`, `customData` (see Resume and recurring updates above). |
| `proxy` | object | (none) | Proxy configuration. Prefilled to Apify's residential proxy group. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "mode": "search",
  "placeName": "Hotel Example Paris",
  "hotelId": "11111",
  "hotelUrl": "https://www.expedia.com/Example-City-Hotels-Sample-Hotel.h11111.Hotel-Information",
  "hotelOverallRating": 9.2,
  "hotelRatingLabel": "Wonderful",
  "hotelTotalReviews": 1843,
  "provider": "expedia",
  "scrapedAt": "2026-04-18T07:30:00.000Z",
  "extra": {
    "searchDestination": "Paris",
    "starRating": 5,
    "description": "Recently renovated boutique hotel steps from the city center",
    "priceFrom": 420,
    "priceFormatted": "$420 nightly",
    "priceStrikeout": "$600",
    "currency": "USD",
    "images": [
      "https://images.example.com/lodging/sample-1.jpg",
      "https://images.example.com/lodging/sample-2.jpg"
    ],
    "checkIn": "2026-11-10",
    "checkOut": "2026-11-13",
    "adults": 2,
    "childrenAges": [],
    "vacationRental": false,
    "searchRank": 1,
    "regionId": "2734",
    "badges": ["VIP Access", "$58 off"],
    "priceMessages": ["$420 nightly", "Total with taxes and fees", "for 3 nights", "Nov 10 - Nov 13"]
  }
}
```

### Plan Requirement

Expedia protects its pages heavily. Datacenter connections are blocked, so this actor does not work on a datacenter proxy: it needs a residential proxy connection (prefilled), and each run also has to pass the site's human-verification step in a real browser session before any page loads. That step is automatic, but its pass rate varies from run to run, so a run can occasionally fail to get through and ends with a clear failure message instead of empty results. Residential access comes from your Apify proxy settings; no separate third-party account is required. Keep the proxy group on Residential, and do not switch it to a datacenter group.

**On an Apify plan without Residential proxy access:** the run still proceeds on a backup connection, but the log carries a boxed warning that the selected connection is unavailable on a free plan and some pages may still be unavailable, with a link to upgrade (https://apify.com/pricing) or supply your own proxy URLs; a follow-up line notes when that backup connection actually served rows. If your account has no proxy access at all, the run stops with a message saying a paid Apify plan is required (or to check the credentials of any proxy URLs you supplied). If the backup connection never manages to load a usable page, the run ends **FAILED** with a clear status message telling you to upgrade or try again; it never reports SUCCEEDED with zero results for a blocked run.

### FAQ

#### How much does it cost?

You pay per record returned. The Hotel detail enrichment event is added for each hotel returned from a property URL in URL mode. Searches and Scrape reviews only runs pay just the per-record rate. Hotels suppressed as unchanged in incremental mode are never billed. The **Pricing** tab shows the current rates. Use **Max items** and **Max Reviews per Hotel** to cap the cost of any run.

#### Is it legal to scrape Expedia?

This actor collects only publicly available listing and review data. You are responsible for how you use it: follow Expedia's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Prices and availability change constantly and are not guaranteed to still be bookable by the time you read them.

#### What is the difference between Resume and Incremental mode?

They solve different problems and are the actor's most common point of confusion. **Resume** (`resumeFromRunId`) continues one specific interrupted run so it doesn't restart from scratch. **Incremental mode** is for running the same destination search or hotel/search URLs again and again, for example daily, and getting back only the hotels that are new, changed, or gone since the last run.

#### Can I get only new or changed hotels on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Repeat runs then return only new, updated and reappeared hotels, and unchanged ones are not billed unless you turn on **Emit unchanged**.

#### Why did my run return fewer hotels than expected?

Price, star rating and destination filters narrow the result set, and **Max items** caps the total regardless of filters. A search with no matching hotels for the chosen dates and filters also returns fewer records without failing the run. A destination name that can't be resolved at all fails the run loudly instead (see below); it never silently returns fewer rows.

#### Why did my run fail instead of returning an empty dataset?

The run fails loudly, with a clear status message, in three cases instead of silently returning an empty dataset: Expedia blocks every single request even after retrying with a different session; **Resume** points at a run or dataset that can't be read (or clashes with existing incremental state for the same search); or the **Destination** text can't be resolved by either Expedia's own lookup or the geocoder fallback (try a more specific name, or set **Destination ID** directly). A search with genuinely zero matching hotels for the chosen dates/filters still succeeds with zero records; that is a real "no results," not a resolution failure.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more travel data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🏨 <a href="https://apify.com/abotapi/trip-com-scraper"><b>Trip.com Hotels, Rooms &amp; Reviews Scraper</b></a><br>Pull structured hotel listings from trip.com with prices, room types, amenities...</td><td>🏨 <a href="https://apify.com/abotapi/booking-com-scraper"><b>Booking.com Hotels, Prices and Reviews Scraper</b></a><br>Scrape booking.com hotels, apartments, villas and hostels. Search any destination with...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/wego-com-scraper"><b>Wego Scraper</b></a><br>Scrape Wego hotels by city or search URL. Get rates, providers, booking links...</td><td>🏨 <a href="https://apify.com/abotapi/vrbo-vacation-rentals-scraper"><b>Vrbo Scraper</b></a><br>Extract vacation rental data from Vrbo.com by location, region, coordinates, property...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/airbnb-stays-reviews-scraper"><b>Airbnb Scraper</b></a><br>Scrape Airbnb stays by destination or link: nightly and total price, rating breakdown...</td><td>🏨 <a href="https://apify.com/abotapi/klook-activities-scraper"><b>Klook Scraper</b></a><br>Scrape Klook activities, tours, attractions and travel experiences from search pages or...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/expedia-universal-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Search mode runs a destination query and paginates results. URL mode fetches hotels by property or search URL.

## `destination` (type: `string`):

Search mode only. Type a place name the way you would on the site, for example Paris, New York or Tokyo; the Actor looks it up itself. A bare Expedia region id (for example 2734) or the older "2734|Paris" form also works.

## `destinationId` (type: `string`):

Optional. You usually don't need this -- the Actor resolves the Destination text above on its own (Expedia's own lookup first, a geocoder fallback second). Set this only if you already know the numeric region ID from an Expedia URL (for example 2734 for Paris, 178293 for New York; it is the number after .d in any Expedia URL) and want to skip resolution entirely.

## `startUrls` (type: `array`):

URL mode only. Paste any Expedia link: a hotel page (…h891081.Hotel-Information, also on a localized site such as expedia.co.uk or the mobile m.expedia.com host), a share link (expe.onelink.me / expe.app.link), a Hotel-Search results page (its dates, guests, sort and filters are used), or a destination guide page (…d612707.Travel-Guide-Hotels). A bare property id such as 891081 also works. Hotel cards are priced for the Check in / Check out inputs (a hotel link's own dates are reported in the log, not used); a Hotel-Search link's own dates are used unless they are in the past. See the README for the full URL-shape table.

## `checkIn` (type: `string`):

Required. Pick a date, or type a relative value such as 1 day (tomorrow) or 7 days.

## `checkOut` (type: `string`):

Required. Pick a date, or type a relative value such as 2 days (day after tomorrow).

## `adults` (type: `integer`):

Number of adult guests. Affects pricing and availability in the results.

## `children` (type: `array`):

Add one entry per child and type the age in years (0 to 17). Two kids age 5 and 10? Add 5 and 10. Two 5 year olds? Add 5 twice.

## `rooms` (type: `integer`):

Number of rooms to book. Each room uses the same adult and children configuration.

## `minPrice` (type: `integer`):

Minimum nightly price (the nightly price each hotel card shows, in the selected currency). Hotels without a price for your dates are skipped while a price filter is set. Leave empty for no minimum.

## `maxPrice` (type: `integer`):

Maximum nightly price (the nightly price each hotel card shows, in the selected currency). Hotels without a price for your dates are skipped while a price filter is set. Leave empty for no maximum.

## `starRatings` (type: `array`):

Filter by star rating. Select one or more. Leave empty to include all star ratings.

## `minGuestRating` (type: `string`):

Only return properties rated at least this well by guests. Leave empty for any rating.

## `amenities` (type: `array`):

Only return properties that have ALL of the selected amenities. Leave empty for no amenity filter.

## `propertyTypes` (type: `array`):

Only return properties of ANY of these kinds (hotel, hostel, apartment and so on). Leave empty for all types.

## `paymentOptions` (type: `array`):

Only return properties offering the selected booking terms.

## `mealPlans` (type: `array`):

Only return properties offering the selected meal plans.

## `travelerExperience` (type: `array`):

Only return properties tagged for ALL of the selected kinds of trip.

## `accessibilityFeatures` (type: `array`):

Only return properties with the selected accessibility features.

## `neighborhoodIds` (type: `array`):

Limit results to these neighborhoods. Use the number after neighborhood= in an Expedia search link after you pick a neighborhood filter on the site (for example 178676 is Montmartre in Paris). Pasting that whole search link into Start URLs applies it too.

## `brandIds` (type: `array`):

Limit results to ANY of these hotel brands. Use the number after hotel_brand= in an Expedia search link after you pick a brand filter on the site (for example 2141 is ibis). Pasting that whole search link into Start URLs applies it too.

## `propertyName` (type: `string`):

Only hotels whose name matches this text, e.g. Marriott or Ibis (the site's own Search by property name filter). Search mode, and pasted Hotel-Search links (their hotelName parameter).

## `sortBy` (type: `string`):

Order in which hotels are returned.

## `includePropertyDetails` (type: `boolean`):

Search mode only: also fetch each hotel's address, map coordinates, full amenity list, policies, fees and what's nearby (one extra request per hotel). URL mode always includes these for hotel links.

## `includeRooms` (type: `boolean`):

Also fetch every room type with its rate options: price for your dates, cancellation terms, meal plan, pay now or pay later. Works in both modes. Adds roughly 0.2 to 1 MB of proxy traffic per hotel.

## `maxPropertyImages` (type: `integer`):

Keep at most this many photo links per hotel. Leave empty to keep all.

## `reviewsOnly` (type: `boolean`):

When enabled, the output is individual review records (one row per review) instead of hotel cards. Works in both Search and URL input types.

## `includeReviews` (type: `boolean`):

When enabled, each hotel card gets up to Max Reviews per Hotel reviews embedded under extra.reviews. Ignored if Scrape reviews only is on.

## `maxReviewsPerHotel` (type: `integer`):

Cap on reviews embedded in each hotel card (default 20). Only applies when Include reviews in hotel cards is on. Ignored in Scrape reviews only mode (Max Items caps the overall total there, and each review row is billed as a normal result).

## `reviewsFrom` (type: `string`):

YYYY-MM-DD. Only keep reviews on or after this date. Speeds up runs when you only need recent reviews.

## `reviewSort` (type: `string`):

Order in which reviews are collected. Leave empty for the site's default (most relevant). When a review date range is set, empty means newest first.

## `reviewsTo` (type: `string`):

YYYY-MM-DD. Only keep reviews on or before this date.

## `minReviewRating` (type: `integer`):

Only keep reviews rated at least this (1 to 10).

## `maxReviewRating` (type: `integer`):

Only keep reviews rated at most this (1 to 10). Combine with Lowest rated first to collect complaints quickly.

## `includeReviewPhotos` (type: `boolean`):

Keep links to guest photos on each review.

## `includeOwnerResponses` (type: `boolean`):

Keep the hotel's reply to each review (reviews only mode).

## `includeCategoryRatings` (type: `boolean`):

Attach hotel level aggregate ratings (cleanliness, staff, location, and similar categories) to each review record.

## `currency` (type: `string`):

Currency requested for prices. Expedia.com quotes its own prices in US dollars whatever is selected here, so check extra.priceCurrency on each hotel for the currency the prices are actually in.

## `locale` (type: `string`):

Language and region for content on hotel cards and for review language.

## `maxItems` (type: `integer`):

Hard cap on the total number of records saved to the dataset across all pages and hotels.

## `proxy` (type: `object`):

Proxy settings. Residential proxies are recommended for reliable access.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large crawl of hotels without returning or charging for hotels already collected there. Use this after an interrupted run, or when continuing a destination pull in another run. For recurring daily monitoring of the same search or URLs, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring of the same destination search or hotel/search URLs. The first run returns all matching hotels as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED hotels. Turn on "Emit unchanged" or "Emit expired" only when you also want those hotels returned (and billed). Hotels are compared on their listing content (name, ratings, review count, price and totals, rooms, amenities, policies, address, deals); the photo set (extra.images, extra.gallery) is never compared because the site rotates it, and by default these are also left out of change detection but still returned: extra.checkIn and extra.checkOut (they roll forward daily when Check-in/Check-out are left empty), availability scarcity messages, sponsored flag, search rank, price messages, map image and search URL. Add more in Ignore fields when detecting changes below. State is kept separately for each mode/destination/URL/filter setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the destination/URLs and filters. Different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return hotels that have not changed since the last run, marked UNCHANGED. This returns and bills extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return hotels that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search/URLs, not when Max Items capped it or when Resume was used. This returns and bills extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Output field names (or extra.\* nested paths) that should NOT make a hotel count as UPDATED. Always ignored by default: extra.checkIn and extra.checkOut (left empty, Check-in/Check-out default to "tomorrow"/"the day after", which rolls forward every day on a scheduled run), the site's scarcity text (extra.availabilityMessage), ad placement (extra.sponsored), the card's search rank (extra.searchRank), the price block's text lines that carry the dates (extra.priceMessages), the card's highlighted amenities and booking-terms text (extra.highlightedAmenities, extra.offerMessages), the map link (extra.staticMapImage) and your own customData. Add fields such as hotelTotalReviews or extra.priceFormatted here if changes to them are not relevant to your monitoring. Ignored fields are still returned in the output, only change detection skips them.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify > Settings > Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON; the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "destination": "Paris",
  "startUrls": [
    {
      "url": "https://www.expedia.com/Paris-Hotels-Hotel-Trianon-Rive-Gauche.h891081.Hotel-Information"
    },
    {
      "url": "https://www.expedia.co.uk/Paris-Hotels-Terrass-Hotel.h868.Hotel-Information"
    },
    {
      "url": "https://expe.onelink.me/hnLd/5dfidl8b"
    },
    {
      "url": "https://www.expedia.com/Hotel-Search?destination=Barcelona&d1=2027-08-01&d2=2027-08-03&adults=2"
    },
    {
      "url": "https://www.expedia.com/Paris-Hotels.d612707.Travel-Guide-Hotels"
    }
  ],
  "checkIn": "1 day",
  "checkOut": "2 days",
  "adults": 2,
  "children": [],
  "rooms": 1,
  "starRatings": [],
  "sortBy": "RECOMMENDED",
  "includePropertyDetails": false,
  "includeRooms": false,
  "reviewsOnly": false,
  "includeReviews": false,
  "maxReviewsPerHotel": 20,
  "includeReviewPhotos": true,
  "includeOwnerResponses": true,
  "includeCategoryRatings": true,
  "currency": "USD",
  "locale": "en_US",
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `results` (type: `string`):

Records matching the selected mode

## `details` (type: `string`):

Address, amenities, policies, fees and nearby places

## `rooms` (type: `string`):

Room types and rate plans

## `reviews` (type: `string`):

Review rows and embedded reviews

## `changes` (type: `string`):

Incremental mode change rows

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "search",
    "destination": "Paris",
    "startUrls": [
        {
            "url": "https://www.expedia.com/Paris-Hotels-Hotel-Trianon-Rive-Gauche.h891081.Hotel-Information"
        },
        {
            "url": "https://www.expedia.co.uk/Paris-Hotels-Terrass-Hotel.h868.Hotel-Information"
        },
        {
            "url": "https://expe.onelink.me/hnLd/5dfidl8b"
        },
        {
            "url": "https://www.expedia.com/Hotel-Search?destination=Barcelona&d1=2027-08-01&d2=2027-08-03&adults=2"
        },
        {
            "url": "https://www.expedia.com/Paris-Hotels.d612707.Travel-Guide-Hotels"
        }
    ],
    "checkIn": "1 day",
    "checkOut": "2 days",
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/expedia-universal-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "search",
    "destination": "Paris",
    "startUrls": [
        { "url": "https://www.expedia.com/Paris-Hotels-Hotel-Trianon-Rive-Gauche.h891081.Hotel-Information" },
        { "url": "https://www.expedia.co.uk/Paris-Hotels-Terrass-Hotel.h868.Hotel-Information" },
        { "url": "https://expe.onelink.me/hnLd/5dfidl8b" },
        { "url": "https://www.expedia.com/Hotel-Search?destination=Barcelona&d1=2027-08-01&d2=2027-08-03&adults=2" },
        { "url": "https://www.expedia.com/Paris-Hotels.d612707.Travel-Guide-Hotels" },
    ],
    "checkIn": "1 day",
    "checkOut": "2 days",
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/expedia-universal-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "search",
  "destination": "Paris",
  "startUrls": [
    {
      "url": "https://www.expedia.com/Paris-Hotels-Hotel-Trianon-Rive-Gauche.h891081.Hotel-Information"
    },
    {
      "url": "https://www.expedia.co.uk/Paris-Hotels-Terrass-Hotel.h868.Hotel-Information"
    },
    {
      "url": "https://expe.onelink.me/hnLd/5dfidl8b"
    },
    {
      "url": "https://www.expedia.com/Hotel-Search?destination=Barcelona&d1=2027-08-01&d2=2027-08-03&adults=2"
    },
    {
      "url": "https://www.expedia.com/Paris-Hotels.d612707.Travel-Guide-Hotels"
    }
  ],
  "checkIn": "1 day",
  "checkOut": "2 days",
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/expedia-universal-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/expedia-universal-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/cXyLXbDpgkeiKAhUp/builds/lFeg37zAdPfbOoAOS/openapi.json
