# Restaurant Menu Extractor - Prices, Dishes & PDF Menus (`quidnovi/restaurant-menu-extractor`) Actor

Turn any restaurant's own website into a structured menu: sections, dish names, descriptions, prices and dietary tags. Reads schema.org menus, Squarespace/BentoBox/WordPress menu blocks, HTML menu pages and PDF menus. No AI, no browser: fast and cheap. You pay only for menus actually extracted.

- **URL**: https://apify.com/quidnovi/restaurant-menu-extractor.md
- **Developed by:** [Quid Novi](https://apify.com/quidnovi) (community)
- **Categories:** Travel, 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 menu extracteds

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 Extractor – Menu Prices, Dishes & PDF Menus from Any Restaurant Website

**Restaurant Menu Extractor** turns a restaurant's own website into a clean, structured **menu dataset**: menu sections, dish names, descriptions, **menu prices** and dietary tags (vegan, vegetarian, gluten-free, dairy-free, spicy). It reads HTML menu pages **and PDF menus**, follows "Menu", "Dinner", "Drinks" and "Happy Hour" links automatically, and exports to JSON, CSV or Excel.

No AI and no headless browser, so it is fast and cheap. **You only pay for menus that were actually extracted.** Sites with no usable menu are reported with a reason and are free.

### What can this restaurant menu scraper do?

- 🍽️ **Extract menu items with prices** from any restaurant website URL (homepage or menu page)
- 📄 **Parse PDF menus** (including side-by-side multi-column layouts)
- 🧭 **Auto-discover menu pages**: dinner, lunch, brunch, drinks, wine, cocktails, desserts, happy hour
- 🧩 Reads **schema.org Menu (JSON-LD)**, **Squarespace menu blocks**, **BentoBox** menus and **WordPress restaurant-menu plugins**, plus a generic dish/price parser for everything else
- 🏷️ **Dietary tags** detected from names and descriptions
- 📊 **Price stats** per restaurant (min, max, median) for quick **menu price comparison** and **competitor menu analysis**
- ✅ **Pay only for success**: no charge for blocked, image-only or JavaScript-only menus
- 🤖 Respects **robots.txt**. Never crawls delivery or ordering platforms (DoorDash, Uber Eats, Grubhub, Toast online ordering, Yelp…), and tells you when a menu only lives there

### Who uses it?

- **Restaurant owners and operators**: track competitor menu prices, benchmark your pricing, and spot menu trends near you
- **Food distributors and CPG sales teams**: see which dishes and ingredients local restaurants feature before a sales call
- **Restaurant tech, POS and marketing agencies**: onboard a new client's menu in seconds instead of retyping it
- **Market researchers and data teams**: build menu price datasets and track menu inflation
- **AI agents and RAG apps**: give an LLM a clean, structured menu instead of messy HTML

### How to use it

1. Paste one or more restaurant website URLs into **Restaurant website URLs**.
2. (Optional) Turn on **One row per menu item** for a spreadsheet-friendly output.
3. Click **Start** and download your data as JSON, CSV, Excel or HTML, or pull it through the API.

### Input example

```json
{
  "startUrls": [{ "url": "https://www.hattieb.com" }, { "url": "https://zunicafe.com" }],
  "flattenItems": false,
  "maxPagesPerRestaurant": 6
}
```

### Output example (one restaurant, trimmed)

```json
{
  "url": "https://www.hattieb.com",
  "status": "ok",
  "restaurantName": "Hattie B's Hot Chicken - Birmingham",
  "itemCount": 27,
  "sectionCount": 9,
  "currency": "USD",
  "priceStats": { "min": 3.0, "max": 15.5, "median": 7.0, "count": 27 },
  "confidence": "high",
  "extractionMethods": ["schema.org-jsonld"],
  "items": [
    { "section": "On the Bone", "name": "Half Bird - All White", "price": 15.5, "priceText": "15.50",
      "description": "Half bird all white meat with choice of heat level", "dietary": [] }
  ],
  "charged": true
}
```

Statuses: `ok`, `no_menu_found`, `too_few_items`, `menu_on_ordering_platform`, `blocked_by_robots`, `fetch_error`, `unsupported_platform`. Only `ok` is charged.

### How much does it cost to scrape a restaurant menu?

You pay a flat price **per restaurant menu extracted** (see the Pricing tab), regardless of how many dishes or PDF pages the menu has. Failed sites are free. A run over 100 restaurant websites usually takes about 1–2 minutes.

### Limitations (honest)

- Menus published **only as images** or rendered **only by JavaScript** are not extracted. They are reported as `no_menu_found` and not charged. In our test on 25 random US restaurant sites, 10 menus were extracted (40%), 3 sites blocked crawlers through robots.txt, and 1 site had its menu only on an ordering platform.
- Prices come back as printed. Multi-price items like "5.00 / 6.50" keep the full `priceText`, and `price` holds the first number.
- `confidence: high` means structured markup; `medium` means the heuristic text or PDF parser was used.

### FAQ

**Is it legal to scrape restaurant menus?** This Actor only reads pages that restaurants publish openly on their own websites. It obeys robots.txt and skips third-party ordering and delivery platforms. You are responsible for how you use the data. Consult a lawyer if unsure.

**Can I monitor menu price changes?** Yes. Schedule the Actor (for example weekly) and compare `items` between runs.

**Does it work with the API, Make, Zapier, n8n or an AI agent (MCP)?** Yes, like every Apify Actor.

Found a menu that should work but doesn't? Open an issue on the Issues tab with the URL. We fix parsers quickly.

# Actor input Schema

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

Homepage or menu page of each restaurant's own website. The Actor finds linked menu pages and PDF menus automatically. Delivery/ordering/review platforms (DoorDash, Uber Eats, Grubhub, Toast ordering, Yelp…) are not crawled.

## `flattenItems` (type: `boolean`):

Off: one row per restaurant with a nested items array. On: one row per dish (easier for spreadsheets). Price is the same: per restaurant menu.

## `maxPagesPerRestaurant` (type: `integer`):

Homepage + menu pages + PDF menus to open per restaurant.

## `minItems` (type: `integer`):

Minimum priced items for a successful (charged) menu.

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

Max concurrency.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.hattieb.com"
    },
    {
      "url": "https://zunicafe.com"
    },
    {
      "url": "https://franklinbbq.com"
    }
  ],
  "flattenItems": false,
  "maxPagesPerRestaurant": 6,
  "minItems": 3,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Structured menus (sections, dishes, prices, dietary tags) and per-URL status.

# 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.hattieb.com"
        },
        {
            "url": "https://zunicafe.com"
        },
        {
            "url": "https://franklinbbq.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("quidnovi/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://www.hattieb.com" },
        { "url": "https://zunicafe.com" },
        { "url": "https://franklinbbq.com" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("quidnovi/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://www.hattieb.com"
    },
    {
      "url": "https://zunicafe.com"
    },
    {
      "url": "https://franklinbbq.com"
    }
  ]
}' |
apify call quidnovi/restaurant-menu-extractor --silent --output-dataset

```

## MCP server setup

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