# Restaurant Menu Pricing Modifiers Scraper (`arezdev/restaurant-menu-pricing-modifiers-scraper`) Actor

Extract public menu items, prices, descriptions and modifier groups from ChowNow, Square Online, BentoBox and supported Schema.org Menu pages. Export structured restaurant data for menu research, competitor pricing and FoodTech workflows.

- **URL**: https://apify.com/arezdev/restaurant-menu-pricing-modifiers-scraper.md
- **Developed by:** [Arezdev](https://apify.com/arezdev) (community)
- **Security:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 menu item 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

**Turn public restaurant ordering pages into clean menu, price, and modifier data for FoodTech research, POS prospecting, and menu inflation tracking.**

Extract one flat record per menu item from ChowNow Direct, Square Online catalogs, BentoBox menu markup, and Schema.org Menu pages. Results include published descriptions, prices, categories, dietary labels, images, restaurant contact details, and modifier groups. Requests use HTTP streams and direct JSON endpoints.

Best for menu research and comparing published item prices. Each successful row is a menu item. Use a direct ordering or menu URL; Toast pages need compatible public menu markup, and checkout-only pricing or browser-only menus may be unavailable.

### Ready-to-run example

The Input form includes public example values. A fresh `apify run` uses the input schema defaults. To explicitly select the small sample, including when input was saved by an earlier local run, use:

```sh
apify run --input-file .actor/input.example.json
```

You can also paste [.actor/input.example.json](.actor/input.example.json) into the JSON input editor in Apify Console. Replace the example sources with your own targets and increase the limits when you are ready.

### How to use

1. Paste public restaurant menu or ordering URLs into **Restaurant ordering or menu URLs**.
2. Leave **Include modifier summaries** enabled to capture add-ons, selection limits, and option prices.
3. Choose a global record limit and run the Actor.
4. Open the **Restaurant menu items** output table or export JSON, CSV, Excel, or another supported Apify format.

Try this small Square Online menu example:

```json
{
  "restaurantUrls": [
    "https://hudsonsplace.square.site/"
  ],
  "includeModifiers": true,
  "maxItems": 3,
  "timeoutSecs": 15,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

The Console sample uses a public Square Online menu and saves at most three rows. Check the sample output, then replace the URL with your target restaurant. Live menus can change or become unavailable; HTTP errors appear as diagnostic rows.

### What data can you extract?

| Fields | Meaning |
| --- | --- |
| orderingPlatform, status | Detected platform and SUCCESS or ERROR |
| restaurantName, restaurantPhone | Published restaurant identity and phone |
| streetAddress, city, state, zipCode | Published restaurant address |
| menuCategory, itemId, itemName | Menu section, stable source ID, and dish name |
| itemDescription, basePrice, currency | Description, numeric starting price, and published currency |
| dietaryLabels | Published dietary labels separated by semicolons |
| hasModifiers, modifierGroupsSummary | Modifier flag and readable option groups with price adjustments |
| itemImageUrl, sourceUrl, scrapedAt | Public image, submitted source URL, and UTC extraction time |
| errorMessage | Safe error information for failed targets; null on successful items |

All output fields are scalar values or null. Unknown prices and currencies remain null. Prices are expressed in major currency units. A zero price remains numeric zero. Prices exclude taxes, delivery fees, service fees, and checkout totals.

ChowNow serving-size children are combined into one menu item when the API publishes their parent relationship. The default size supplies the base price; the summary lists each size's full price. Square SKU variations are similarly summarized as full variation prices. Other modifier prices represent published adjustments. Popular-item duplicates are removed by source item ID. Dietary labels are extracted from published structured labels.

### Input settings

| Setting | Default | Description |
| --- | --- | --- |
| restaurantUrls | Required | Up to 10,000 public HTTP or HTTPS menu URLs; duplicate URLs are removed |
| includeModifiers | true | Include flat group and option summaries; preserve known modifier flags when disabled |
| maxItems | 3 | Global saved-record limit, 1-10,000; Console prefill is 3 |
| timeoutSecs | 15 | Total request deadline in seconds, 3-60, including retries and redirects |
| maxConcurrency | 25 | Upper bound, 1-100; actual workers adapt to memory and the remaining record limit |
| proxyConfiguration | Optional | Configure direct-only requests, custom proxies, or an explicitly enabled final Apify proxy tier |

Without a forced proxy setting, requests start directly. HTTP 403, 429, and 503 can trigger an Apify datacenter retry: SHADER first, then the account's default datacenter proxy when SHADER is unavailable. Your configured final proxy tier is used only when useApifyProxy is explicitly true. Residential groups are never enabled automatically.

Setting useApifyProxy to false forces direct requests. Nonempty proxyUrls force those proxies. Custom proxies must support HTTP CONNECT. Proxy tunnels use validated destination IPs while preserving the restaurant hostname for HTTP authority and TLS verification. Apify proxy retries require the platform-provided proxy password; the Actor does not retrieve account details or call the users/me endpoint.

### Output example

```json
{
  "orderingPlatform": "ChowNow",
  "status": "SUCCESS",
  "restaurantName": "Example Kitchen",
  "restaurantPhone": "+12125550100",
  "streetAddress": "12 Main St",
  "city": "New York",
  "state": "NY",
  "zipCode": "10001",
  "menuCategory": "Lunch",
  "itemId": "soup-1",
  "itemName": "Tomato Soup",
  "itemDescription": "Slow-cooked tomato soup.",
  "basePrice": 8,
  "currency": "USD",
  "dietaryLabels": "Vegan",
  "hasModifiers": true,
  "modifierGroupsSummary": "Extras [min 0, max 2]: Bread (USD 1.50), Croutons (USD 0.00)",
  "itemImageUrl": null,
  "sourceUrl": "https://restaurant.example.com/menu",
  "scrapedAt": "2026-10-20T12:00:00.000Z",
  "errorMessage": null
}
```

This example is illustrative. Menu text is converted to plain text. Potential spreadsheet formulas are prefixed with an apostrophe for safer CSV exports, while valid phone numbers and numeric prices are preserved. Render extracted text as text in downstream applications.

### Use the results in Clay, n8n, Make, or your API

- **Clay:** Import the exported CSV or connect your Apify workflow. Match restaurant domains and names to enrichment tables.
- **n8n:** Run the Actor with an Apify node, wait for completion, then read the run's defaultDatasetId. Filter status to SUCCESS before enrichment.
- **Make:** Trigger an Apify run, retrieve dataset items, and route records to Sheets, Airtable, or your CRM.
- **API:** Start the Actor through Apify's API and fetch items from the default dataset returned in the run. Use sourceUrl and itemId as a comparison key across scheduled runs.

[Apify API documentation](https://docs.apify.com/api/v2) describes authenticated runs, dataset exports, and scheduling integrations.

### Pricing and limits

The custom pay-per-event event is **menu-item-scraped**. Each saved SUCCESS item triggers that event only when it is configured in the run's pricing information. **ERROR rows cost $0 in custom event fees.** Network failures remain visible in the dataset.

For a concrete illustration, at an event price of **$0.001 per successful item**, 1,000 successful items cost **$1.00 in event fees**. Adding 100 error records adds **$0.00 in custom event fees**. This is an arithmetic example; the actual event price must be configured and displayed in Apify Console. Compute, storage, proxy charges, or other platform fees can still apply.

Free Apify accounts are limited to **25 saved records per run**, including SUCCESS and ERROR records. Paying accounts follow maxItems, up to 10,000 records. Local runs follow maxItems without the free-account cap. Reaching either the record cap or the event budget stops new work, cancels in-flight requests, flushes buffered records, and exits cleanly.

Batches flush every 100 records or five seconds. Error batches are pushed without the custom event name. Success batches are serialized and restricted to the remaining chargeable budget.

[Apify's pay-per-event documentation](https://docs.apify.com/sdk/js/docs/concepts/pay-per-event) explains event setup and budget behavior.

### FAQ and troubleshooting

**Which ordering URLs work?**

ChowNow URLs must contain both company and location IDs, such as /order/25580/locations/37827. Square Online subdomains and custom domains exposing public storefront identifiers are supported. BentoBox and other restaurant websites must publish JSON-LD MenuItem data, MenuItem microdata, or recognizable semantic menu cards.

**Does this require a browser or an API key?**

Extraction uses public HTTP endpoints and page markup. Merchant authentication is not required. Private dashboards, ordering checkout sessions, and login-only catalogs are outside the extraction scope.

**Why did a target produce an ERROR row?**

The restaurant may be inactive, blocked, unavailable, or missing a publicly readable menu. Error codes identify DNS, TLS, timeout, HTTP, unsafe URL, oversized payload, or missing-menu problems. Per-restaurant failures do not fail the whole run. Invalid Actor input exits with code 1.

**Why are some fields null?**

A site may omit a phone number, address, image, currency, or price. The Actor preserves missing values rather than inventing them. Schema.org menus often lack modifier details.

**Are PDF or image menus supported?**

Provide a public HTML ordering or menu page. OCR, PDF menu extraction, and menus rendered solely by JavaScript without a public catalog or embedded data are outside this Actor's supported formats. When a homepage links to an HTML menu, the Actor tries up to three same-origin menu links.

**How are large or interrupted responses handled?**

Decompressed limits are 4 MiB for HTML, 2 MiB for JSON, and 15 MiB for XML requests. Oversized HTML can retain its prefix. A timeout or connection reset after a successful response and at least 16 KiB of HTML can also retain a partial page. Only complete recognized menu items are emitted; an incomplete JSON response is rejected. A warning identifies partial HTML. User cancellation does not salvage in-flight responses.

**How are requests secured?**

Targets and each of up to six redirect hops are checked. Private, local, reserved, and mixed public/private DNS results are blocked. Requests pin validated DNS addresses, preserve domain authority, and verify TLS certificates. JSON pollution keys and external markup declarations are rejected or removed. JSON and HTML nesting are capped at 128 levels.

### Development and deployment

Requires Node.js 22 or newer.

```sh
npm ci
npm run build
npm test
npm run smoke
npm run verify:local
```

npm test uses node:test with deterministic fixtures and no network dependency. npm run smoke makes live, direct requests to public Square and BentoBox examples and reports timing and process memory. Smoke targets can be passed as command-line arguments. npm run verify:local launches isolated SDK processes for a live Square menu, an inactive source, and invalid input. All child runners use UV_THREADPOOL_SIZE=64 and NODE_ENV=production.

For a local Actor run, place a valid input in storage/key_value_stores/default/INPUT.json, then run npm start. Local dataset records appear under storage/datasets/default. The examples/input.json file contains a small live sample.

The multi-stage Dockerfile builds on node:22-alpine, installs from the lockfile, and runs as the non-root node user. It sets UV_THREADPOOL_SIZE=64, NODE_ENV=production, and a bounded V8 heap. The default Apify run uses 256 MiB and a 3,600-second run timeout.

When deploying in Apify Console, select **LIMITED_PERMISSIONS**, configure the **menu-item-scraped** pricing event and its price, and build from this repository. Limited-permission compatibility is enforced by avoiding account-profile API calls and using the current run's default storage. Publishing and pricing configuration happen in Apify Console; local source files do not activate monetization.

The implementation was validated against the [ChowNow public ordering application](https://hudsonsplace.square.site/), [Square storefront markup](https://hudsonsplace.square.site/), and the [Schema.org Menu vocabulary](https://schema.org/Menu).

The lockfile overrides the SDK's transitive basic-ftp dependency to 6.2.3 to include the [upstream parser security fixes](https://github.com/patrickjuchli/basic-ftp/releases). FTP target URLs remain unsupported.

# Actor input Schema

## `restaurantUrls` (type: `array`):

Public HTTP or HTTPS restaurant URLs. ChowNow URLs must include the location ID. Omitted URLs use the public Hudson's Place Square Online menu for a first run.

## `includeModifiers` (type: `boolean`):

Include published option names, price adjustments, and selection constraints in a flat text summary.

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

Global limit across all restaurants, including error records. Free Apify accounts are capped at 25 records.

## `timeoutSecs` (type: `integer`):

Overall time limit per HTTP request, including redirects, fallback protocols, and proxy retries.

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

Upper bound on concurrent restaurant jobs. Concurrency adapts to the available memory and remaining item budget.

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

Omit for direct-first requests with datacenter retries on 403, 429, or 503. Explicit useApifyProxy enables your configured proxy as the final tier. Set useApifyProxy to false to force direct connections; custom proxyUrls force those proxies.

## Actor input object example

```json
{
  "restaurantUrls": [
    "https://hudsonsplace.square.site/"
  ],
  "includeModifiers": true,
  "maxItems": 3,
  "timeoutSecs": 15,
  "maxConcurrency": 25
}
```

# Actor output Schema

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

Default dataset items. Review each record status before using extracted data downstream.

# 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 = {
    "restaurantUrls": [
        "https://hudsonsplace.square.site/"
    ],
    "maxItems": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("arezdev/restaurant-menu-pricing-modifiers-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 = {
    "restaurantUrls": ["https://hudsonsplace.square.site/"],
    "maxItems": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("arezdev/restaurant-menu-pricing-modifiers-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 '{
  "restaurantUrls": [
    "https://hudsonsplace.square.site/"
  ],
  "maxItems": 3
}' |
apify call arezdev/restaurant-menu-pricing-modifiers-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arezdev/restaurant-menu-pricing-modifiers-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/CxigD1tfJZLVxDLTC/builds/Cai0pjo64pKRooRW3/openapi.json
