# Tripadvisor Menu Scraper (`rainminer/tripadvisor-menu-scraper`) Actor

Extract structured restaurant menu data from Tripadvisor — dish names, descriptions, prices, currencies, and categories read straight from the menu photos travellers upload. Built for price intelligence, food delivery catalogues, competitor menu research, and hospitality data pipelines.

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

## Pricing

from $0.37 / 1,000 menu items

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

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

![Tripadvisor](https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Tripadvisor_2025_Logo.svg/500px-Tripadvisor_2025_Logo.svg.png)

The **Tripadvisor Menu Scraper** is an Apify Actor that turns Tripadvisor **menu photos into structured menu data**. Give it a restaurant URL — or a whole city's restaurant list — and get back dish names, descriptions, prices, currencies, and categories in a flat dataset you can export to JSON, CSV, or Excel.

Most Tripadvisor restaurants have **no machine-readable menu at all**. What they do have is a **Menu photo album** filled with wall menus, chalkboards, laminated pages, and happy-hour cards uploaded by travellers. This Actor reads those photos with AI vision, which is the only way to get prices off them.

> **Important — AI output is not production-ready as-is.**\
> This Actor uses AI vision to read menu photos. Results can include misread names, wrong prices, invented or missing items, and inconsistent formatting. Treat the dataset as raw input for **your own data cleaning / QA pipeline** (confidence filters, validation against the source photo, human review) before any production use.

***

### Key Features

- **Reads photo-only menus**: extracts items and prices from the restaurant's Menu photo album — printed pages, wall boards, chalkboards, digital screens, and menu cards.
- **Whole cities in one run**: pass a Tripadvisor restaurant list URL and the Actor expands it into individual restaurants.
- **Real prices**: every item carries a parsed numeric `price`, the original `priceText` as written on the board, and an ISO currency code.
- **Traceable to the source**: each row keeps the `imageUrl` of the exact menu photo it came from, so any value can be re-checked by eye.
- **Confidence scoring**: each item gets a `0`–`1` confidence score so you can drop the low-certainty rows.
- **Language detection**: each item carries an ISO 639-1 language code detected from the item text itself, with an optional single-language filter.
- **Deduplication**: items appearing across several menu photos of the same restaurant are merged into one row.

***

### Why Scrape Tripadvisor Menus?

Tripadvisor is one of the largest public sources of restaurant information, and its menu data is locked inside traveller-uploaded images. Unlocking it powers:

- **Price intelligence** — track dish-level menu prices across competitors and over time.
- **Food delivery and aggregator platforms** — bootstrap menu catalogues for restaurants that never published one.
- **Restaurant and hospitality research** — compare offerings and price points across a city or a chain.
- **Dietary and allergy databases** — structured dish names and ingredient descriptions.
- **Travel and dining apps** — show visitors what to expect, and what it costs, before they sit down.

***

### Who Is It For?

- **Food-tech startups** that need affordable, scalable menu data.
- **Restaurant groups** auditing competitor menus and price positioning across locations.
- **Market researchers** studying pricing trends in hospitality.
- **Developers** building culinary discovery, reservation, or delivery products.

***

### Input Schema

```json
{
  "startUrls": [
    {
      "url": "https://www.tripadvisor.com/Restaurant_Review-g60763-d12874338-Reviews-Los_Tacos_No_1-New_York_City_New_York.html"
    },
    { "url": "https://www.tripadvisor.com/Restaurants-g60763-New_York_City_New_York.html" }
  ],
  "maxRestaurantsPerListUrl": 5,
  "maxMenuPhotos": 3,
  "maxItems": 200,
  "menuLanguage": "en",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "US"
  }
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `startUrls` | Array | — | Tripadvisor restaurant URLs (`Restaurant_Review-…`) and/or restaurant list URLs (`Restaurants-g…`) |
| `maxRestaurantsPerListUrl` | Integer | `5` | Max restaurants opened per restaurant list URL. Ignored for direct restaurant URLs |
| `maxMenuPhotos` | Integer | `3` | Max menu photos downloaded and analyzed per restaurant |
| `maxItems` | Integer | `200` | Max unique menu items written per start URL, after deduplication |
| `menuLanguage` | String | `"en"` | ISO 639-1 language filter (e.g. `en`, `es`, `fr`, `it`). Leave empty to keep every language |
| `proxyConfiguration` | Object | Residential (US) | Proxy settings. Residential proxies are required — see [Proxies](#proxies) |

***

### Output Schema

Each dataset item represents one unique menu item:

```json
{
  "placeUrl": "https://www.tripadvisor.com/Restaurant_Review-g60763-d12874338-Reviews-Los_Tacos_No_1-New_York_City_New_York.html",
  "restaurantName": "Los Tacos No. 1",
  "imageUrl": "https://media-cdn.tripadvisor.com/media/photo-w/16/b9/3d/99/photo1jpg.jpg",
  "category": "TACOS / TOSTADAS",
  "name": "CARNE ASADA",
  "description": "GRILLED STEAK",
  "price": 3.95,
  "currency": "USD",
  "priceText": "$3.95",
  "language": "en",
  "confidence": 0.95,
  "scrapedAt": "2026-08-01T07:34:31.701Z"
}
```

| Field | Description |
|---|---|
| `placeUrl` | Tripadvisor restaurant page URL |
| `restaurantName` | Display name of the restaurant |
| `imageUrl` | The menu photo this item was read from |
| `category` | Menu section header (e.g. `"Desserts"`) — `null` if not visible |
| `name` | Item name in its original language |
| `description` | Item description or ingredients — `null` if absent |
| `price` | Parsed numeric price without currency symbol (e.g. `8.99`) — `null` if absent |
| `currency` | ISO 4217 currency code (e.g. `"USD"`, `"EUR"`) — `null` if indeterminate |
| `priceText` | Original price text exactly as shown on the menu (e.g. `"$3.95"`) — `null` if absent |
| `language` | ISO 639-1 language code of the item text (e.g. `"en"`, `"it"`) |
| `confidence` | Model confidence that the item was read correctly (`0`–`1`). Filter out low scores if you only want high-certainty rows |
| `scrapedAt` | ISO timestamp of when this item was scraped |

***

### How It Works

1. **Input** — provide restaurant URLs and/or restaurant list URLs.
2. **Collect** — the Actor opens each restaurant page and its Menu photo album.
3. **Read** — menu photos are pre-filtered for readable text, then analyzed by AI vision.
4. **Output** — one dataset row per unique menu item, with prices and the source photo.

***

### Proxies

Tripadvisor is protected by DataDome. Plain HTTP requests are answered with **HTTP 403** no matter which proxy they come from — no proxy, datacenter, and residential are all refused — so restaurant pages are loaded in a hardened browser instead. The browser gets through, but a single IP is challenged again after a page or two: a proxy-less run reached one restaurant and was then blocked on every page that followed, while the same input over Apify residential proxies finished every restaurant with no failed requests.

**Residential proxies are therefore the default**, a deliberate exception to the usual preference for proxy-less or datacenter runs. A run against a single restaurant may well succeed without them; anything larger will not.

Menu photos themselves are served from a public media CDN and are fetched without a proxy, so residential traffic is spent only on the restaurant pages.

***

### Pricing

This Actor uses pay-per-event pricing with three events (see the Store pricing tab for current rates):

| Event | When charged |
|---|---|
| `place-scraped` | Once per restaurant page opened and inspected for menu photos, including restaurants with none |
| `menu-photo-analyzed` | Once per menu photo successfully analyzed by AI vision (capped by `maxMenuPhotos`) |
| `menu-item` | Once per unique menu item written to the dataset |

**Budgeting tips**

- **`place-scraped` is charged for every restaurant opened**, whether or not it has a Menu album. Loading a protected page over a residential proxy costs the same either way, and restaurants without menu photos produce no further charges.
- **`menu-item` is usually the dominant cost** for restaurants with long menus, because a large printed menu yields many dataset rows from a single photo.
- Lower `maxMenuPhotos` to cap analysis charges; lower `maxItems` to cap row charges.

***

### Notes and Limitations

- **AI-powered extraction**: output is **non-deterministic** and **not ready for production use without your own cleaning pipeline**. The same restaurant can produce different results across runs, and the model can misread names, prices, and descriptions, or label a dish with its description. Use the `confidence` field and validate against `imageUrl` before shipping data downstream. Photos are billed as analyzed even when they yield no usable items.
- **Photo availability**: results depend on what travellers uploaded. Restaurants with no Menu album produce no output — this is normal, not a failure.
- **Photos age at different rates**: a Menu album can mix boards photographed years apart. Prices belong to the photo in `imageUrl`, and when the same dish appears on several boards only one row survives deduplication. Set `maxMenuPhotos` to `1` if you only want the most prominent menu photo.
- **Photo quality**: handwritten, angled, or low-light menu photos yield less complete extractions and lower confidence scores.
- **Menu language**: set `menuLanguage` to the primary script of the menus you are scraping for best results with non-Latin alphabets.
- **Throughput**: restaurant pages are loaded through a hardened browser, so plan on roughly **20–30 seconds per restaurant** plus a few seconds per menu photo. Results are pushed per restaurant as soon as it finishes, so a timeout still keeps partial output.

***

**Something not working?**\
Use the **Issues** tab to report bugs or request features.

### Image Credit

Image credit: [tripadvisor.com](https://www.tripadvisor.com/)

# Actor input Schema

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

Tripadvisor restaurant review URLs (Restaurant\_Review-...) or restaurant list URLs (Restaurants-g...). List URLs are expanded into the restaurants they contain.

## `maxRestaurantsPerListUrl` (type: `integer`):

Maximum number of restaurants to open for each restaurant list URL. Ignored for direct restaurant URLs.

## `maxMenuPhotos` (type: `integer`):

Maximum number of menu photos to download and analyze per restaurant. Caps menu-photo-analyzed billing for that restaurant.

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

Maximum number of unique menu items to output per start URL after deduplication.

## `menuLanguage` (type: `string`):

ISO 639-1 language code for menu extraction (e.g. en, es, fr, it). When set, only items in this language are extracted — useful for bilingual menus that would otherwise produce duplicates. Leave empty to keep every language.

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

Residential proxies keep multi-restaurant runs alive — without them Tripadvisor blocks every page after the first restaurant. A one-restaurant run may succeed with no proxy at all.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.tripadvisor.com/Restaurant_Review-g60763-d12947829-Reviews-Carmine_s_44th_Street_NYC-New_York_City_New_York.html"
    }
  ],
  "maxRestaurantsPerListUrl": 3,
  "maxMenuPhotos": 3,
  "maxItems": 200,
  "menuLanguage": "en",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `overview` (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.tripadvisor.com/Restaurant_Review-g60763-d12947829-Reviews-Carmine_s_44th_Street_NYC-New_York_City_New_York.html"
        }
    ],
    "maxRestaurantsPerListUrl": 3,
    "maxMenuPhotos": 3,
    "maxItems": 200,
    "menuLanguage": "en",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("rainminer/tripadvisor-menu-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.tripadvisor.com/Restaurant_Review-g60763-d12947829-Reviews-Carmine_s_44th_Street_NYC-New_York_City_New_York.html" }],
    "maxRestaurantsPerListUrl": 3,
    "maxMenuPhotos": 3,
    "maxItems": 200,
    "menuLanguage": "en",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("rainminer/tripadvisor-menu-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.tripadvisor.com/Restaurant_Review-g60763-d12947829-Reviews-Carmine_s_44th_Street_NYC-New_York_City_New_York.html"
    }
  ],
  "maxRestaurantsPerListUrl": 3,
  "maxMenuPhotos": 3,
  "maxItems": 200,
  "menuLanguage": "en",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call rainminer/tripadvisor-menu-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rainminer/tripadvisor-menu-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/6RDbFRJscyp6Q2xL3/builds/Bt2LRvRATFrOL5Z0I/openapi.json
