# Expedia Reviews Scraper — Hotels.com & Activities · $2.5/1K 💰 (`memo23/expedia-scraper`) Actor

\[Only $2.5💰] Expedia + Hotels.com + Things-To-Do activities at $2.50/1k — only scraper covering all 3 brands. Deeplink auto-resolution, date filter, 10+ regional TLDs, bare property IDs. 25+ fields: sentiment, traveled-with, manager responses, category ratings. JSON or CSV. Fast at scale.

- **URL**: https://apify.com/memo23/expedia-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Travel, Lead generation, MCP servers
- **Features:** Limited permissions
- **Stats:** 578 total users, 138 monthly users, 99.9% runs succeeded, 8 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $2.50 / 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.

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 Reviews Scraper — Hotels + Activities + Hotels.com, one actor

![How Expedia Reviews Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-expedia.png)

Scrape every review from  **Expedia hotels**, **Expedia Things-To-Do activities** (tours, attractions, day trips), **and Hotels.com** — all in one run, with deeplink resolution, date filtering, and 10+ regional TLDs supported. **The only Apify scraper that covers all 3 brands plus activities** under a single PPE actor. **$2.50 per 1,000 reviews.**

### Why use this scraper

- **🏨 3 brands in one actor** — Expedia hotels, Expedia activities (Things-To-Do), and Hotels.com share the same code path and same input array. No need to run separate scrapers per brand.
- **🎢 Activities / Things-To-Do support** — tours, museum tickets, day trips, transfers, bus tours, classes — anything bookable on Expedia's activities section. Rows are tagged `provider: "expedia-activity"` for easy filtering. **No other Apify Expedia scraper does activities.**
- **🔗 Deeplink auto-resolution** — paste `https://expe.app.link/KjB1EuacRSb` or `https://expe.onelink.me/...` and the scraper follows the redirect chain to the real hotel page. Useful for ingesting URLs from mobile-app sharing, marketing campaigns, and social media.
- **📅 Date filtering with smart pagination** — pass `reviewsFrom: "2025-01-01"` and the scraper stops paginating when it reaches older reviews. You only pay for the reviews you want. Perfect for monitoring-mode delta runs.
- **🌍 10+ regional TLDs** — `.com`, `.co.uk`, `.com.br`, `.de`, `.fr`, `.com.sg`, `.com.au`, `.es`, `.it`, `.nl`, `.jp` — all resolve via the same scraper.
- **🆔 Bare property IDs accepted** — paste `600217` or `h600217` instead of full URLs. Auto-expanded to `https://www.expedia.com/Hotel.h{id}.Hotel-Information`.
- **🏷️ Sentiment + traveled-with + management response** on hotel rows — liked/disliked highlights, "Traveled with family / partner / business", and hotel manager replies when present.

### Overview

Expedia Group operates several review surfaces: hotels (under `expedia.com`, `expedia.co.uk`, `expedia.de`, …), Things-To-Do activities (`/things-to-do/{slug}.a{id}.activity-details`), and the Hotels.com sister brand. Every Apify Expedia scraper before this one targets just one of these. This actor unifies all three plus four input shortcuts (long URL, short URL, bare ID, deeplink) so a buyer can ingest URLs from any source — CRM exports, mobile-app shares, Excel pastes — without preprocessing.

Every dataset row represents one review. Rows are tagged `provider: "expedia" | "expedia-activity" | "hotels"` so downstream consumers can split them apart. Hotel rows additionally carry the hotel-level aggregate (overall rating, total reviews, **full 5-category breakdown** — Cleanliness, Staff & service, Amenities, Property conditions & facilities, Location — **Expedia's AI-generated guest-sentiment summary** when available, **the property's full photo gallery** — typically 100-300 image URLs per hotel, and a **rich descriptive block** — name, star class, address, coordinates, description, categorized amenities, and highlights) when `includeCategoryRatings: true`.

### Supported inputs

#### All 7 Expedia Group hotel brands — one actor

Every Expedia Group hotel brand runs on the same backend, so a property resolves across all of them and the **output schema is identical** no matter which brand URL you paste. Paste a hotel URL from any of these — or just the bare property id, which is universal across the `.h`-id brands:

| Brand | Example hotel URL to paste |
|---|---|
| **Expedia** | `https://www.expedia.com/Las-Vegas-Hotels-Caesars-Palace.h41245.Hotel-Information` |
| **Hotels.com** | `https://www.hotels.com/ho434012/` (its `ho`-id auto-resolves to the unified property) |
| **Travelocity** | `https://www.travelocity.com/Las-Vegas-Hotels-Caesars-Palace.h41245.Hotel-Information` |
| **Orbitz** | `https://www.orbitz.com/Las-Vegas-Hotels-The-Palazzo.h1769973.Hotel-Information` |
| **Wotif** | `https://www.wotif.com/…h41245.Hotel-Information` |
| **CheapTickets** | `https://www.cheaptickets.com/…h41245.Hotel-Information` |
| **ebookers** | `https://www.ebookers.com/…h41245.Hotel-Information` (regional TLDs like `ebookers.ch` work too) |

Reviews are served from Expedia's backend for every brand, so a row's `provider` tag reads `expedia` (or `hotels` for a Hotels.com URL) regardless of the brand you pasted.

#### Input types

| Input type | Example URL | Provider tag | Output |
|---|---|---|---|
| **Expedia hotel (long slug)** | `https://www.expedia.co.uk/Kastoria-Hotels-Hotel-Anastassiou.h6136437.Hotel-Information` | `expedia` | One row per review + hotel aggregate |
| **Expedia hotel (short path)** | `https://www.expedia.com/h17369936.Hotel-Information` | `expedia` | Same |
| **Expedia hotel (minimal slug)** | `https://www.expedia.com/Hotel.h600217.Hotel-Information` | `expedia` | Same |
| **Bare property ID** | `600217` or `h600217` | `expedia` | Auto-expanded to `Hotel.h{id}.Hotel-Information` |
| **Expedia activity (Things-To-Do)** | `https://www.expedia.com.sg/things-to-do/singapore-hop-on-hop-off-bus-tour.a203344.activity-details` | `expedia-activity` | One row per review + activity metadata block (price, highlights, features) |
| **Hotels.com hotel** | `https://el.hotels.com/ho434012/hotel-anastassiou-kastoria-ellada/` | `hotels` | One row per review + hotel aggregate |
| **Expedia deeplink (`expe.app.link`)** | `https://expe.app.link/KjB1EuacRSb` | `expedia` | Auto-resolved to hotel page, then scraped |
| **Expedia deeplink (`expe.onelink.me`)** | `https://expe.onelink.me/hnLd/5dfidl8b` | `expedia` | Same |
| **Any mix of the above** | array of all 8 types | mixed | Scraper partitions and dispatches each correctly |
| **Regional Expedia TLDs** | `.co.uk`, `.com.br`, `.de`, `.fr`, `.com.sg`, `.com.au`, `.es`, `.it`, `.nl`, `.jp`, … | `expedia` | All work — same code path |
| 🆕 **Google Maps URL, Place ID, or Feature ID** | `https://www.google.com/maps/place/Hotel+Anastassiou+Kastoria/@40.5,21.2/...`, `ChIJRUjdceH8WRMRe-u4F6qScEI`, or `0x1359fce171dd4845:0x427092aa17b8eb7b` | `google-maps-resolved` | **Fully auto-resolved.** The hotel's name + coordinates are read from Google, matched against the Expedia Group catalog, and the matched property is scraped in the same run — on Expedia, Hotels.com, or both (see `googleMapsTarget`). A `google-maps-resolved` row documents the mapping (Expedia id, Hotels.com id, match score, distance). No confident match → a `google-maps-unresolved` row lists the closest candidates with ready-to-use URLs instead of guessing. Short share links (`maps.app.goo.gl`) work too. |

> ⚠️ **Not supported:** Expedia destination/region landing pages (`.../Place.d{id}.Place-To-Visit`) have no reviews of their own — they only list nearby hotels and tours. If you pass one, the actor emits a single explanatory row with a hint about the URL types it does support and **skips the network fetch entirely**. You don't get charged for dead URLs.

### Use cases

| Audience | Use case |
|---|---|
| **Hotel reputation managers** | Daily monitoring of new reviews across owned properties; alert on negative trends |
| **Hospitality consultants** | Benchmark a hotel against direct competitors in the same city; surface category-rating gaps |
| **Tour & activity operators** | Track Things-To-Do listings (your own + competitors) for review-rate, rating-band, recurring complaints |
| **Market analysts** | Geographic + price-tier sentiment analysis across Expedia + Hotels.com inventory |
| **Aggregator builders** | Power downstream review-aggregation SaaS with comprehensive multi-brand data |
| **PE / hospitality due diligence** | Sentiment + rating-trend snapshot of a target property before acquisition |
| **Academic / consumer-research** | Auto-translation patterns, multi-language review trends, traveler-companion demographics |

### How it works

1. **Classify each `startUrls[]` entry** — hotel / activity / Hotels.com / bare ID / deeplink / Google Maps input. Reject non-Expedia-Group hosts. Destination pages emit a single explanatory row.
2. **Resolve deeplinks & Google Maps inputs** — for `expe.app.link` / `expe.onelink.me`, follow redirects to the canonical hotel/activity URL. For Google Maps URLs / Place IDs / Feature IDs, read the place's name + coordinates from Google, match against the Expedia Group hotel catalog (name similarity + geo distance), map to the Hotels.com legacy id when targeted, and enqueue the matched property like any pasted URL — only high-confidence matches are scraped; everything else is reported transparently.
3. **For hotel URLs**: collect every review, plus (when `includeCategoryRatings: true`) the rating summary, the **full 5-category breakdown**, Expedia's **AI-generated guest-sentiment bullets** and the **property photo gallery**, all attached to each row.
4. **For activity URLs**: collect every activity review plus the activity's details (price, highlights, features). Paginate through review pages.
5. **For Hotels.com URLs**: Collect every Hotels.com review, then match the hotel to its **unified Expedia property ID** and add the same hotel-level blocks as the Expedia flow — so Hotels.com rows carry the identical aggregate block (overall rating, total reviews, category ratings, AI summary, photo gallery, property details, and the opt-in location/policies/rooms blocks).
6. **Date filter (when `reviewsFrom` is set)**: stop paginating as soon as a review older than the cutoff appears. Reviews are sorted newest-first by Expedia.
7. **Emit one flat row per review** to the Apify dataset. JSON or CSV export.

### Input configuration

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `startUrls` | `string[]` | yes | — | Any mix of hotel URLs (long / short / minimal), activity URLs, Hotels.com URLs, bare IDs, deeplinks |
| `maxItems` | `integer` | no | `1000` | Max review rows **per Start URL** (per hotel/activity), newest first — *not* a run total. A run with N URLs returns up to `maxItems × N` rows, so cost is deterministic and every hotel gets its own budget (no busy property starves the others). Each row = one paid `result` event. No effect in `skipReviews` mode (1 row/hotel). |
| `reviewsFrom` | `string` (YYYY-MM-DD) | no | — | Only emit reviews on/after this date. Triggers smart-pagination stop. Leave empty for all reviews. |
| `reviewsTo` | `string` (YYYY-MM-DD) | no | — | Upper date bound — keep only reviews posted on/before this day. Combine with `reviewsFrom` for a closed date window. Leave empty for no upper bound. |
| `minRating` | `integer` (1–10) | no | — | Keep only reviews scoring at or above this value on Expedia's 1–10 scale. Leave empty for no lower bound. |
| `maxRating` | `integer` (1–10) | no | — | Keep only reviews scoring at or below this value. Pair with `minRating` for a band (e.g. `1`–`4` to pull only negative reviews). Leave empty for no upper bound. |
| `includeCategoryRatings` | `boolean` | no | `true` | Include hotel/activity-level aggregate: overall rating, total reviews, **full 5-category breakdown** (Cleanliness, Staff & service, Amenities, Property conditions & facilities, Location), **AI-generated guest-sentiment summary** (`reviewSummaryAI`), and **the property's full photo gallery** (`propertyImages` — typically 100-300 URLs per hotel). Disable if you only need per-review fields — saves one `additional-cost` event per URL. |
| `includePropertyImages` | `boolean` | no | `true` | When `true`, every gallery image URL is returned in `propertyImages` and **billed per image** at $0.0001 each (~$0.02 for a typical 200-image hotel). Set `false` to skip image extraction and pay nothing for images. Has no effect unless `includeCategoryRatings: true`. |
| `maxPropertyImages` | `integer` | no | — (full gallery) | Keep only the **first N** gallery images per hotel (gallery order — hero shots first). Billed only for images returned: `maxPropertyImages: 10` → at most 10 image units per hotel instead of 100-300. |
| `includePropertyDetails` | `boolean` | no | `true` | When `true`, each row carries rich hotel-level descriptive fields: `propertyName`, `starRating`, `propertyType`, `propertyAddress`, `propertyCoordinates`, `propertyDescription`, `propertyLanguages`, `propertyAmenities` (categorized), `propertyHighlights`. Extracted from the **same page-layout response as the photo gallery — no extra per-item charge**. Set `false` to omit them. Has no effect unless `includeCategoryRatings: true`. |
| `includeLocation` | `boolean` | no | **`false`** | **Opt-in.** When `true`, each row carries `propertyLocation` — nearby landmarks, transit/airports, and restaurants, each with walk/drive times. Fires a separate request per hotel, **billed one `additional-cost` ($0.001) per hotel**. Leave off if you don't need location data. |
| `includePolicies` | `boolean` | no | **`false`** | **Opt-in.** When `true`, each row carries `propertyPolicies` — check-in/out times & fees, minimum age, pets, children & extra beds, deposits, payment types, and "you need to know" notes, grouped by section. Fires a separate request per hotel, **billed one `additional-cost` ($0.001) per hotel**. |
| `includeRooms` | `boolean` | no | **`false`** | **Opt-in.** When `true`, each row carries `propertyRooms` — room types, bed config, occupancy, inclusions, nightly & total rate, and cancellation terms. **Prices are a snapshot** for the search window below (stamped as `quotedFor`). Fires a separate (heavy) request per hotel, **billed one `additional-cost` ($0.001) per hotel**. |
| `roomsCheckIn` | `string` (YYYY-MM-DD) | no | ~30 days out | Check-in date for the room-rate quote. Only used with `includeRooms`. |
| `roomsCheckOut` | `string` (YYYY-MM-DD) | no | night after check-in | Check-out date for the room-rate quote. Only used with `includeRooms`. |
| `roomsAdults` | `integer` | no | `2` | Adults for the room-rate quote. Only used with `includeRooms`. |
| `roomsRegion` | `string` | no | `auto` | Region & currency for room-rate quotes. `auto` follows each URL's own domain (`expedia.se` → SEK); an explicit code (`us`, `uk`, `de`, `se`, `jp`, `au`, `in`, `br`, … 33 regions) forces that currency for every quote — even bare hotel IDs. |
| `roomsLanguage` | `string` | no | `english` | Language of room-rate **labels** (room names, meal plans, cancellation texts). `english` (default): English labels with local-currency prices — "Breakfast buffet + 237 kr". `site-default`: the regional site's own language — "Frukostbuffé + 237 kr". Never affects the currency. |
| `includeRoomOffers` | `boolean` | no | **`false`** | **Opt-in.** Adds `mealPlans[]` + `cancellationOptions[]` to every room — each meal-plan and cancellation option with its "+ $X" price delta. **Self-sufficient** — enabling it alone also fetches the room rates (no need to set `includeRooms` too). Billed via the `additional-cost` event: the base rooms unit **plus 1 unit per available room** that carries an offer breakdown (price tracks bookable rooms, not rate permutations). |
| `skipReviews` | `boolean` | no | **`false`** | **Rooms-only / price-monitoring mode.** When `true`, reviews are **not scraped** — the actor emits **one summary row per hotel** (`rowType: "hotel"`) carrying whatever hotel-level data you enabled (rooms, offers, details, location, policies, images). No per-review charges, far fewer requests per hotel (so far fewer 429s at scale). Ideal for weekly rate-intelligence runs over a hotel list. |
| `includeRatingDistribution` | `boolean` | no | **`false`** | Attach `ratingDistribution` to every row — the 5-band review-score histogram exactly as Expedia shows it: `[{ rating: 10, label: "Excellent", count: 523, percent: 52 }, { rating: 8, label: "Good", count: 231, … }, … 6-Okay, 4-Poor, 2-Terrible]`. It takes **one extra lookup per hotel**, **billed one `additional-cost` ($0.001) per hotel**. |
| `reviewSort` | `enum` | no | `newest` | Sort order for the review list: `newest`, `relevant`, `highest`, `lowest`, `mostHelpful` — all five work, using Expedia's own sort options (`mostHelpful` uses Expedia's most relevant order). |
| `includeReviewPhotos` | `boolean` | no | `true` | Set `false` to strip `photos[]` from every emitted row. Useful for CSV exports or compact JSON. |
| `includeOwnerResponses` | `boolean` | no | `true` | Set `false` to strip `hotelResponse` from every emitted row. Use when you only want guest-side review text. |
| `includeReviewMarkdown` | `boolean` | no | **`false`** | Add a self-contained `reviewMarkdown` field to every review row — title, score, date, text, liked/disliked and stay context as one markdown string, ready to drop into a RAG / vector pipeline without reshaping the JSON. No extra request, no extra charge. |
| `maxConcurrency` | `integer` | no | `10` | Parallel fetches. Sweet spot 8-15 via Apify Residential. |
| `minConcurrency` | `integer` | no | `1` | Floor — auto-scales up. |
| `maxRequestRetries` | `integer` | no | `100` | Per-URL retry budget on transient failures. |
| `proxy` | object | no | Apify Residential | Auto-prefilled — leave alone unless you need a specific country. |

#### Example input — mixed run, all 6 input types

```json
{
  "startUrls": [
    "https://www.expedia.co.uk/Kastoria-Hotels-Hotel-Anastassiou.h6136437.Hotel-Information",
    "https://www.expedia.com/h17369936.Hotel-Information",
    "https://www.expedia.com.br/Rio-De-Janeiro-Hotels-Premier-Copacabana-Hotel.h53296.Hotel-Information",
    "https://www.expedia.com.sg/things-to-do/singapore-hop-on-hop-off-bus-tour-by-open-top-bus.a203344.activity-details",
    "https://el.hotels.com/ho434012/hotel-anastassiou-kastoria-ellada/",
    "https://expe.app.link/KjB1EuacRSb",
    "600217"
  ],
  "reviewsFrom": "2025-01-01",
  "includeCategoryRatings": true,
  "maxItems": 1000,
  "maxConcurrency": 10,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

This yields up to 1,000 review rows since 2025-01-01 across 7 input URLs spanning 3 brands and 5 regional TLDs.

### Output schema

Two row shapes — distinguishable by the `provider` field.

#### `provider: "expedia"` or `"hotels"` (hotel review row)

```jsonc
{
  "provider":          "expedia",            // or "hotels" for Hotels.com
  "hotelId":           "6136437",
  "hotelUrl":          "https://www.expedia.co.uk/Kastoria-Hotels-Hotel-Anastassiou.h6136437.Hotel-Information",
  "placeName":         "Kastoria Hotels Hotel Anastassiou",   // title-cased from the URL slug ("" when the URL has none); the real hotel name is `propertyName` below
  "placeAddress":      "",
  "locale":            "en_GB",
  "verified":          true,

  // ── Per-review fields ───────────────────────────────────────
  "reviewId":          "6136437|2026-04-12|10|...",
  "reviewText":        "The hotel was wonderful and the staff very welcoming...",
  "reviewTitle":       "Amazing stay",
  "reviewRating":      10,                    // 0-10 numeric
  "reviewRatingLabel": "Exceptional",         // "Exceptional" / "Wonderful" / "Very good" / "Good" / "Okay" / "Poor"
  "rating":            "10/10 Exceptional",   // raw label string as Expedia returns it
  "reviewDate":        "2026-04-12T00:00:00.000Z",
  "stayDate":          "2026-03-01T00:00:00.000Z",   // first day of the stay month (null if none)
  "stayDuration":      3,                     // number of nights (null if none)
  "authorName":        "Maria",
  "reviewerName":      "Maria",               // same value as authorName

  // ── Trip metadata ───────────────────────────────────────────
  "tripType":          "Stayed 3 nights in Mar 2026",   // raw tripSummary text
  "traveledWith":      "Traveled with partner",          // "Traveled with family" / "partner" / "friends" / … ("" if absent)

  // ── Sentiment highlights ────────────────────────────────────
  "liked":             ["Cleanliness", "Staff & service"],
  "disliked":          ["Room amenities"],
  "sentiments":        ["Liked: Cleanliness, Staff & service", "Disliked: Room amenities"],   // raw labels

  // ── Photos (stripped when includeReviewPhotos: false) ───────
  "photos":            ["https://...jpg", "https://...jpg"],

  // ── Management response (when present; stripped when includeOwnerResponses: false) ──
  "hotelResponse":       "Thank you for staying with us...",
  "hotelResponseAuthor": "Hotel Manager",
  "hotelResponseDate":   "2026-04-15T00:00:00.000Z",

  // ── Hotel aggregate (when includeCategoryRatings: true) ─────
  "hotelOverallRating":   8.2,
  "hotelRatingLabel":     "Very good",
  "hotelTotalReviews":    1247,
  "hotelCategoryRatings": [                                  // up to 5 categories
    { "label": "Cleanliness",                       "percent": 88 },
    { "label": "Staff & service",                   "percent": 84 },
    { "label": "Amenities",                         "percent": 84 },
    { "label": "Property conditions & facilities",  "percent": 82 },
    { "label": "Location",                          "percent": 88 }
  ],

  // ── Expedia AI guest-sentiment summary (when available) ─────
  // Bulleted topics auto-generated by Expedia from many reviews. Each
  // bullet aggregates several individual reviews — `mentions` tells you
  // how many. `null` when the property has too few reviews for Expedia
  // to summarise (typically small / low-volume listings).
  "reviewSummaryAI": [
    {
      "category": "What guests liked",
      "text":     "Breakfast was consistently delicious, with hot food served hot and cold items kept cool.",
      "mentions": 7
    },
    {
      "category": "What guests liked",
      "text":     "The spa experience was exceptional with beautifully designed rooms and large whirlpool tubs.",
      "mentions": 3
    }
  ],
  "reviewSummaryAIDisclaimer": "From real guest reviews summarized by AI.",

  // ── Full property photo gallery (when available) ────────────
  // Every image Expedia shows in the hotel's gallery, ordered as
  // Expedia returns them, deduplicated. Typically 100-300 images
  // per property. URLs are the canonical CDN paths — append your
  // own `?impolicy=resizecrop&rw={width}&ra=fit` query string if
  // you want a specific size, or use as-is for the original.
  "propertyImages": [
    "https://images.trvl-media.com/lodging/10000000/9730000/9721900/9721825/w5977h3997x10y0-0092ce90.jpg",
    "https://images.trvl-media.com/lodging/10000000/9730000/9721900/9721825/92e976cb.jpg",
    "https://images.trvl-media.com/lodging/10000000/9730000/9721900/9721825/8d2cbffe.jpg"
    // … 217 more
  ],

  // ── Property description & amenities (when includePropertyDetails: true) ──
  "propertyName":        "The Anniversary Inn Logan",
  "starRating":          2.5,                       // star class, NOT the guest score
  "propertyType":        "BedAndBreakfast",         // schema.org type
  "propertyAddress": {
    "street":     "169 East Center Street",
    "city":       "Logan",
    "region":     "UT",
    "postalCode": "84321",
    "country":    "USA"
  },
  "propertyCoordinates": { "latitude": 41.731704, "longitude": -111.830614 },
  "propertyDescription": "The Anniversary Inn Logan provides free continental breakfast and more.\nRoom features\nAll guestrooms are individually furnished…",
  "propertyLanguages":   ["English"],
  "propertyAmenities": [                             // categorized
    { "category": "Parking",        "items": ["Free self parking on site"] },
    { "category": "Breakfast",      "items": ["Continental breakfast included", "Served daily"] },
    { "category": "Spa",            "items": ["In-room massages"] },
    { "category": "Accessibility",  "items": ["No elevator", "Wheelchair accessible (may have limitations)"] }
    // … more categories (Pets, Conveniences, Guest services, Business services, More)
  ],
  "propertyHighlights": [
    { "title": "Highly rated by solo travelers", "description": "This property received multiple high ratings from solo travelers." },
    { "title": "Top rated breakfast",            "description": "Experience delightful mornings with the top rated breakfast." }
  ],

  // ── Location intelligence (only when includeLocation: true) ──
  "propertyLocation": {
    "whatsNearby": [
      { "name": "Logan Utah Temple",      "distance": "7 min walk" },
      { "name": "Utah State University",  "distance": "18 min walk" }
    ],
    "gettingAround": [
      { "name": "Pleasant View Station",  "distance": "56 min drive" }
    ],
    "restaurants": [
      { "name": "Center Street Grill",    "distance": "3 min walk" },
      { "name": "Le Nonne",               "distance": "5 min walk" }
    ]
  },

  // ── Policies & important info (only when includePolicies: true) ──
  "propertyPolicies": [
    { "section": "Check-in",  "items": ["Check-in start time: 5:00 PM; Check-in end time: midnight", "Minimum check-in age: 21", "Early check-in fee: USD 35.00"] },
    { "section": "Check-out", "items": ["Check-out before noon", "Late check-out fee: USD 35.00"] },
    { "section": "Pets",      "items": ["Pets not allowed"] },
    { "section": "Fees",      "items": ["Deposit: USD 50.00 per stay"] }
    // … more sections (Children & extra beds, Payment types, You need to know, …)
  ],

  // ── Room inventory + rates (only when includeRooms: true) ──
  // Prices are a point-in-time snapshot for the `quotedFor` window, quoted by
  // the `pos` point-of-sale (drives the currency — see `roomsRegion`). Labels
  // are English by default (`roomsLanguage`), whatever the currency.
  "propertyRooms": {
    "quotedFor": { "checkIn": "2026-06-28", "checkOut": "2026-06-29", "adults": 2 },
    "pos": "www.expedia.com",           // POS host that produced the quote
    "currency": "USD",                  // ISO currency of the quote
    "rooms": [
      {
        "name": "Signature Room",
        "details": ["Sleeps 2", "1 King Bed", "Breakfast available", "Parking included"],
        "nightlyRate": "$414",          // lead (cheapest) offer — localized display string
        "totalRate": "$1,465",
        "nightlyRateValue": 414,        // same prices as plain numbers — sort/compare ready
        "totalRateValue": 1465,
        // Struck-through rate when the lead rate is discounted (a public deal or a
        // member price); null when there is no discount, as here.
        "nightlyRateBeforeDiscount": null,
        "totalRateBeforeDiscount": null,
        "nightlyRateBeforeDiscountValue": null,
        "totalRateBeforeDiscountValue": null,
        "memberPrice": false,           // true only for a member-only rate, never for a public "X off" deal
        "discountLabel": null,          // Expedia's discount text on the room card, e.g. "1 010 kr off"
        "currency": "USD",
        "cancellation": "Non-refundable",
        // Meal-plan "Extras" with their per-stay price delta:
        "mealPlans": [
          { "name": "No extras",                             "subText": null, "price": "+ $0",   "selected": true  },
          { "name": "Half board",                            "subText": null, "price": "+ $99",  "selected": false },
          { "name": "All-inclusive (food/beverages/snacks)", "subText": null, "price": "+ $295", "selected": false }
        ],
        // Cancellation-policy choices with their price delta. The same policy can
        // repeat with different payment terms in subText (pay deposit vs pay later):
        "cancellationOptions": [
          { "name": "Non-Refundable",                "subText": null,                   "price": "+ $0",   "selected": true  },
          { "name": "Fully refundable before Jul 9", "subText": "Reserve now, pay deposit", "price": "+ $0",   "selected": false },
          { "name": "Fully refundable before Jul 9", "subText": "Reserve now, pay later",   "price": "+ $646", "selected": false }
        ],
        "cancellationFixed": false      // the room offers a choice of policies
      },
      // A room with ONE set policy (no choice on the card) and a public discount:
      {
        "name": "Family Room, Land View",
        "nightlyRate": "$470",
        "totalRate": "$1,410",
        "totalRateBeforeDiscount": "$1,560",   // public deal, so memberPrice stays false
        "memberPrice": false,
        "discountLabel": "$150 off",
        "cancellation": "Fully refundable before Tue, 17 Nov",
        "cancellationOptions": [
          { "name": "Fully refundable before Tue, 17 Nov", "subText": null, "price": "+ $0", "selected": true }
        ],
        "cancellationFixed": true       // that single policy is included in the rate
        // … other fields as in the room above
      }
      // … one entry per room type
    ]
  },

  // ── Review-score histogram (only when includeRatingDistribution: true) ──
  // The 5-band distribution exactly as Expedia shows it. Same value on
  // every row of a given hotel (it's a property-level aggregate).
  "ratingDistribution": [
    { "rating": 10, "label": "Excellent", "count": 523, "percent": 52 },
    { "rating": 8,  "label": "Good",      "count": 231, "percent": 23 },
    { "rating": 6,  "label": "Okay",      "count": 129, "percent": 13 },
    { "rating": 4,  "label": "Poor",      "count": 76,  "percent": 7  },
    { "rating": 2,  "label": "Terrible",  "count": 51,  "percent": 5  }
  ],

  // ── LLM-ready markdown (only when includeReviewMarkdown: true) ──
  // The whole review as one self-contained markdown string — drop it
  // straight into a RAG / vector pipeline without reshaping the JSON.
  "reviewMarkdown": "### Hotel Anastassiou — 10/10 · Exceptional · 2026-04-12\n\n**Amazing stay**\n\nThe hotel was wonderful and the staff very welcoming...\n\nLiked: Cleanliness; Staff & service\n\nDisliked: Room amenities\n\n_Traveled with: partner · 3 night(s) · Reviewer: Maria_",

  "scrapedAt":         "2026-05-16T10:24:31Z"
}
```

#### `provider: "expedia-activity"` (activity review row)

```jsonc
{
  "provider":               "expedia-activity",
  "activityId":             "203344",
  "activityUrl":            "https://www.expedia.com.sg/things-to-do/singapore-hop-on-hop-off-bus-tour.a203344.activity-details",
  "placeName":              "Singapore Hop-On Hop-Off Bus Tour by Open-Top Bus",   // same value as activityName
  "placeAddress":           "",
  "locale":                 "en_SG",
  "siteid":                 "1",
  "currency":               "SGD",

  // ── Activity aggregate (always included for activities) ─────
  "activityName":           "Singapore Hop-On Hop-Off Bus Tour by Open-Top Bus",
  "activitySubHeading":     "By Big Bus Tours",
  "activityPrice":          "S$65",
  "activityOverallRating":  "8.4/10",
  "activityRatingLabel":    "Very good",
  "activityTotalReviews":   216,
  "activityHighlights":     ["Explore Singapore by open-top bus", "Frequent service every 25-40 min", ...],
  "activityFeatures":       ["Free cancellation", "Printed voucher", "Instant confirmation", ...],

  // ── Per-review fields ───────────────────────────────────────
  "reviewId":               "203344|Sep 16, 2025||Overall the bus experience was g|1",  // derived hash: activityId|submittedOn|author|text[:32]|index (no stable id in the feed)
  "reviewText":             "Overall the bus experience was good, but a couple of stops felt rushed...",
  "reviewTitle":            "",                    // activities never carry a review title
  "reviewRating":           6,                     // 0-10 numeric, parsed from the score
  "reviewRatingLabel":      "6.0 out of 10",       // accessibility score string, NOT a word label
  "rating":                 "6.0/10",              // raw display score
  "reviewDate":             "Sep 16, 2025",        // Expedia's display submission date (not ISO)
  "activityDate":           "Sep 06, 2025",        // when the user did the activity (display date)
  "authorName":             "",                    // reviewer name; "" when the review is anonymous
  "reviewerName":           "",                    // same value as authorName
  "userLocation":           "United States",
  "reviewedOn":             "Reviewed on Expedia"  // the review's source, not a date
}
```

> Hotel rows and activity rows have **different field sets** — that's intentional. Activities don't have `traveledWith` / `stayDuration` / `hotelResponse`; hotels don't have `activityHighlights` / `activityFeatures`. Filter by `provider` if you only want one shape.

### Pricing

Pay-per-event — no monthly subscription.

| Event | When it fires | Rate |
|---|---|---|
| `Actor Start` | Once per run | $0.008 |
| `result` | Per review row emitted | $0.0025 |
| `additional-cost` (monitoring mode / category ratings) | Per hotel/activity when `includeCategoryRatings: true` | $0.001 |
| `additional-cost` (gallery images) | When `includePropertyImages: true` — **1 unit per gallery image returned** (typically 100-300 per hotel; cap the count & cost with `maxPropertyImages`) | $0.001 / unit |
| `additional-cost` (location) | Per hotel when `includeLocation: true` — the `propertyLocation` block | $0.001 |
| `additional-cost` (policies) | Per hotel when `includePolicies: true` — the `propertyPolicies` block | $0.001 |
| `additional-cost` (rooms) | Per hotel when `includeRooms: true` — the `propertyRooms` block | $0.001 |
| `additional-cost` (room offers) | When `includeRoomOffers: true` — **1 unit per available room** that carries an offer breakdown, per hotel (on top of the base rooms unit) | $0.001 / unit |

**Cost examples:**

| Run | Charges |
|---|---|
| 1 hotel, 50 reviews, with category ratings (no images) | $0.008 + 50 × $0.0025 + 1 × $0.001 = **$0.134** |
| 1 hotel, 50 reviews, with category ratings **+ 220 gallery images** | $0.134 + 220 × $0.0001 = **$0.156** |
| 10 hotels × 100 reviews avg, with category ratings (no images) | $0.008 + 1000 × $0.0025 + 10 × $0.001 = **$2.52** |
| 10 hotels × 100 reviews avg, with category ratings + ~200 images per hotel | $2.52 + 10 × 200 × $0.0001 = **$2.72** |
| 1 activity, 200 reviews, with metadata | $0.008 + 200 × $0.0025 + 1 × $0.001 = **$0.509** |
| Date-filtered (5 hotels, reviewsFrom: last 7 days) — say 30 new reviews total | $0.008 + 30 × $0.0025 + 5 × $0.001 = **$0.088** |

### What makes this richer than competing actors

We surveyed all Expedia + Hotels.com review scrapers on Apify Store. Direct comparison:

| Capability | Competitor actors | This actor |
|---|---|---|
| Expedia hotel reviews | ✅ most | ✅ |
| **Expedia Things-To-Do activities** | ❌ none | ✅ unique — tours, attractions, day trips |
| **Hotels.com reviews via the same scraper** | ❌ usually separate actor | ✅ same input array, same row shape |
| Multi-region TLDs (`.co.uk`, `.de`, `.com.br`, …) | partial | ✅ 10+ TLDs validated |
| **Expedia deeplink resolution** (`expe.app.link`, `expe.onelink.me`) | ❌ | ✅ |
| **Bare property ID input** (e.g. `600217`) | ❌ | ✅ auto-expanded |
| Date filtering with smart pagination stop | ❌ usually fetches everything | ✅ stops early, saves cost |
| Sentiment (liked / disliked) extraction | partial | ✅ structured array |
| Traveled-with normalization | ❌ | ✅ `family` / `partner` / `business` / `solo` / `friends` |
| Management response capture | partial | ✅ text + date + responder name |
| Hotel category ratings (cleanliness / staff / amenities / property conditions / location) | ❌ usually missing or partial (3 of 5) | ✅ full 5-category breakdown when `includeCategoryRatings: true` |
| Expedia AI guest-sentiment summary (bulleted topics + mention counts) | ❌ not extracted by other scrapers | ✅ included when Expedia has generated one for the property |
| **Full property photo gallery URLs** (100-300 per hotel) | ❌ not extracted by other scrapers | ✅ every gallery image URL, deduplicated, when `includeCategoryRatings: true` |
| **Property description + categorized amenities + address/coords/star-class/highlights** | ❌ reviews-only | ✅ full descriptive block per hotel, no per-item charge, when `includePropertyDetails: true` |
| **Location intelligence** (nearby landmarks / transit / restaurants + walk-drive times) | ❌ not extracted | ✅ structured `propertyLocation` block, opt-in via `includeLocation: true` |
| **Policies & important info** (check-in/out times & fees, min age, pets, deposits) | ❌ not extracted | ✅ structured `propertyPolicies` block, opt-in via `includePolicies: true` |
| **Room inventory + live rates** (room types, occupancy, nightly/total price, cancellation) | ❌ not extracted | ✅ structured `propertyRooms` block with configurable date window, opt-in via `includeRooms: true` |
| **Meal-plan & cancellation pricing** (Half board / All-inclusive / refundable-vs-not, each priced) | ❌ not extracted | ✅ `mealPlans[]` + `cancellationOptions[]` on every room — each option with its "+ $X" delta |
| Activity highlights + features (for Things-To-Do) | ❌ | ✅ full arrays |
| Destination-page graceful handling (no dead-URL charges) | ❌ usually 0-row failures | ✅ single explanatory row, no fetch |

### FAQ

**Can I scrape Expedia hotels AND Hotels.com in the same run?**
Yes. Paste both in `startUrls`. Each row carries a `provider` field (`expedia` / `hotels`) so you can split them apart in your downstream pipeline.

**What's a "Things-To-Do" activity, and how is it different from a hotel?**
Activities are tours, attractions, day trips, classes, transfers — anything bookable in Expedia's "Things to do" section. URL pattern: `/things-to-do/{slug}.a{id}.activity-details`. The output row has a different shape (no `traveledWith`, `stayDuration`, or `hotelResponse`; adds `activityHighlights`, `activityFeatures`, `userLocation`, `activityDate`). Rows are tagged `provider: "expedia-activity"`.

**Can I paste a deeplink URL like `expe.app.link/KjB1EuacRSb`?**
Yes. The scraper follows the redirect chain to the canonical hotel/activity URL before fetching reviews. Useful when ingesting URLs from mobile-app shares, marketing campaigns, or social media.

**Can I use a Google Maps URL or Google Place ID instead of an Expedia URL?**
Yes — use the dedicated `googleMapsInputs` field (or paste them straight into `startUrls`; both work): Google Maps hotel URLs, bare Place IDs (`ChIJ...`), Feature IDs (`0x...:0x...`), or `maps.app.goo.gl` share links. The actor reads the hotel's name and coordinates from Google, matches it against the Expedia Group catalog, and scrapes the matched property in the same run. Use `googleMapsTarget` to choose the platform: `expedia` (default), `hotels` (Hotels.com — the legacy `ho` id is mapped automatically, since it can differ from Expedia's id for pre-merger hotels), or `both`. Every Google input produces a `google-maps-resolved` row documenting the mapping (both ids, name-match score, geo distance) so you can audit the match; review rows then join on `expediaHotelId` / the hotels.com id. If the match isn't high-confidence, you get a `google-maps-unresolved` row listing the nearest candidates with ready-to-use URLs — the actor deliberately never guesses, because scraping the wrong hotel's reviews is worse than none. Ideal when your pipeline starts from Google Places data (e.g. a Google reviews scraper) and you want the same hotels' Expedia/Hotels.com reviews without manual URL hunting.

**How does date filtering work with pagination?**
Pass `reviewsFrom: "YYYY-MM-DD"`. Expedia returns reviews newest-first, so the scraper paginates until it hits a review older than your cutoff, then stops. You only pay for reviews that match. Perfect for daily-delta monitoring workflows.

**Why do I get an "explanatory row" instead of reviews on a destination URL?**
URLs like `.../Place.d12345.Place-To-Visit` are destination/region landing pages — they don't have reviews of their own, just lists of nearby hotels and tours. Rather than silently failing or charging you for a dead fetch, the scraper emits a single row with a clear hint about the URL types that ARE supported. No network charge.

**Can I just paste hotel IDs without full URLs?**
Yes. `600217` and `h600217` both auto-expand to `https://www.expedia.com/Hotel.h600217.Hotel-Information`. Saves time when importing IDs from a CRM or spreadsheet.

**Does this work on regional Expedia domains?**
Yes — all 33 Expedia points-of-sale are supported and verified: `.com`, `.ca`, `.mx`, `.com.br`, `.com.ar`, `.co.uk`, `.ie`, `.de`, `.fr`, `.es`, `.it`, `.nl`, `.be`, `.at`, `.ch`, `.dk`, `.fi`, `.gr`, `.no`, `.se`, `.com.au`, `.co.nz`, `.co.jp`, `.co.kr`, `.co.in`, `.com.sg`, `.com.hk`, `.com.tw`, `.com.my`, `.co.th`, `.com.ph`, `.com.vn`, `.co.id` — plus regional Hotels.com hosts (`fr.hotels.com`, …). Same scraper, same row shape. The hotel's `locale` field on each row tells you which region the data came from, and room rates are quoted in that region's currency (see `roomsRegion`).

**What if `includeCategoryRatings: false`?**
You'll save one `additional-cost` event per URL ($0.001 each), but you'll lose **all** hotel-level fields: `hotelOverallRating`, `hotelTotalReviews`, `hotelCategoryRatings`, `reviewSummaryAI`, `reviewSummaryAIDisclaimer`, `propertyImages`, and the descriptive block (`propertyName`, `starRating`, `propertyType`, `propertyAddress`, `propertyCoordinates`, `propertyDescription`, `propertyLanguages`, `propertyAmenities`, `propertyHighlights`). Set this to `false` if you only need per-review data and have those aggregates elsewhere.

**🆕 What is `propertyRooms` (`includeRooms`)?**
Opt-in (`includeRooms: true`, default off). Adds a `propertyRooms` object with the hotel's room inventory **and live rates** for a search window: `{ quotedFor: { checkIn, checkOut, adults }, pos, currency, rooms: [ { name, details[], nightlyRate, totalRate, nightlyRateValue, totalRateValue, nightlyRateBeforeDiscount, totalRateBeforeDiscount, nightlyRateBeforeDiscountValue, totalRateBeforeDiscountValue, memberPrice, discountLabel, currency, cancellation, mealPlans[], cancellationOptions[], cancellationFixed } ] }`. Each room's `details` lists bed config / occupancy / inclusions (e.g. "Sleeps 2", "1 Queen Bed", "Breakfast included"). `nightlyRate`/`totalRate` are the localized display strings ("$320", "3 415 kr", "￥51,312"); `nightlyRateValue`/`totalRateValue` are the same prices as **plain numbers** — ready for sorting, math, and cross-hotel comparison without parsing. `pos` records which Expedia point-of-sale produced the quote (e.g. `www.expedia.se`) and the top-level `currency` its ISO code.

When you enable **`includeRoomOffers: true`** (a paid option — see below; it also fetches the room rates on its own, so you don't need `includeRooms` as well), each room also carries two arrays that mirror the room card's selectors — **this is where the "+$X" deltas live**:

- **`mealPlans[]`** — the "Extras" / board-basis options, each `{ name, price, selected }`: e.g. `{ "name": "Half board", "price": "+ $99", "selected": false }`, `{ "name": "All-inclusive (food/beverages/snacks)", "price": "+ $295" }`. Empty for properties that don't offer meal-plan tiers.
- **`cancellationOptions[]`** — the cancellation-policy choices, each `{ name, price, selected }`: e.g. `{ "name": "Non-Refundable", "price": "+ $0", "selected": true }`, `{ "name": "Fully refundable before Jul 9", "price": "+ $90" }`. Some rooms have **one set policy** instead of a choice (the card shows e.g. "Fully refundable / Before Tue, 17 Nov" with no radio buttons): those rooms get that policy as the single selected option at `+ 0`, the same text in `cancellation`, and `cancellationFixed: true`.

**Discounts:** when the lead rate is discounted, `nightlyRateBeforeDiscount` / `totalRateBeforeDiscount` (+ the `*Value` numbers) carry the struck-through rate the site shows. `memberPrice` is `true` only when Expedia marks the discount as a member price; a public deal anyone can book ("4 619 kr off") leaves it `false`. `discountLabel` carries Expedia's own text for the discount exactly as the room card shows it ("1 010 kr off", "Member Price 8 863 kr off", or "Mobile Price 417 kr off" for a rate Expedia offers only in its mobile app), null when there is none. Expedia sometimes shows the same room at different discounts to different visitors at the same moment, so the label tells you which discount a row reflects. All of these are read from the same rate plan as the lead price.

`selected: true` marks the option included in the base `totalRate`; add another option's `price` to that base to get its total. The top-level `nightlyRate`/`totalRate`/`cancellation` on the room are the **lead (cheapest, base) offer**, kept for quick reference.

> **Pricing:** the per-offer meal-plan + cancellation breakdown is premium rate-intelligence data, billed via the **`additional-cost` event — 1 unit ($0.001) per available room that carries an offer breakdown**, per hotel, on top of the 1-unit base rooms charge. The price tracks bookable rooms, not the number of rate permutations, so it's predictable at scale: a hotel with 11 available rooms costs 11 units = $0.011 for the offers (plus $0.001 for the room data), no matter how many board/cancellation variants each room exposes. Sold-out rooms cost nothing, and rooms with a single set policy cost nothing extra: their policy is still included in the output. Without `includeRoomOffers`, `mealPlans`/`cancellationOptions` come back as empty arrays and you only pay the base rooms charge.

**Local currency — any of 33 regions:** room prices are quoted by an Expedia **point-of-sale**, and the POS decides the currency. Two ways to pick it:

- **Automatic (default)** — the quote follows each input URL's own domain: `https://www.expedia.se/...` → SEK, `expedia.co.jp` → JPY, `expedia.com.br` → BRL, `fr.hotels.com` → EUR, plain `.com` and bare property IDs → USD.
- **`roomsRegion`** — force one region for the whole run regardless of URL form. Pick from 33 verified codes (`us ca mx br ar uk ie de fr es it nl be at ch dk fi gr no se au nz jp kr in sg hk tw my th ph vn id`), each quoting in its local currency (USD, CAD, MXN, BRL, GBP, EUR, CHF, DKK, NOK, SEK, AUD, NZD, JPY, KRW, INR, SGD, HKD, TWD, MYR, THB, PHP, VND, IDR). Example: bare IDs + `"roomsRegion": "se"` → every quote in SEK. (Argentina's POS quotes in USD — that's Expedia's own behavior.)

The output is self-describing: `propertyRooms.pos` is the POS host used, `propertyRooms.currency` the ISO code, and every room carries both the localized price strings and numeric `nightlyRateValue`/`totalRateValue`.

**English labels + local currency (default):** label language is independent of currency. By default the actor requests **English** labels from every POS — so an `expedia.se` quote reads `"Breakfast buffet" + 237 kr` (English name, SEK price). Set `roomsLanguage: "site-default"` if you want the labels in the regional site's own language instead (`"Frukostbuffé" + 237 kr`). Every one of the 33 POSes supports the English mode — verified live.

**Prices are a point-in-time snapshot** — they depend on the search dates, which is why the window is stamped into `quotedFor`. Control it with `roomsCheckIn` / `roomsCheckOut` (YYYY-MM-DD) and `roomsAdults`; leave them empty to default to ~30 days out, 1 night, 2 adults. Fires a separate (~309KB) request per hotel and is **billed one `additional-cost` event ($0.001) per hotel** for the room inventory; the meal-plan/cancellation breakdown is billed separately per option (see the pricing note above). (If a hotel is sold out for the chosen dates, `propertyRooms` is `null` — pick different dates.)

**🆕 What is `propertyPolicies` (`includePolicies`)?**
Opt-in (`includePolicies: true`, default off). Adds a `propertyPolicies` array to every hotel row — the property's policies & important info grouped by section: **Check-in** (start/end times, early-check-in fee, minimum age), **Check-out** (time, late fee), **Pets**, **Children & extra beds**, **Fees / deposits**, accepted **payment types**, and the "**You need to know**" notes (ID requirements, incidental holds, safety features). Each section is `{ section, items[] }`. Fires a separate request per hotel and is **billed one `additional-cost` event ($0.001) per hotel**.

**🆕 What is `propertyLocation` (`includeLocation`)?**
When you opt in with `includeLocation: true`, every hotel row gains a structured `propertyLocation` object with three lists — `whatsNearby` (landmarks/attractions), `gettingAround` (transit stations & airports), and `restaurants` — each entry a `{ name, distance }` pair where distance is a human-readable walk/drive time (e.g. `{ "name": "Utah State University", "distance": "18 min walk" }`). It's **opt-in (default off)** because it fires a separate request per hotel and is **billed one `additional-cost` event ($0.001) per hotel**. Like all hotel-level fields, the same block is repeated on every review row for that hotel.

**🆕 What property descriptive data do I get (`includePropertyDetails`)?**
When `includePropertyDetails: true` (default), every hotel row carries: `propertyName` (canonical name), `starRating` (the property's star class, e.g. 2.5 — distinct from the guest review score), `propertyType` (e.g. "Hotel", "BedAndBreakfast", "Resort"), `propertyAddress` (street/city/region/postalCode/country), `propertyCoordinates` (lat/lng), `propertyDescription` (the "About this property" narrative), `propertyLanguages` (languages spoken), `propertyAmenities` (categorized — Parking, Breakfast, Spa, Accessibility, …), and `propertyHighlights` (the marketing highlight cards). All of this is extracted from the **same page-layout response as the photo gallery**, so it adds **no extra charge** beyond the `additional-cost` event that `includeCategoryRatings` already triggers. Set `includePropertyDetails: false` to omit these fields.

**🆕 What is `propertyImages` and how big does it get?**
Every URL in the hotel's photo gallery on Expedia (e.g. main exterior shots, all room photos, lobby, restaurant, pool, etc.) — typically **100-300 image URLs per property**. Only need the first few? Set `maxPropertyImages` (e.g. `10`) to keep just the first N in gallery order (hero/cover shots first) — you're billed only for what's returned. The URLs are returned as the canonical CDN paths (`images.trvl-media.com/lodging/...jpg`) with the `?impolicy=...` resize query stripped, so you can append your own size parameter (`?impolicy=resizecrop&rw=1200&ra=fit` for 1200px-wide, etc.) or use the original. Images are deduplicated and ordered as Expedia returns them. Returns `null` when extraction fails (rare — usually a transient GraphQL error). Requires `includeCategoryRatings: true` AND `includePropertyImages: true` (both default `true`).

**🆕 How is `propertyImages` billed?**
Each image is billed as **1 `additional-cost` unit ($0.001)** — and only for images actually returned. A typical 200-image hotel adds ~$0.20 to your run cost; a 500-image resort adds ~$0.50. Two ways to control this:

- **`maxPropertyImages`** — cap the gallery to the first N images (hero/cover shots come first in gallery order). `{ "includePropertyImages": true, "maxPropertyImages": 10 }` returns at most 10 URLs and bills at most 10 units ($0.01) per hotel.
- **`includePropertyImages: false`** — the PageLayout image extraction is skipped entirely and you pay $0 for images.

**🆕 What is `reviewSummaryAI` and where does it come from?**
Expedia generates an AI-written bulleted summary of guest sentiment for each hotel that has enough review volume — surfaced in their iOS app's "See all reviews" screen. Each bullet aggregates several individual reviews into a topic (e.g. *"Breakfast was consistently delicious, with hot food served hot and cold items kept cool."*) and carries a `mentions` count showing how many guest reviews fed into it. The category heading (`"What guests liked"` etc.) is preserved alongside the text. Returns `null` when Expedia hasn't generated one — usually low-volume / niche properties. The accompanying `reviewSummaryAIDisclaimer` field carries Expedia's own attribution string (*"From real guest reviews summarized by AI."*) so downstream consumers can credit the source correctly. Only fetched when `includeCategoryRatings: true`.

**🆕 Can I paste a Google Maps URL or Place ID?**
Yes — paste it directly into `startUrls`. The actor recognises Maps URLs (`/maps/place/Hotel+Name/...`, `maps.app.goo.gl/...`) and bare Place IDs (`ChIJX_BcZ7C_TBQRyQXrJlw0fJg`). It parses the hotel name from the URL slug and emits a row tagged `provider: "google-maps-pending"` containing the parsed name, a pre-built Expedia hotel-search URL, and a hint with the next step. **Full auto-resolution (Maps URL → Expedia canonical hotel URL → reviews fetched in same run) is queued for v1.1.** The blockers are (a) capturing Expedia's hotel-search GraphQL persisted-query from a live browser session, (b) building a match-quality scorer for ambiguous results. Until then, the explanatory row gives you a deterministic shortcut to find the right Expedia URL manually.

**🆕 What does `reviewSort` do?**
`reviewSort` controls the order of reviews fetched. **All five values work**, using Expedia's own sort options:

- **`newest`** (default) — Expedia's most recent order. Required for `reviewsFrom` date-filter mode (the smart-stop pagination relies on newest-first ordering).
- **`relevant`** — Expedia's default "most relevant" order.
- **`highest`** / **`lowest`** — highest- or lowest-rated reviews first.
- **`mostHelpful`** — uses Expedia's most relevant order (Expedia has no separate helpfulness dimension; relevance already accounts for helpful-vote weighting).

The sort order in use is printed at run start.

**🆕 What do `includeReviewPhotos` and `includeOwnerResponses` do?**
Toggles that strip the matching fields from emitted rows (`reviewPhotos[]` and `hotelResponse`). Useful for CSV exports (flatter row shape), or when you want compact rows. **Note:** these are post-fetch filters — they don't reduce proxy bandwidth cost. GraphQL-level skipping (slimming the request body) is queued for v1.1.

### Support

- **Bugs / feature requests** — open an [issue on this actor](https://console.apify.com/actors/uVgVxc4Db5dKvybiP/issues)
- **Custom exports / tailored field selection** — email <muhamed.didovic@gmail.com>
- **API access (no Apify fee, raw usage)** — same address for direct API setup

### Additional services

- Custom scraping work for sites not in the catalog
- Bulk historical dataset purchase (skip the scraping step entirely)
- Webhook delivery to your S3 / GCS / DB pipeline
- Daily / hourly scheduled monitoring with email or Telegram alerts

Contact: <muhamed.didovic@gmail.com>

### Explore more scrapers

Other travel + reviews actors in the [memo23 portfolio](https://apify.com/memo23):

- [Trustpilot Reviews](https://apify.com/memo23/trustpilot-scraper-ppe) — flagship reviews scraper (264 u30d, 4.9★)
- [Glassdoor Reviews](https://apify.com/memo23/glassdoor-scraper-ppr) — employer reviews
- [Indeed Reviews](https://apify.com/memo23/apify-indeed-reviews-ppr) — employer + interview reviews
- [Clutch](https://apify.com/memo23/apify-clutch-cheerio) — B2B services reviews
- [TripAdvisor](https://apify.com/memo23/tripadvisor-scraper) — competing travel reviews
- [Booking.com](https://apify.com/memo23) — see profile for related travel scrapers

***

### 🤖 For AI Agents & LLM Apps

Compact reference for AI agents calling this actor via the [Apify MCP server](https://mcp.apify.com) or the Apify API (actor: `memo23/expedia-scraper`).

**Purpose:** Returns review rows from Expedia hotels, Expedia Things-To-Do activities, and Hotels.com — all three brands (plus a destination hotel search) in one actor, with deeplink and bare-ID resolution.

**Minimal input** (URL mode):

```json
{ "startUrls": ["https://www.expedia.co.uk/Kastoria-Hotels-Hotel-Anastassiou.h6136437.Hotel-Information"], "maxItems": 50 }
```

Destination-search variant: `{ "location": "London", "maxItems": 50 }` — one lightweight row per hotel (name, prices, review score, hotelId, URL).

**Output:** one dataset row per review — provider, url, hotelId, hotelName, reviewId, reviewText, reviewTitle, reviewRating, reviewRatingLabel, reviewDate, stayDuration, verified, authorName, tripType, traveledWith, sentiments {liked, disliked}, reviewPhotos, hotelResponse {responseText, respondedOn, responderName}, hotelOverallRating, hotelTotalReviews, hotelCategoryRatings, reviewSummaryAI, propertyImages, propertyName, propertyAddress, propertyRooms. Activity rows (`provider: "expedia-activity"`) carry activityName, activityPrice, activityOverallRating, activityHighlights, activityFeatures instead of the hotel block.

**Behaviors an agent should know:**

- Always set `maxItems` (default 1000). Billing: $0.008 per run start, $0.0025 per review row, $0.001 per `additional-cost` unit (category ratings, gallery images, location, policies, rooms).
- `startUrls` accepts hotel URLs, activity URLs, Hotels.com URLs, bare property IDs (`600217`), and `expe.app.link` deeplinks across 33 regional TLDs; `location` runs a destination search when `startUrls` is empty (both can be combined).
- `reviewsFrom` (YYYY-MM-DD) stops pagination at the cutoff — reviews are fetched newest-first, and you only pay for rows emitted.
- `includePropertyImages` defaults to `true` and is billed per gallery image (typically 100-300 per hotel) — set it `false` or cap with `maxPropertyImages` to control cost.
- `skipReviews: true` emits one summary row per hotel (rooms, policies, details — whatever is enabled) with no per-review charges — for rate/price monitoring.
- Destination landing pages (`.../Place.d{id}.Place-To-Visit`) have no reviews: the actor emits one explanatory row and skips the fetch, uncharged. Google Maps URLs emit a `google-maps-pending` row with a search shortcut, not reviews.

***

### ⚠️ Disclaimer

This Actor is an independent tool and is **not affiliated with, endorsed by, or sponsored by** Expedia Group, Inc., Expedia.com, Hotels.com, or any related entity. All trademarks mentioned are the property of their respective owners.

The scraper accesses only **publicly visible** review content rendered by Expedia.com, Hotels.com, and their regional TLD variants. No login bypass, no API-key forgery, no CAPTCHA solving, no private/authenticated endpoints. The actor honours `robots.txt` and rate-limits via concurrency cap (default 10) to avoid burdening Expedia Group's infrastructure.

Users are responsible for:

- Complying with Expedia.com's and Hotels.com's Terms of Service in your target jurisdiction
- Following GDPR (EU/UK), CCPA (US), PIPL (China), LGPD (Brazil), and any other applicable data-protection law when storing, processing, or republishing review + guest-name data
- Not contacting reviewers listed in scraped data for unsolicited commercial outreach
- Not republishing scraped reviews in a way that competes commercially with Expedia or Hotels.com
- Respecting reviewer privacy — many reviewers use first-name-only handles, do not attempt to de-anonymize

We do not store scraped data — the Actor returns rows directly to your Apify dataset for your authorized use.

***

### SEO Keywords

expedia scraper, expedia reviews scraper, expedia.com scraper, scrape expedia, expedia api, hotels.com scraper, hotels.com reviews, expedia things-to-do scraper, expedia activities scraper, hotel reviews api, tour reviews scraper, attraction reviews scraper, travel reviews data, hospitality reviews api, hotel reputation monitoring, sentiment analysis hotels, expedia deeplink resolver, expedia.co.uk scraper, expedia.de scraper, expedia.com.br scraper, multi-region expedia, expedia JSON export, expedia CSV export, travel data mining, hotel category ratings scraper, expedia AI review summary, AI generated hotel sentiment summary, guest sentiment AI bullets, expedia 5-category breakdown, cleanliness staff amenities property conditions location ratings, expedia property images scraper, expedia hotel photo gallery, expedia gallery URLs, hotel image scraping, scrape hotel photos expedia, expedia hotel amenities scraper, expedia property description scraper, hotel amenities data, scrape hotel address coordinates, hotel metadata scraper, expedia star rating scraper, expedia nearby places scraper, hotel location intelligence, points of interest near hotel, hotel walk drive times, expedia what's nearby data, expedia hotel policies scraper, check-in check-out times scraper, hotel pet policy data, hotel deposit fees scraper, expedia important information, expedia room rates scraper, hotel price scraper, room inventory scraper, expedia nightly rate tracking, hotel rate intelligence, room type occupancy scraper, expedia cancellation policy scraper, expedia meal plan prices, half board all-inclusive scraper, board basis pricing, expedia rate plan scraper, refundable rate prices

# Actor input Schema

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

Hotel pages, activity (Things-To-Do) pages, Hotels.com pages, **or bare Expedia property IDs**. Expedia and Hotels.com URLs may be mixed in the same run.

**Expedia hotels** — long slug (`.../City-Hotels-Name.h600217.Hotel-Information`), minimal slug (`.../Hotel.h600217.Hotel-Information`), short path (`.../h600217.Hotel-Information`), all regional TLDs (`expedia.co.uk`, `expedia.com.br`, `expedia.de`, `expedia.com.sg`, …), and deeplinks (`expe.app.link/...`, resolved automatically). Bare ids (digits or `h600217`) expand to `https://www.expedia.com/Hotel.h{id}.Hotel-Information`.

**Expedia activities (Things-To-Do)** — `https://www.expedia.com/things-to-do/{slug}.a{id}.activity-details` (and regional equivalents). Returns activity reviews under `provider: "expedia-activity"`.

**Hotels.com** — `https://{locale}.hotels.com/ho{id}/{slug}/`.

**Not supported:** destination/region landing pages (`.../Name.d{id}.Place-To-Visit`) — those have no reviews of their own. If supplied, the run produces one explanatory row per such URL and skips the fetch entirely (no charges burned on dead URLs). Also accepts **Google Maps hotel URLs / Place IDs / Feature IDs** (auto-resolved; see `googleMapsTarget`).

## `googleMapsInputs` (type: `array`):

Google Maps inputs to auto-resolve to hotels: full Maps URLs (`https://www.google.com/maps/place/...`), bare Place IDs (`ChIJRUjdceH8WRMRe-u4F6qScEI`), Feature IDs (`0x1359fce171dd4845:0x427092aa17b8eb7b`), or short share links (`maps.app.goo.gl/...`). The hotel's name + coordinates are read from Google and matched against the Expedia Group catalog; only a high-confidence match is scraped (pick the platform with `googleMapsTarget` below). Every input emits a `google-maps-resolved` row documenting the mapping (ids, match score, distance) — or a `google-maps-unresolved` row with the closest candidates; the actor never guesses. Google inputs pasted into Start URLs above work identically.

## `location` (type: `string`):

City or destination to search, e.g. London, Tokyo, New York. Leave empty to use Option A (URLs) instead.

## `searchCheckIn` (type: `string`):

Check-in date for the destination search (YYYY-MM-DD). Prices are a snapshot for this window. Leave empty to default to ~30 days from the run date. Only used in Option B.

## `searchCheckOut` (type: `string`):

Check-out date for the destination search (YYYY-MM-DD). Leave empty to default to the night after check-in. Only used in Option B.

## `searchAdults` (type: `integer`):

Number of adults per room for the search price quote. Defaults to 2. Only used in Option B.

## `searchSort` (type: `string`):

How to sort the destination search results.

## `searchCurrency` (type: `string`):

ISO 4217 currency code for search prices, e.g. USD, EUR, GBP, THB. Defaults to USD. Only used in Option B.

## `searchRegionId` (type: `string`):

Power-user override: skip city→regionId resolution and search this Expedia regionId directly (e.g. 6195474 for London City Centre). Leave empty to resolve from the Destination field above.

## `ratePositionReport` (type: `boolean`):

Adds a `rowType: "ratePositionReport"` row per searched destination (up to 5 per run, biggest first), built from this run's destination-search hotel rows (the Destination field above). Needs at least 3 hotels with a nightly rate per destination. Billed per report via the `rate-position-report` event; never charged when a report can't be built.

## `ratePositionSubject` (type: `string`):

Which hotel is YOURS (the report subject). Use (part of) the hotel name as it appears on Expedia, e.g. `Marriott Marquis`. When it matches a hotel in the search results, the report adds that hotel's nightly-rate percentile and guest-score percentile within its destination. Leave empty for a market-only report.

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

Maximum review rows scraped **per Start URL** (per hotel / activity / destination), newest first. This is a PER-URL cap, not a run total — a run with N URLs returns up to `maxItems × N` rows. That makes cost deterministic up front and guarantees every hotel returns data (one busy property can't consume the others' budget). Each review row is billed as one `result` event. Default: 1000 per URL. In `skipReviews` mode each hotel is a single summary row, so this has no effect there.

## `googleMapsUrlNote` (type: `string`):

**You can paste Google Maps URLs, bare Place IDs (`ChIJ...`), or Feature IDs (`0x...:0x...`) directly into `Start URLs` above — the actor auto-resolves them to the matching hotel and scrapes it in the same run (Expedia, Hotels.com, or both — see the toggle below).** How it works: the place's name and coordinates are read from Google, matched against the unified Expedia Group hotel catalog, and only a high-confidence match is scraped (a `google-maps-resolved` row documents the mapping incl. Expedia and Hotels.com ids and a match score). If no confident match exists, you get a `google-maps-unresolved` row listing the closest candidates with ready-to-use URLs — the actor never guesses, because scraping the wrong hotel's reviews is worse than none. Short share links (maps.app.goo.gl) are supported too.

## `googleMapsTarget` (type: `string`):

Which platform a Google Maps URL / Place ID / Feature ID in Start URLs resolves to. `expedia` (default) scrapes the matched hotel on Expedia; `hotels` scrapes it on Hotels.com (the legacy ho-id is mapped automatically); `both` scrapes the two platforms in one run.

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

Format should be YYYY-MM-DD, e.g., 2025-02-20

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

Upper date bound — keep only reviews posted on or before this day (YYYY-MM-DD). Combine with `reviewsFrom` for a closed date window. Leave empty for no upper bound.

## `minRating` (type: `integer`):

Keep only reviews whose overall score is at or above this value, on Expedia's 1–10 scale. Leave empty for no lower bound.

## `maxRating` (type: `integer`):

Keep only reviews whose overall score is at or below this value, on Expedia's 1–10 scale. Pair with `minRating` for a band (e.g. 1–4 to pull only negative reviews). Leave empty for no upper bound.

## `includeReviewMarkdown` (type: `boolean`):

When `true`, each review row gets a self-contained `reviewMarkdown` field — title, score, date, text, liked/disliked and stay context in one markdown string, ready to drop into a RAG / vector pipeline without reshaping the JSON. Off by default.

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

Order in which to fetch reviews. **All five values work**, using Expedia's own sort options. `mostHelpful` uses Expedia's most relevant order (Expedia has no separate helpfulness dimension; relevance already accounts for helpful-vote weighting). The sort order in use is printed at run start so you can verify what's being sent.

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

When `false`, the actor strips the `reviewPhotos` array from each emitted row. Useful for CSV exports where flat structure matters, or to make rows smaller. Default `true`.

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

When `false`, the actor strips the `hotelResponse` object from each emitted row. Set `false` when you only want guest-side review data. Default `true`.

## `skipReviews` (type: `boolean`):

When `true`, the actor does **not** scrape reviews — it emits a single summary row per hotel carrying whatever hotel-level data you enabled (room rates, meal-plan/cancellation pricing, property details, location, policies, images). Ideal for rate-intelligence / price-monitoring runs: no per-review charges, far fewer requests per hotel (so fewer 429s at scale). Leave `false` for normal review scraping.

## `includePropertySummary` (type: `boolean`):

When `true`, any property that returns **zero review rows** — e.g. nothing new inside your `reviewsFrom` window — still emits one hotel-summary row (`rowType: "hotel"`) carrying its cached `hotelOverallRating`, `hotelRatingLabel`, `hotelTotalReviews` and `hotelCategoryRatings` (plus any property details/images you enabled). Perfect for weekly monitoring: a property's current standing stays readable even on weeks with no new reviews, instead of that property being absent from the dataset. Requires `includeCategoryRatings: true` so the rating data is fetched. Off by default (fully backward-compatible). Distinct from `skipReviews`, which never scrapes reviews at all.

## `includeRatingDistribution` (type: `boolean`):

When `true`, attaches a `ratingDistribution` array to every row — the 5-band review-score histogram exactly as Expedia shows it: `[{ rating: 10, label: "Excellent", count: 523, percent: 52 }, { rating: 8, label: "Good", count: 231, … }, … 6-Okay, 4-Poor, 2-Terrible]`. It takes **one extra lookup per hotel** and is **billed one `additional-cost` event ($0.001) per hotel**. Pairs well with `includePropertySummary` for weekly monitoring. Off by default.

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

Fetch hotel-level aggregate fields such as overall rating, rating label, total reviews, and category ratings. Billed one `additional-cost` event ($0.001) per hotel/activity URL. Default: true.

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

Attach rich hotel-level descriptive fields to every review row: `propertyName`, `starRating`, `propertyType`, `propertyAddress`, `propertyCoordinates`, `propertyDescription`, `propertyLanguages`, `propertyAmenities` (categorized), and `propertyHighlights`. Extracted from the same page-layout response as the photo gallery — **no extra per-item charge**. Set `false` to omit these fields. Requires `includeCategoryRatings: true`.

## `includePropertyImages` (type: `boolean`):

Fetch every image URL in the hotel's photo gallery (typically 100-300 URLs per property) and attach them as `propertyImages` on every row. **Billed 1 `additional-cost` unit ($0.001) per image returned** — a 220-image hotel adds ~$0.22. Cap the count (and the cost) with `maxPropertyImages` below, or set `false` to skip the gallery and pay nothing for images. Requires `includeCategoryRatings: true`.

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

Keep only the **first N** gallery image URLs per hotel (gallery order — hero/cover shots first). You are **billed only for the images returned**, so `maxPropertyImages: 10` costs at most 10 `additional-cost` units ($0.01) per hotel instead of the full gallery's 100-300. Leave empty for the complete gallery. Only used when the photo gallery is enabled.

## `includeLocation` (type: `boolean`):

Attach a `propertyLocation` block to every review row — nearby landmarks/attractions, transit & airports, and restaurants, each with walk/drive times (e.g. "Utah State University — 18 min walk"). **Opt-in**: this fires a separate request per hotel and is **charged one `additional-cost` event ($0.001) per hotel**. Leave off if you don't need location data.

## `includePolicies` (type: `boolean`):

Attach a `propertyPolicies` block to every review row — check-in/check-out times & fees, minimum check-in age, pet policy, children & extra beds, deposits, accepted payment types, and 'You need to know' notes, grouped by section. **Opt-in**: fires a separate request per hotel and is **charged one `additional-cost` event ($0.001) per hotel**.

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

Attach a `propertyRooms` block to every row — each room type's name, bed config, occupancy, inclusions, nightly & total price, and cancellation terms. **Prices are a point-in-time snapshot** for the search window below (stamped in the output as `quotedFor`). **Opt-in**: fires a separate request per hotel and is **charged one `additional-cost` event ($0.001) per hotel**.

## `includeRoomOffers` (type: `boolean`):

Adds `mealPlans` and `cancellationOptions` to every room — each meal-plan (No extras / Half board / All-inclusive …) and cancellation-policy (Non-refundable / Fully refundable …) option with its '+ $X' price delta, exactly as shown on the Expedia room card. **Self-sufficient: enabling this alone also fetches the room rates** (you don't need to set `includeRooms` too). Premium rate-intelligence data — billed via the `additional-cost` event: the base rooms unit plus 1 unit per available room that carries an offer breakdown (the price tracks bookable rooms, not the number of rate permutations). Rooms with one set cancellation policy get it as a single selected option, at no extra charge.

## `roomsCheckIn` (type: `string`):

Check-in date for the room-rate quote, format YYYY-MM-DD. Only used when room rates are enabled. Leave empty to default to ~30 days from the run date.

## `roomsCheckOut` (type: `string`):

Check-out date for the room-rate quote, format YYYY-MM-DD. Only used when room rates are enabled. Leave empty to default to the night after check-in.

## `roomsAdults` (type: `integer`):

Number of adults for the room-rate quote. Only used when room rates are enabled. Defaults to 2.

## `roomsRegion` (type: `string`):

Which Expedia point-of-sale quotes the room prices — this decides the **currency** of `nightlyRate` / `totalRate` / `mealPlans` / `cancellationOptions`. **Auto (default)**: currency follows each input URL's own domain — `www.expedia.se` URLs quote in SEK, `www.expedia.co.jp` in JPY, plain `.com` (and bare hotel IDs) in USD. Pick an explicit region to force every quote into that currency regardless of URL form — e.g. bare hotel IDs + `Sweden` → SEK. All 33 regions verified live. Note: option labels (meal plans etc.) come back in that region's language; numeric `nightlyRateValue`/`totalRateValue` fields are always plain numbers.

## `roomsLanguage` (type: `string`):

Language of the room-rate **labels** (room names, meal plans, cancellation texts). **English (default)**: labels come back in English while prices keep the local currency — e.g. on `expedia.se` you get `"Breakfast buffet" + 237 kr` instead of `"Frukostbuffé" + 237 kr`. **Site default**: labels in the regional site's own language (Swedish on `.se`, German on `.de`, …). Currency is never affected by this setting — use the region selector above for that.

## `maxConcurrency` (type: `integer`):

Maximum number of pages that can be processed at the same time. For large hotel lists, 3-5 keeps long runs steady.

## `minConcurrency` (type: `integer`):

Minimum number of pages that will be processed at the same time.

## `maxRequestRetries` (type: `integer`):

Number of times the crawler will retry a failed request before giving up.

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

Specifies proxy servers that will be used by the scraper in order to hide its origin.<br><br>For details, see <a href='https://apify.com/apify/web-scraper#proxy-configuration' target='_blank' rel='noopener'>Proxy configuration</a> in README.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.expedia.co.uk/Kastoria-Hotels-Hotel-Anastassiou.h6136437.Hotel-Information",
    "https://www.expedia.com/h17369936.Hotel-Information",
    "https://www.expedia.com.br/Rio-De-Janeiro-Hotels-Premier-Copacabana-Hotel.h53296.Hotel-Information",
    "https://www.expedia.com.sg/things-to-do/singapore-hop-on-hop-off-bus-tour-by-open-top-bus.a203344.activity-details",
    "600217",
    "https://expe.app.link/KjB1EuacRSb",
    "https://el.hotels.com/ho434012/hotel-anastassiou-kastoria-ellada/"
  ],
  "googleMapsInputs": [
    "ChIJRUjdceH8WRMRe-u4F6qScEI"
  ],
  "location": "London",
  "searchAdults": 2,
  "searchSort": "RECOMMENDED",
  "searchCurrency": "USD",
  "ratePositionReport": false,
  "maxItems": 1000,
  "googleMapsTarget": "expedia",
  "includeReviewMarkdown": false,
  "reviewSort": "newest",
  "includeReviewPhotos": true,
  "includeOwnerResponses": true,
  "skipReviews": false,
  "includePropertySummary": false,
  "includeRatingDistribution": false,
  "includeCategoryRatings": true,
  "includePropertyDetails": true,
  "includePropertyImages": true,
  "includeLocation": false,
  "includePolicies": false,
  "includeRooms": false,
  "includeRoomOffers": false,
  "roomsAdults": 2,
  "roomsRegion": "auto",
  "roomsLanguage": "english",
  "maxConcurrency": 10,
  "minConcurrency": 1,
  "maxRequestRetries": 100,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# 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 = {
    "startUrls": [
        "https://www.expedia.co.uk/Kastoria-Hotels-Hotel-Anastassiou.h6136437.Hotel-Information",
        "https://www.expedia.com/h17369936.Hotel-Information",
        "https://www.expedia.com.br/Rio-De-Janeiro-Hotels-Premier-Copacabana-Hotel.h53296.Hotel-Information",
        "https://www.expedia.com.sg/things-to-do/singapore-hop-on-hop-off-bus-tour-by-open-top-bus.a203344.activity-details",
        "600217",
        "https://expe.app.link/KjB1EuacRSb",
        "https://el.hotels.com/ho434012/hotel-anastassiou-kastoria-ellada/"
    ],
    "googleMapsInputs": [
        "ChIJRUjdceH8WRMRe-u4F6qScEI"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/expedia-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 = {
    "startUrls": [
        "https://www.expedia.co.uk/Kastoria-Hotels-Hotel-Anastassiou.h6136437.Hotel-Information",
        "https://www.expedia.com/h17369936.Hotel-Information",
        "https://www.expedia.com.br/Rio-De-Janeiro-Hotels-Premier-Copacabana-Hotel.h53296.Hotel-Information",
        "https://www.expedia.com.sg/things-to-do/singapore-hop-on-hop-off-bus-tour-by-open-top-bus.a203344.activity-details",
        "600217",
        "https://expe.app.link/KjB1EuacRSb",
        "https://el.hotels.com/ho434012/hotel-anastassiou-kastoria-ellada/",
    ],
    "googleMapsInputs": ["ChIJRUjdceH8WRMRe-u4F6qScEI"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/expedia-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 '{
  "startUrls": [
    "https://www.expedia.co.uk/Kastoria-Hotels-Hotel-Anastassiou.h6136437.Hotel-Information",
    "https://www.expedia.com/h17369936.Hotel-Information",
    "https://www.expedia.com.br/Rio-De-Janeiro-Hotels-Premier-Copacabana-Hotel.h53296.Hotel-Information",
    "https://www.expedia.com.sg/things-to-do/singapore-hop-on-hop-off-bus-tour-by-open-top-bus.a203344.activity-details",
    "600217",
    "https://expe.app.link/KjB1EuacRSb",
    "https://el.hotels.com/ho434012/hotel-anastassiou-kastoria-ellada/"
  ],
  "googleMapsInputs": [
    "ChIJRUjdceH8WRMRe-u4F6qScEI"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call memo23/expedia-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,memo23/expedia-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/6I1hBKNMgBNHeYxuL/builds/anmh1bZIh3nFN4hcS/openapi.json
