# Restaurant Menu Scraper: Websites, Uber Eats, Skip, DoorDash (`yugenox/restaurant-menu-extractor`) Actor

Extract restaurant menus (sections, dishes, prices, photos) from restaurant websites, PDF menus, Clover, Uber Eats, SkipTheDishes and DoorDash, plus ordering, POS and reservation stack, socials, emails, phones, logo and brand colours. Optional dish-by-dish delivery-app price comparison.

- **URL**: https://apify.com/yugenox/restaurant-menu-extractor.md
- **Developed by:** [Yugenox Corp](https://apify.com/yugenox) (community)
- **Categories:** Lead generation, E-commerce, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $40.00 / 1,000 restaurant with menus

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

## Restaurant Menu Scraper: Website, Clover, Uber Eats, Skip, DoorDash

Give it a list of restaurant websites and get back **structured menus** (sections, dishes, descriptions, prices, photos). Each restaurant also comes with its **ordering, POS and reservation stack**, **socials**, **emails**, **phones**, **address and opening hours**, **logo** and **brand colours**.

The scraper reads structured menu data first and uses AI only when it has to:

1. **Structured data on the website.** Many restaurant sites publish their menu as schema.org data. It is exact, costs nothing extra, and nothing gets invented.
2. **Clover online ordering.** If the site links a Clover ordering page, the full POS menu is read with exact prices.
3. **AI reading of the menu page** for plain-text menus, including fine-dining menus that write prices as bare numbers ("29").
4. **AI reading of PDF menus** linked from the site.
5. **Delivery apps: Uber Eats, SkipTheDishes, DoorDash.** Used when the website has no readable menu, or for every restaurant if you want a price comparison.
6. **Menu photos (optional).** AI vision reads menus that exist only as images.

You can also paste an Uber Eats, SkipTheDishes, DoorDash or Clover link directly to get that store's menu.

### What you get

| Field | Example |
|---|---|
| `name`, `finalUrl`, `domain` | PAI Northern Thai Kitchen, https://paitoronto.com/ |
| `menu.source` | `json-ld`, `clover`, `llm-text`, `llm-pdf`, `llm-image`, `ubereats`, `skip`, `doordash` |
| `menu.sections[].items[]` | `{ name, description, price, priceValue, imageUrl, available }` |
| `menu.itemCount` / `menu.pricedCount` | 133 / 133 |
| `links` | order URL, reservation URL + provider, menu URL, PDF menus, Instagram, Facebook, TikTok, Uber Eats, DoorDash, Skip, Clover, Toast, Square… |
| `posStack` | `["clover", "opentable", "skip", "ubereats"]` |
| `emails`, `phones` | `["downtown@paitoronto.com"]`, `["+14169014724"]` |
| `business` | address, coordinates, opening hours, cuisine, price range, rating, all locations of a chain |
| `logoCandidates`, `brand` | logo URLs, `{ fonts: [...], colors: ["#e75230"] }` |
| `platformStores` | per ordering platform (Clover, Uber Eats, Skip, DoorDash): store name, URL, phone, address, rating, review count, open now |
| `channelPrices`, `channelMarkup` | the same dish on the website vs. Uber Eats / Skip / DoorDash, with markup % |
| `status`, `warnings` | `ok`, `no-menu`, `partial`, `skipped` (the listed website is a Facebook/Instagram/Yelp page), `blocked`, `not-found`, `unreachable`, `error`, and why |

#### Sample row (shortened)

```json
{
  "name": "Bank Bao",
  "finalUrl": "https://www.bankbao.ca/",
  "status": "ok",
  "menu": {
    "source": "clover",
    "currency": "CAD",
    "itemCount": 56,
    "pricedCount": 56,
    "sections": [
      { "name": "Baos", "items": [
        { "name": "BB Fried Chicken - Bao", "price": "8.95", "priceValue": 8.95, "imageUrl": "https://cloverstatic.com/…", "available": true }
      ] }
    ]
  },
  "links": { "orderUrl": "https://www.clover.com/online-ordering/bnk-bao-toronto", "instagram": "https://www.instagram.com/bankbao.ca/", "menuUrl": "https://www.bankbao.ca/menu" },
  "posStack": ["clover"]
}
```

With **Delivery-app menus: always**, each restaurant also gets its delivery-app menus and a dish-by-dish comparison:

```json
"channelPrices": [
  { "item": "Pepper Shrimp", "website": 20, "skip": 20, "doordash": 22, "markupPct": { "skip": 0, "doordash": 10 } }
],
"channelMarkup": {
  "doordash": { "comparedTo": "website", "matchedItems": 33, "medianMarkupPct": 0, "maxMarkupPct": 16.7, "minMarkupPct": 0 }
}
```

In **"One row per menu item"** mode, every dish becomes its own row with the restaurant name, website, section, price and source. You can drop that straight into a spreadsheet.

### Input

- **Restaurant websites or ordering links**: websites, or direct Clover / Uber Eats / SkipTheDishes / DoorDash store links.
- **Google Maps results (dataset ID)**: point it at the dataset of a Google Maps scraper run. Every place with a website is processed, and its place ID is carried over. Duplicate websites (chain locations) are processed once.
- **Delivery-app menus**:
  - `fallback` (default) uses the apps only when the website has no menu.
  - `always` also fetches every linked app and compares prices.
  - `never` reads the website only.
- **Menu sources**: switch sources on or off and set their priority.
- **Read menu photos**: opt-in AI vision for image-only menus.
- **Menu pages per site / PDF menus per site**: how deep to look.
- **Brand kit / Page text**: extra fields.
- **Output format**: one row per restaurant, or one row per menu item.

### Use cases

- **Restaurant tech and POS sales.** See which ordering system, POS and reservation platform each restaurant uses, and pull contacts in the same pass.
- **Delivery-app price monitoring.** Compare the restaurant's own prices with Uber Eats, SkipTheDishes and DoorDash, dish by dish.
- **Menu and food-trend analytics.** Collect structured menus across a city: dishes, price points, cuisines.
- **Market research and lead lists.** Enrich a Google Maps export with menus, socials and emails.
- **Local directories and apps.** Keep menus and opening hours current.

### FAQ

**How accurate are the prices?** Structured sources (website data, Clover and the delivery apps) give exact prices. AI-read menus are checked against the page: each price is matched to the price printed next to the dish, and dishes that don't appear in the source text are dropped. `pricedCount` tells you how many items have a price.

**Why does a restaurant show `no-menu`?** Some sites show their menu only as a photo, load it with scripts, or don't publish one. The `warnings` field says which case it is. Turning on *Read menu photos* or *Delivery-app menus: always* often fills the gap.

**What if a restaurant's "website" is its Facebook or Instagram page?** Those pages need a login, so they are not opened. The row is marked `skipped` and is not charged. When the input comes from a Google Maps dataset, the actor still looks the restaurant up on Uber Eats by name and location.

**Which rows am I charged for?** You pay per restaurant, and only when its site or ordering page was actually read and saved: one charge for a restaurant with a menu, or a smaller one for a restaurant that was read but has no menu. Each delivery-app menu fetched (Uber Eats, SkipTheDishes, DoorDash) and each menu photo read by *Read menu photos* is a small add-on. Blocked, missing, unreachable and skipped sites are free. In "one row per menu item" mode you are still charged per restaurant, not per row.

**What about menus with no prices on the website?** You still get the dish list, with `pricedCount: 0` and a warning. If the restaurant is on a delivery app, the app's priced menu is used instead.

**Do I need an account or cookies?** No. Everything is read from public pages.

**How fast is it?** Around 3–10 seconds per restaurant with 10 in parallel. Delivery-app pages take a little longer.

**Which countries work?** Any restaurant website works. The delivery-app lookups are tuned for Canada and the US.

**Is it legal to scrape restaurant menus from websites and delivery apps?** This scraper only collects public data: pages anyone can open without logging in, such as restaurant websites, their PDF menus and public store pages on Clover, Uber Eats, SkipTheDishes and DoorDash. Rows can include email addresses and phone numbers published on those pages, and some of them may belong to individual people, so handle them as personal data. Follow the privacy laws that apply to you (GDPR, PIPEDA, CCPA and similar) and the terms of the sites you collect from, and check with a lawyer if you are unsure about your use case. For background, see [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/)

**Does it access any private data?** No. It never logs in and uses no accounts, passwords or personal cookies. Pages that need a login, such as Facebook or Instagram profiles, are not opened (the row is marked `skipped`). It reads only what restaurants and ordering platforms show to every visitor: menus, prices, opening hours, business contact details, links and logos.

# Actor input Schema

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

Restaurant websites as full links (https://paitoronto.com). Several links pasted into one line are split automatically, and a remote text file with one website per line works too. You can also paste ordering links directly: Clover (clover.com/online-ordering/…), Uber Eats (ubereats.com/…/store/…), SkipTheDishes (skipthedishes.com/…) or DoorDash (doordash.com/store/…).

## `datasetId` (type: `string`):

Optional. The ID of a dataset produced by a Google Maps scraper. Every place that has a website is processed; its place ID and coordinates are carried over.

## `maxRestaurants` (type: `integer`):

Upper limit on how many restaurants are processed (after removing duplicate websites).

## `outputMode` (type: `string`):

One row per restaurant (with the menu nested inside) or one row per menu item (flat, spreadsheet-friendly).

## `followDeliveryPlatforms` (type: `string`):

Fallback: use Uber Eats / SkipTheDishes / DoorDash only when the website has no readable menu. Always: also fetch every linked delivery app and compare prices dish by dish (adds additionalMenus + channelPrices). Never: website only.

## `extractMenu` (type: `boolean`):

Turn off to collect only links, contacts, ordering/reservation stack and brand info (faster, cheaper).

## `menuSources` (type: `array`):

Which sources may be used, tried in this order. Structured data on the website is exact and free; AI reading handles plain-text and PDF menus.

## `ocrMenuImages` (type: `boolean`):

When a restaurant's menu exists only as images, read them with AI vision. Billed per image that turns out to be a menu.

## `maxMenuPages` (type: `integer`):

How many menu pages to open per website (lunch, dinner, drinks, other locations…).

## `maxPdfs` (type: `integer`):

How many linked PDF menus to read per website.

## `includeBrandKit` (type: `boolean`):

Add the website's main fonts and brand colours.

## `includePageText` (type: `boolean`):

Add the visible text of the home and menu pages (up to 60,000 characters) — useful for your own AI processing.

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

Stop after saving this many rows (restaurants, or menu items in item mode). Leave empty for no limit.

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

How many restaurants are processed at the same time.

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

Datacenter proxy is used by default; sites that block it are retried automatically on residential IPs.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://paitoronto.com"
    },
    {
      "url": "https://chubbysjamaican.com"
    }
  ],
  "maxRestaurants": 100,
  "outputMode": "restaurant",
  "followDeliveryPlatforms": "fallback",
  "extractMenu": true,
  "menuSources": [
    "json-ld",
    "clover",
    "website-llm",
    "pdf-llm",
    "ubereats",
    "skip",
    "doordash"
  ],
  "ocrMenuImages": false,
  "maxMenuPages": 3,
  "maxPdfs": 2,
  "includeBrandKit": true,
  "includePageText": false,
  "maxConcurrency": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Every row this run saved, as JSON.

## `resultsCsv` (type: `string`):

The same rows as CSV, ready for a spreadsheet.

## `resultsTable` (type: `string`):

The same rows narrowed to the key columns (the "overview" view).

# 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://paitoronto.com"
        },
        {
            "url": "https://chubbysjamaican.com"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("yugenox/restaurant-menu-extractor").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://paitoronto.com" },
        { "url": "https://chubbysjamaican.com" },
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("yugenox/restaurant-menu-extractor").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://paitoronto.com"
    },
    {
      "url": "https://chubbysjamaican.com"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call yugenox/restaurant-menu-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yugenox/restaurant-menu-extractor"
        }
    }
}
```

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/joHANxqio9ab1rOmA/builds/r8Ke9CamDsfwoxP72/openapi.json
