# Viator Scraper — Tours, Prices, Ratings & Reviews (`tortuga/viator-scraper`) Actor

Scrape Viator tours and activities from destination, category and search pages: prices, ratings, reviews, duration, cancellation policy, images, plus optional details and reviews.

- **URL**: https://apify.com/tortuga/viator-scraper.md
- **Developed by:** [Trevor Ortega](https://apify.com/tortuga) (community)
- **Categories:** Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 tour scrapeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## Viator Scraper

Scrape tours and activities from Viator destination, category, attraction and search pages: prices, discounts, ratings, review counts, duration, cancellation policy, images, badges and, optionally, the full tour details (highlights, inclusions, itinerary with coordinates, meeting point, languages, supplier) and the newest reviews of each tour.

Built for reliability: plain HTTP with browser TLS fingerprints, no headless browser, no login. Every Viator page carries its full page model as embedded JSON, so the scraper reads structured data instead of fragile CSS selectors. You pay only for the tours (and reviews) you get.

See also: [GetYourGuide Scraper](https://apify.com/tortuga/getyourguide-scraper), the companion actor with the same input and output fields for GetYourGuide.

### What data does Viator Scraper extract?

Every tour from a listing page (destination, category, attraction or search):

| Field | Description |
|---|---|
| `id` | Viator product code (the `67584P2` at the end of the URL) |
| `title` | Tour title |
| `url` | Tour URL |
| `city` | City the tour belongs to ("Paris", "Versailles") |
| `destination`, `destinationId`, `region` | Destination of the page, its Viator ID and parent region/country |
| `priceFrom` | Lowest price ("from" price), number |
| `currency` | ISO currency code of the prices |
| `originalPrice` | Pre-discount price when the tour is on sale, else `null` |
| `discountPercent` | Discount in percent when on sale |
| `memberDiscountPercent` | Extra discount Viator shows to signed-in members, when offered |
| `priceVariesByGroupSize` | True when the price depends on group size |
| `rating` | Average rating 1-5 (one decimal, as shown on Viator) |
| `reviewCount` | Number of reviews |
| `duration` | Duration text ("2 hours", "3 hours to 3 hours 30 minutes") |
| `durationMinutes`, `durationMaxMinutes` | Duration in minutes (min and max) |
| `freeCancellation`, `cancellationPolicy` | Free cancellation flag and text (full policy text with details) |
| `badges` | "Best Seller", "Likely to Sell Out", "Special Offer", "Travelers' Pick", "Recommended by Travelers", "New on Viator" |
| `isBestseller`, `isLikelyToSellOut`, `isSpecialOffer`, `isTravelersPick`, `isPrivateTour`, `isNew` | Booleans |
| `tags` | Card features: "Free Cancellation", "Private Tour", "Price varies by group size", member price |
| `categories` | Category of the page (category and attraction pages) and the tour's breadcrumb with details |
| `languageCodes` | Guide language codes |
| `description`, `shortDescription` | Tour description texts |
| `images`, `photoCount`, `videoCount` | Image URLs (large size) and media counts |
| `latitude`, `longitude`, `maxGroupSize` | On attraction pages already from the listing; otherwise with details |
| `sourceUrl`, `scrapedAt` | Start URL that produced the item and the run timestamp |

With **Include tour details** on, each tour also gets:

| Field | Description |
|---|---|
| `highlights` | Highlight bullet points |
| `inclusions` / `exclusions` | What's included / not included |
| `additionalInfo` | Accessibility, fitness level, confirmation and other notes |
| `cancellationPolicy`, `cancellationPolicyDetails` | Full cancellation policy text and rules |
| `itinerary` | Itinerary stop names in order |
| `itineraryStops` | Stops with duration, admission info and coordinates |
| `meetingPoint`, `meetingPoints`, `endPoint` | Meeting point address and instructions, all start points, end point |
| `latitude`, `longitude` | Meeting point coordinates (falls back to the first itinerary stop) |
| `languages` | Guide languages ("English", "French", …) |
| `supplier`, `supplierId` | Tour operator |
| `maxGroupSize`, `maxTravelersPerBooking` | Group size limit and booking limit |
| `nextAvailableDate` | Next date with availability |
| `tourOptions` | Bookable options ("Group tour from Paris", "Semi-private from Paris") |
| `ratingDistribution` | Review count per star, e.g. `{"5": 3274, "4": 601, …}` |
| `mobileTicket`, `reserveNowPayLater`, `productType`, `timeZone` | Booking details |

With **Include reviews** on, reviews are saved as separate items with `type: "review"`:

| Field | Description |
|---|---|
| `tourId`, `tourUrl` | Tour the review belongs to |
| `reviewId` | Viator review ID |
| `rating` | Stars, integer 1-5 |
| `title`, `text` | Review title and text |
| `reviewerName` | Reviewer's nickname as Viator shows it on the review ("Lori_K"); `null` if the review shows none |
| `reviewerId`, `reviewerLocation` | Always `null` today: Viator's public reviews show no reviewer profile, id or home town. Kept so the shape matches our other review scrapers |
| `publishedAt` | Publication date and time (ISO) |
| `helpfulVotes` | "Helpful" votes |
| `language`, `isMachineTranslated` | Language of the site version the review was read from and translation flag |
| `source` | `VIATOR` or `TRIPADVISOR` |
| `ownerResponse`, `ownerResponseDate` | The operator's public reply |
| `photoCount` | Number of traveler photos attached |

Reviewer avatars are not collected, and neither is the account name of the operator staff member who wrote a response (only the response text).

### How to use Viator Scraper

1. Paste one or more Viator URLs into **Start URLs**: a destination (`https://www.viator.com/Rome/d511-ttd`), a category (`https://www.viator.com/Paris-tours/Walking-Tours/d479-g16-c56`), an attraction (`https://www.viator.com/Paris-attractions/Eiffel-Tower/d479-a89`), a search (`https://www.viator.com/searchResults/all?text=rome+food+tour`) or a single tour URL. Or type **Search terms** instead.
2. Set **Max tours** to cap the run (and the cost). Pages hold 24 tours each and are followed automatically.
3. Optionally pick a **Currency** and switch on **Include tour details** and/or **Include reviews**.
4. Click **Start**. Results appear in the **Dataset** tab; export as JSON, CSV or Excel, or read them through the API.

### Input example

```json
{
  "startUrls": [
    { "url": "https://www.viator.com/Paris/d479-ttd" },
    { "url": "https://www.viator.com/Paris-tours/Walking-Tours/d479-g16-c56" }
  ],
  "searchTerms": ["lisbon sunset cruise"],
  "maxItems": 200,
  "currency": "EUR",
  "includeDetails": true,
  "includeReviews": true,
  "maxReviewsPerTour": 20
}
```

### Output example

```json
{
  "id": "67584P2",
  "title": "Versailles Palace and Gardens Tour from Paris",
  "url": "https://www.viator.com/tours/Paris/Golden-Versailles-Palace-and-Garden-Tour/d479-67584P2",
  "city": "Paris",
  "destination": "Paris",
  "priceFrom": 72.85,
  "currency": "USD",
  "originalPrice": null,
  "rating": 4.4,
  "reviewCount": 4617,
  "duration": "3 hours to 3 hours 30 minutes",
  "durationMinutes": 180,
  "freeCancellation": true,
  "cancellationPolicy": "You can cancel up to 24 hours in advance of the experience for a full refund.",
  "badges": ["Best Seller", "Likely to Sell Out"],
  "isBestseller": true,
  "tags": ["Free Cancellation"],
  "images": ["https://dynamic-media.tacdn.com/media/photo-o/2e/cc/70/1e/caption.jpg?w=1600&h=1000&s=1"],
  "highlights": ["Explore the Palace of Versailles, a UNESCO World Heritage site, with a guide", "…"],
  "inclusions": ["Timed entry tickets to the Palace of Versailles", "Guided tour of the Palace of Versailles", "…"],
  "exclusions": ["Guide tips", "Hotel Pickup/Drop Off"],
  "itinerary": ["Paris", "Palace of Versailles", "Jardins du Chateau de Versailles"],
  "meetingPoint": "Café Pierre Hermé Pierre, Pl. de la Résistance, 75007 Paris, France - Your guide will meet you outside of Café Pierre Hermé…",
  "latitude": 48.8626944,
  "longitude": 2.3010142,
  "languages": ["English"],
  "supplier": "The Tour Guy",
  "maxGroupSize": 25,
  "ratingDistribution": { "1": 250, "2": 170, "3": 323, "4": 601, "5": 3274 }
}
```

Review item:

```json
{
  "type": "review",
  "tourId": "67584P2",
  "reviewId": 1079957055,
  "rating": 5,
  "title": "A+ Tour",
  "text": "Very informative and a beautiful place. I would definitely recommend this tour…",
  "reviewerName": "Lori_K",
  "reviewerId": null,
  "reviewerLocation": null,
  "publishedAt": "2026-09-30T08:22:07",
  "helpfulVotes": 0,
  "language": "en",
  "ownerResponse": "Hi, thank you for the wonderful A+ review! …"
}
```

### How much does it cost?

Pay per result: $0.002 per tour from a listing page, plus $0.003 per tour when **Include tour details** is on (one extra page per tour; a single tour URL as input always counts as a detailed tour), plus $0.001 per review. No subscription; Apify's free plan is enough to try it. 1,000 tours cost $2, or $5 with details.

### How do I scrape all tours in a city?

Use the destination URL (`https://www.viator.com/<City>/d<id>-ttd`) and raise **Max tours**. The scraper follows `/2`, `/3`, … pages until the last page Viator reports (Paris has over 4,000 tours in 171 pages). Category pages (`/<City>-tours/<Category>/d<id>-g<n>-c<n>`) and attraction pages (`/<City>-attractions/<Name>/d<id>-a<n>`) are the best way to get one segment, such as walking tours or Eiffel Tower tickets.

### How to scrape Viator reviews?

Switch on **Include reviews** and set **Max reviews per tour**. Reviews are saved newest first as separate dataset items with `type: "review"`, linked to the tour by `tourId`. Use the **Reviews** view of the dataset, or filter by `type`. Each review carries the reviewer's nickname as shown on Viator.

### Can I get prices in my currency?

Yes. Set **Currency** to any ISO code Viator supports (USD, EUR, GBP, AUD, CAD, …). All prices, including `originalPrice`, come back in that currency, and `currency` tells you which one was applied.

### Does it work without login or a browser?

Yes. All data comes from public pages that Viator serves to anonymous visitors. No account, cookies or headless browser are needed. Viator uses bot protection; the scraper rotates browser fingerprints and proxy sessions and retries challenged pages automatically. If your log shows many "DataDome challenge" retries, switch the proxy to the RESIDENTIAL group.

### Integrations and API

Use the run in Zapier, Make, n8n, Google Sheets, or call it from Python/Node with the Apify client. See the **API** tab for ready-made snippets. Schedule it to track prices, discounts or new tours in a destination.

### Is it legal to scrape Viator?

This Actor collects only data that Viator shows publicly to logged-out visitors: tour listings, prices, ratings, supplier business names, review texts and the reviewer's nickname as shown on each review (usually a first name and last initial, such as "Lori_K"). It does not collect avatars, contact details or anything behind a login. Reviewer nicknames are personal data under GDPR and similar laws: make sure you have a lawful basis for processing them, and do not use them to contact or profile individuals. You are responsible for how you use the data and for complying with Viator's terms and applicable law.

### Support

Found a bug or need a field added? Open an issue in the **Issues** tab; it is usually answered within a day.

# Actor input Schema

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

Viator pages to scrape: destination pages (https://www.viator.com/Paris/d479-ttd), category pages (https://www.viator.com/Paris-tours/Walking-Tours/d479-g16-c56), attraction pages (https://www.viator.com/Paris-attractions/Eiffel-Tower/d479-a89) or search results (https://www.viator.com/searchResults/all?text=rome+food+tour). A single tour URL (…/tours/Paris/…/d479-67584P2) returns that tour with full details. Listing pages are paginated automatically. Required unless you fill in Search terms.

## `searchTerms` (type: `array`):

Free-text searches, run like the Viator search box (e.g. "rome food tour", "grand canyon helicopter"). Viator returns at most 200 tours per search.

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

Stop after this many tours in total (across all start URLs and searches). Reviews do not count towards this limit; they are capped per tour by 'Max reviews per tour'. Listing pages hold 24 tours each.

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

3-letter ISO code (USD, EUR, GBP, AUD, CAD, …) to show prices in. Leave empty for the site default (usually USD).

## `includeDetails` (type: `boolean`):

Also open every tour page to get highlights, inclusions/exclusions, the full cancellation policy, itinerary stops with coordinates, meeting point, languages, supplier, group size, next available date and the rating distribution. Slower and charged extra per tour (see pricing).

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

Also save the newest reviews of every tour as separate dataset items (type "review"): rating, title, text, the reviewer's nickname as Viator shows it, date, helpful votes and the operator's response. Charged per review (see pricing).

## `maxReviewsPerTour` (type: `integer`):

Upper limit of reviews saved per tour when 'Include reviews' is on (newest first, 10 per request).

## `proxyConfiguration` (type: `object`):

Apify Proxy is recommended. Viator uses DataDome bot protection; if you see many 'DataDome challenge' retries in the log, switch to the RESIDENTIAL proxy group.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.viator.com/Paris/d479-ttd"
    }
  ],
  "maxItems": 100,
  "currency": "USD",
  "includeDetails": false,
  "includeReviews": false,
  "maxReviewsPerTour": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

# 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": [
        {
            "url": "https://www.viator.com/Paris/d479-ttd"
        }
    ],
    "currency": "USD",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("tortuga/viator-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": [{ "url": "https://www.viator.com/Paris/d479-ttd" }],
    "currency": "USD",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("tortuga/viator-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": [
    {
      "url": "https://www.viator.com/Paris/d479-ttd"
    }
  ],
  "currency": "USD",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call tortuga/viator-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tortuga/viator-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/OXL4dPr5VkzksBokY/builds/X4RNu3PCnH7ew3XIR/openapi.json
