# Zomato Scraper - Restaurants, Ratings & Reviews (`abotapi/zomato-scraper`) Actor

Scrape restaurants and reviews from zomato.com. Get names, cuisines, ratings and votes, cost for two, address and coordinates, phone, per-vertical ratings, delivery info, plus per-review rating, author, date, text, photos and tags. Search a city feed or paste URLs.

- **URL**: https://apify.com/abotapi/zomato-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 19 total users, 4 monthly users, 94.3% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 restaurant scrapeds

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

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

## Zomato Scraper: Restaurant Profiles, Ratings & Reviews

Zomato Scraper turns Zomato into a clean restaurant data API. Search a city's Delivery or Dining Out feed, or paste restaurant, feed, or keyword-search links, and get structured JSON for every restaurant: cuisines, ratings, cost, address, coordinates, phone numbers, delivery status, and full review text. Add incremental mode for recurring monitoring, or export straight into Notion, Linear, or Airtable.

### Why This Scraper?

- **Two ways to find restaurants.** Search one or more city slugs on the Delivery or Dining Out feed, with an optional keyword, or paste restaurant, feed, and keyword-search links directly.
- **Full restaurant profiles.** Cuisines, cost for two, per-vertical (Dining and Delivery) ratings, address, coordinates, phone numbers, chain name, and open/closed status, all on by default.
- **Reviews included.** Attach up to 2000 reviews per restaurant with rating, author, date, like count, tags, and full text, sorted the way you want.
- **Offers filter.** Narrow the Dining Out feed and keyword search to restaurants currently running a dining offer.
- **Built for schedules.** Incremental mode returns only new and changed restaurants on recurring runs, with a resume option for one interrupted run.
- **Automatic mid-run checkpointing.** A run that gets migrated or restarted by the platform picks up where it stopped instead of re-collecting and re-charging restaurants already pushed.
- **Cost control.** Cap the number of restaurants and the number of feed pages walked per run, and turn detail or review fetching off entirely if you only need the feed listing.

### Use Cases

- **Food delivery market research:** compare cuisines, cost for two, and delivery times across a city or neighborhood.
- **Reputation and review analysis:** pull full review text and ratings to track sentiment for specific restaurants or chains.
- **Restaurant discovery apps:** feed structured listings into your own search, map, or recommendation tool.
- **Competitive monitoring:** schedule the actor and use incremental mode to catch new listings, rating changes, or closures over time.
- **Local business directories:** collect address, phone, and status data for restaurants in a city or region.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `id` | `"18685577"` |
| `name` | `"Olive Bar & Kitchen"` |
| `url` | `"https://www.zomato.com/ncr/olive-bar-kitchen-mehrauli-new-delhi"` |
| `cuisines` / `cuisineString` | `["Italian", "Mediterranean"]` / `"Italian, Mediterranean"` |
| `locality` | `"Mehrauli, New Delhi"` |
| `costForTwoText` | `"₹4,000 for two"` |
| `rating` | `{ "aggregate": 4.6, "text": "4.6", "subtitle": "Very Good", "votes": 3049 }` |
| `ratingsByVertical` | `{ "DINING": { "rating": 4.6, "reviewCount": 3049 }, "DELIVERY": { "rating": 4.1, "reviewCount": 1200 } }` |
| `timing` | `{ "text": "12noon to 1am", "isOpenNow": true }` |
| `thumbnail` | image URL |
| `deliveryTime` | `"30-35 mins"` |
| `hasOnlineOrdering` / `isServiceable` | `true` / `true` |
| `hasPromo` / `promoOffer` | `true` / `"20% OFF"` |
| `address` | `"One Style Mile, Kalka Das Marg, Mehrauli, New Delhi"` (with **Fetch full details**) |
| `latitude` / `longitude` | `28.5245` / `77.1855` (with **Fetch full details**) |
| `city` / `country` / `zipcode` | `"New Delhi"` / `"India"` / `"110030"` (with **Fetch full details**) |
| `phones` | `["+911234567890"]` (with **Fetch full details**) |
| `chainName` | `"Olive Group"` (with **Fetch full details**) |
| `statusText` / `isDeliveryOnly` / `isPermanentlyClosed` | `"Opens at 12noon"` / `false` / `false` (with **Fetch full details**) |
| `establishments` | `["Fine Dining", "Bar"]` (with **Fetch full details**) |
| `reviewsCount` | `1` (number of reviews captured in this record, not Zomato's total; with **Fetch reviews**) |
| `source` / `detailScraped` | `"search"` / `true` |
| `changeType` | `"NEW"` (incremental mode only) |

With **Fetch reviews** on (the default), each restaurant also carries a `reviews` array. Each entry has its own `id`, numeric `rating` and `ratingText`, full `text`, `author`, `authorProfileUrl`, `authorReviewsCount`, `authorFollowersCount`, `timestamp`, `likeCount`, `commentCount`, dining or delivery `experience`, `photos`, `tags`, `reviewUrl`, and, when the site returns them, `textShort` (currently always null: the site ships its short text as an empty string), `authorId`, `authorPhotoUrl`, `ratingColor`, `ratingBgColor`, `comments` and `ownerReplies` (the reader comments and the restaurant's replies, each a list of objects with `id`, `text`, `author`, `authorId`, `authorProfileUrl`, `authorPhotoUrl` and `timestamp`), `isEditable`, `externalUrl`, `externalHost`, and `externalHostText`. This is one array field on the restaurant record, not a separate record type: reviews never appear as their own top-level dataset rows.

### How to Use

1. Pick a **mode**: **Search a city** (city slugs and a feed) or **Paste URLs** (restaurant, feed, or keyword-search links).
2. For search mode, add one or more cities (a name such as Delhi NCR, Mumbai or Dubai, or a city slug such as `ncr`), choose **Delivery** or **Dining Out**, and optionally a keyword.
3. Leave **Fetch full details** and **Fetch reviews** on for full profiles, or turn either off to save on cost.
4. Set **Max restaurants** to control run size and cost, then click **Start**.

**Search a city's delivery feed:**

```json
{
  "mode": "search",
  "cities": ["ncr"],
  "context": "delivery",
  "maxItems": 20
}
```

**Search a keyword on the Dining Out feed, offers only:**

```json
{
  "mode": "search",
  "cities": ["mumbai"],
  "context": "dine-out",
  "query": "pizza",
  "offersOnly": true,
  "maxItems": 30
}
```

**Paste restaurant and feed URLs:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.zomato.com/ncr/olive-bar-kitchen-mehrauli-new-delhi",
    "https://www.zomato.com/mumbai/dine-out"
  ],
  "fetchReviews": true,
  "maxReviews": 50
}
```

**Supported URL shapes (url mode):** `zomato.com`, `www.zomato.com` and `m.zomato.com` all work.

| Shape | Example |
|---|---|
| Delivery city feed | `https://www.zomato.com/ncr/delivery` |
| Dine-out city feed | `https://www.zomato.com/ncr/dine-out` |
| Restaurants city feed (walked as the dine-out feed) | `https://www.zomato.com/ncr/restaurants` |
| Keyword search feed | `https://www.zomato.com/ncr/restaurants/pizza` |
| Keyword search as a query string (`?q=`; scraped as the keyword feed above) | `https://www.zomato.com/ncr/restaurants?q=pizza` |
| Delivery dish list | `https://www.zomato.com/ncr/delivery/dish-biryani` |
| Locality delivery list | `https://www.zomato.com/ncr/delivery-in-connaught-place` |
| City without a delivery list (warns that the site serves no list there; try dine-out) | `https://www.zomato.com/dubai/delivery` |
| Dine-out offers filter (turns on the Offers filter) | `https://www.zomato.com/ncr/dine-out?dining_payment_enabled_flag=true` |
| Paginated feed (`?page=N`; the feed is still walked from page 1, logged as a warning, and the URL counts as a partial scope for EXPIRED) | `https://www.zomato.com/ncr/restaurants?page=2` |
| Bare city page (walked as `/<city>/restaurants`) | `https://www.zomato.com/mumbai` |
| Restaurant page, base / order / reviews / info / menu / photos | `https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/order` |
| Mobile and apex hosts | `https://m.zomato.com/ncr/kfc-connaught-place-new-delhi/order`, `https://zomato.com/ncr/delivery` |

A URL the actor cannot handle is skipped with a warning in the run log naming the URL and the reason, never silently: another site, a non-city path, an unknown delivery sub-path, and the locality landing pages `/<city>/dine-out-in-<locality>` and `/<city>/<locality>-restaurants` (the site lists the whole city there, not the locality; use `/<city>/delivery-in-<locality>` instead). `?q=` is not honoured on delivery feeds (warned); a restaurant page that cannot be read produces a warning and no row.

**Default URLs:** if you run url mode with the **URLs** field untouched (or omit `urls` through the API), the default list holds one example URL of each shape above except the near-duplicate paginated form (18 URLs, the restaurant pages all resolve to the same restaurant). The list is walked in order until **Max restaurants** (default 20) is reached, so with the default settings only the first feed (or feeds) is scraped and the later example URLs are not reached; raise **Max restaurants** to walk more of them. Pass your own list, or an empty list, to change that. `restaurantUrls` is accepted as an alias of `urls`, and `proxyConfiguration` as an alias of `proxy`.

**Listing only, no detail or review fetch (lowest cost per restaurant):**

```json
{
  "mode": "search",
  "cities": ["mumbai", "dubai"],
  "context": "dine-out",
  "fetchDetails": false,
  "fetchReviews": false,
  "maxItems": 100
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/zomato-scraper").call(run_input={"mode": "search", "cities": ["ncr"], "context": "delivery"})
for restaurant in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(restaurant["name"], restaurant["rating"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/zomato-scraper').call({ mode: 'search', cities: ['ncr'], context: 'delivery' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and this run returns only restaurants not already collected there. A separate, automatic mid-run checkpoint also protects against platform migrations without any input needed, so an interrupted run never re-pushes (or re-charges) a restaurant it already collected.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same city, feed, or URL set. The first run returns everything as `NEW`. Later runs classify each restaurant as `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), or `EXPIRED` (only after a run that scanned the full tracked scope without a cap or a resume, and only with `emitExpired` on). `REAPPEARED` is produced only for a restaurant that an earlier run marked as expired (which requires `emitExpired` on and a complete scan in that earlier run) and that then shows up again in any later run; it does not appear on an ordinary recurring run that never turned `emitExpired` on. `stateKey` names or shares the stored state; leave it empty and the actor derives one automatically from the city/feed/URL and detail/review settings. Change detection compares the restaurant fields (name, cuisines, address, rating, cost, timings text, phones, review text) and ignores live or rotating ones by default while keeping them in the output: `thumbnail`, `isOpenNow`, `deliveryTime`, `promoOffer`, the relative review `timestamp` (such as "15 hours ago"), vote and review counters, and per-review like, comment and author counters plus the per-review reply thread (`comments`). Add more with `ignoreFieldsForChanges` (names match at any depth). Changing the ignore list never causes a one-time wave of `UPDATED` rows. With incremental mode off, output is exactly as before.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped restaurants into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each restaurant, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where restaurants should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (city and feed) or `url` (paste links). |
| `cities` | array | none (prefill: `["ncr"]`) | City names (Delhi NCR, Mumbai, Dubai) resolved with the site's own location lookup, or Zomato city slugs. A name that matches nothing stops the run with an error. If left empty in search mode, the run falls back to `ncr`. |
| `context` | string | `delivery` | `delivery` or `dine-out` feed, for search mode. |
| `query` | string | none | Optional cuisine, dish, or restaurant keyword to narrow the feed. |
| `offersOnly` | boolean | `false` | Apply Zomato's own Offers filter. Only affects the Dining Out feed and keyword search; Delivery has no such filter. |
| `urls` | array | one example URL of each supported URL shape (18 URLs, see above), walked in order until Max restaurants is reached | Restaurant, city-feed, bare-city, or keyword-search URLs, for url mode. Alias: `restaurantUrls`. |
| `fetchDetails` | boolean | `true` | Enrich each restaurant with address, coordinates, phone, per-vertical ratings, and taxonomy from its page. |
| `fetchReviews` | boolean | `true` | Attach reviews to each restaurant. |
| `maxReviews` | integer | `20` | Cap on reviews captured per restaurant (0 to 2000). `0` disables reviews. |
| `reviewsSort` | string | `popular` | `popular`, `newest`, `oldest`, `rating-high`, or `rating-low`. |
| `maxItems` | integer | `20` | Total restaurants to return across all cities or URLs. `0` = no limit, capped at 100000. |
| `maxPages` | integer | none (empty/`0` = unlimited) | Safety bound on feed pages walked per city or URL; the run still stops at Max restaurants or the feed's own end. |
| `resumeFromRunId` | string | none | ID of a previous run or dataset. Restaurants already there are skipped this run (a delta). |
| `incrementalMode` | boolean | `false` | Turn on for daily or recurring monitoring; returns only new and changed restaurants after the first run. |
| `stateKey` | string | none | Optional name for an incremental-mode monitoring campaign, to keep its state stable or deliberately share it. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) restaurants unchanged since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Also return (and bill) restaurants no longer found, marked `EXPIRED`, once a run has fully scanned the tracked search. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Extra field names (any depth) that are not compared when detecting changes; they stay in the output. |
| `proxy` | object | `{ "useApifyProxy": true }` | Connection settings. |
| `mcpConnectors` | array | none | Optional: send a summary of each restaurant to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which restaurant pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run (1 to 1000). Does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "id": "18685577",
  "name": "Olive Bar & Kitchen",
  "url": "https://www.zomato.com/ncr/olive-bar-kitchen-mehrauli-new-delhi",
  "cuisines": ["Italian", "Mediterranean", "Continental"],
  "cuisineString": "Italian, Mediterranean, Continental",
  "locality": "Mehrauli, New Delhi",
  "costForTwoText": "₹4,000 for two",
  "rating": { "aggregate": 4.6, "text": "4.6", "subtitle": "Very Good", "votes": 3049, "color": "3F7E00" },
  "ratingsByVertical": {
    "DINING": { "rating": 4.6, "reviewCount": 3049, "subtitle": "Very Good" },
    "DELIVERY": { "rating": 4.1, "reviewCount": 1200, "subtitle": "Very Good" }
  },
  "address": "One Style Mile, Kalka Das Marg, Mehrauli, New Delhi",
  "city": "New Delhi",
  "country": "India",
  "latitude": 28.5245,
  "longitude": 77.1855,
  "phones": ["+911234567890"],
  "chainName": "Olive Group",
  "hasPromo": true,
  "promoOffer": "20% OFF",
  "detailScraped": true,
  "reviews": [
    {
      "id": "r123",
      "rating": 5,
      "ratingText": "Excellent",
      "author": "Jane D.",
      "timestamp": "5 days ago",
      "text": "Fantastic ambience and food.",
      "likeCount": 3,
      "commentCount": 0,
      "tags": ["Great Ambience"]
    }
  ],
  "reviewsCount": 1,
  "source": "search"
}
```

#### Output views

The dataset has four views in the Console and API (`?view=`):

- **Restaurants Overview** (`overview`): one row per restaurant with name, cuisines, rating, cost, locality and delivery status.
- **Restaurant details** (`detail`): address, coordinates, phones, chain, status and per-vertical ratings.
- **Reviews, one row per review** (`reviews`): the nested `reviews` array unwound into one row per review, for spreadsheets. The stored dataset itself still has one record per restaurant.
- **Changes** (`changes`): `changeType` and `changedFields` for incremental runs.

A run summary is also saved to the `OUTPUT` key of the key-value store.

### Plan Requirement

Zomato pages were measured on 2026-10-19 over the standard Apify datacenter proxy pool (no challenge, full restaurant, feed and review data), so the default **Proxy** setting is the intended path and Residential is not required.

On an account without Apify Proxy access (an Apify free plan) the run still works: it logs a boxed notice (see below), continues on a backup connection, logs "Served over the backup connection (the default connection was unavailable)." and returns rows. The run fails only when every connection, the backup included, refuses the tunnel.

What you may see in the run log:

- If every connection, the backup included, refuses the tunnel the run ends as failed with: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required". If you entered your own proxy URLs, check their credentials.
- If the default connection is refused (for example on a free plan), the actor escalates to a backup connection and warns: "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." When a backup works it logs: "Served over the backup connection (the default connection was unavailable)."
- If residential access is unavailable and the backup cannot load the pages, the run fails with: "No results: residential proxies are unavailable on this run ... and the backup connection could not load the requested Zomato pages."
- If no results page could be loaded at all, the run fails with: "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search."

A run that loads no page never reports an empty success. A custom proxy URL is used as given, with no backup connections.

### FAQ

#### How much does it cost?

Billing is per event, and the Pricing tab shows the current rates:

| Event | Unit | When it is billed |
| --- | --- | --- |
| Restaurant result (`apify-default-dataset-item`) | one per restaurant row pushed | each restaurant returned; with incremental mode, unchanged restaurants are not pushed or billed unless **Emit unchanged** is on |
| Detail enrichment (`detail-enrichment`) | one per restaurant detail page fetched | only with **Fetch full details** on, for restaurants whose details were fetched and delivered |
| Review enrichment (`review-enrichment`) | one per review delivered | only with **Fetch reviews** on; billed per review as it is delivered, capped by **Max reviews per restaurant** (default 20, up to 2000) |

At the defaults (details on, 20 reviews) reviews are the largest part of the bill, so lower **Max reviews per restaurant** or turn off **Fetch reviews** for listing-only runs. Nothing is billed for restaurants or reviews that were not delivered.

Use **Max restaurants** and **Max pages per feed** to cap the cost of any run.

#### Is it legal to scrape Zomato?

This actor collects only publicly visible restaurant and review data; login-gated fields are not collected. You are responsible for how you use it: follow Zomato's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Review text and author names are user-generated content and may carry their own rights.

#### Can I get only new or changed restaurants on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated restaurants, and unchanged ones are not billed. Turn on **Emit expired** if you also want restaurants that disappeared marked `EXPIRED`; that only fires once a run has scanned the entire tracked search without a cap or a resume.

#### Why did my run return fewer restaurants than expected, or none at all?

Most causes return an empty or partial dataset rather than failing the run: a city slug or keyword with no results, a feed page that couldn't be read after retries, or **Max restaurants** capping the run early. Check the run's status message for the reason. The run fails outright only in two cases: an invalid `resumeFromRunId` (a run or dataset ID that doesn't resolve), or combining `resumeFromRunId` with incremental mode on a search that already has saved incremental state.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear, or Airtable.

### 🔗 Want more e-commerce data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🧩 <a href="https://apify.com/abotapi/opentable-reviews-scraper"><b>OpenTable Reviews Scraper</b></a><br>Scrape full OpenTable.com restaurant reviews by keyword, area, or restaurant profile...</td><td>🛒 <a href="https://apify.com/abotapi/grabfood-restaurants-scraper"><b>GrabFood Restaurant Scraper</b></a><br>Scrape GrabFood restaurants across Southeast Asia, including SG, MY, TH, VN, PH, ID, KH...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/wolt-restaurants-scraper"><b>Wolt.com</b></a><br>Scrape Wolt restaurants and full menus at the city scale. Extract 60+ fields, including...</td><td>🛒 <a href="https://apify.com/abotapi/doordash-scraper"><b>DoorDash Scraper</b></a><br>Extract structured doordash.com data at scale. Search by keyword or paste store URLs to...</td></tr>
<tr><td>🧩 <a href="https://apify.com/abotapi/resy-restaurant-availability-scraper"><b>Resy Scraper</b></a><br>Scrape Resy restaurants and live reservation availability by city or URL. Get bookable...</td><td>🛒 <a href="https://apify.com/abotapi/ikea-scraper"><b>IKEA Scraper</b></a><br>Scrape IKEA products across 50+ markets. Extract names, prices, currencies, ratings, full...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/zomato-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

search = pick a city and feed and walk it. url = paste restaurant, city-feed, or search URLs and walk them.

## `cities` (type: `array`):

One or more cities, typed as a name (Delhi NCR, Mumbai, Dubai) or as a Zomato city slug (ncr, mumbai, bangalore, dubai, london). Names are resolved with the site's own location lookup; a name that matches nothing stops the run with an error.

## `context` (type: `string`):

Which feed to walk in each city.

## `query` (type: `string`):

Optional cuisine, dish, or restaurant keyword to narrow the city feed, e.g. pizza, biryani, cafe.

## `offersOnly` (type: `boolean`):

Apply Zomato's own Offers filter to narrow the feed to restaurants currently running a dining offer. Only affects the Dining Out feed and keyword search (Delivery has no such filter on Zomato).

## `urls` (type: `array`):

Paste zomato.com links: city feeds (/<city>/delivery, /dine-out, /restaurants, /restaurants/<keyword> or /restaurants?q=<keyword>), a delivery dish list (/<city>/delivery/dish-<dish>), a locality delivery list (/<city>/delivery-in-<locality>), a bare city page (/<city>), the dine-out offers filter (?dining_payment_enabled_flag=true), or any restaurant page (/order, /reviews, /info, /menu, /photos). zomato.com, www and m hosts all work. Locality landing pages that list the whole city and unknown paths are skipped with a warning. Left untouched, the field scrapes one example of each shape. Alias: restaurantUrls.

## `fetchDetails` (type: `boolean`):

Enrich each restaurant with address, coordinates, phone, per-vertical ratings, and taxonomy from its page.

## `fetchReviews` (type: `boolean`):

Attach reviews (rating, author, date, text, photos, tags) to each restaurant.

## `maxReviews` (type: `integer`):

Cap on reviews captured per restaurant. 0 disables reviews.

## `reviewsSort` (type: `string`):

Order of reviews within each restaurant.

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

Total restaurants to return across all cities or URLs. 0 = no limit up to the cap of 100000.

## `maxPages` (type: `integer`):

Safety bound on feed pages walked per city or URL. Leave empty or 0 to walk all pages (the run still stops at Max restaurants, or the feed's own end).

## `proxy` (type: `object`):

Use Apify Proxy for reliable results. Some regional feeds are served best from the country they belong to.

## `resumeFromRunId` (type: `string`):

Optional. ID of a previous run of this actor (or a dataset ID). Restaurants already in that dataset are skipped, so this run returns only NEW restaurants (a delta). Combine both runs' datasets for the full set. For recurring daily monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching restaurants as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED restaurants. Turn on "Emit unchanged" or "Emit expired" only when you also want those restaurants returned (and billed). State is kept separately for each city/feed/URL and detail/review setup; use State key when you want to name or deliberately share a monitoring campaign. Changes are detected on the restaurant fields (name, cuisines, address, rating, cost, timings text, phones, reviews text). These live or rotating fields are ignored when comparing, but stay in the output: thumbnail, isOpenNow, deliveryTime, promoOffer, the relative review timestamp ("15 hours ago"), vote and review counters, and per-review like, comment and author counters and the per-review reply thread (comments) (add more under Ignore fields below). To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the city/feed/URL and detail/review settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return restaurants that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return restaurants that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max restaurants capped it or when Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names that must not make a restaurant count as UPDATED, in addition to the built-in list (thumbnail, isOpenNow, deliveryTime, promoOffer, the relative review timestamp, vote and review counters, per-review like, comment and author counters, and the per-review reply thread (comments)). A name matches at any depth, for example "costForTwoText" or "phones". Ignored fields still appear in the output; they are just not compared. Only used with Incremental mode.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify then Settings then API & Integrations, then select it here. Notion gets a page-per-item export; other connectors get a best-effort write. Leave empty to skip. Supported: Notion, Linear, Airtable, Apify.

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required only for the Notion export.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "cities": [
    "ncr"
  ],
  "context": "delivery",
  "offersOnly": false,
  "urls": [
    "https://www.zomato.com/ncr/delivery",
    "https://www.zomato.com/ncr/dine-out",
    "https://www.zomato.com/ncr/restaurants",
    "https://www.zomato.com/ncr/restaurants/pizza",
    "https://www.zomato.com/ncr/dine-out?dining_payment_enabled_flag=true",
    "https://www.zomato.com/ncr/restaurants?q=pizza",
    "https://www.zomato.com/mumbai",
    "https://www.zomato.com/ncr/delivery/dish-biryani",
    "https://www.zomato.com/ncr/delivery-in-connaught-place",
    "https://www.zomato.com/dubai/delivery",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/order",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/reviews",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/info",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/menu",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/photos",
    "https://m.zomato.com/ncr/kfc-connaught-place-new-delhi/order",
    "https://zomato.com/ncr/delivery"
  ],
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviews": 20,
  "reviewsSort": "popular",
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `summary` (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 = {
    "mode": "search",
    "cities": [
        "ncr"
    ],
    "urls": [
        "https://www.zomato.com/ncr/delivery",
        "https://www.zomato.com/ncr/dine-out",
        "https://www.zomato.com/ncr/restaurants",
        "https://www.zomato.com/ncr/restaurants/pizza",
        "https://www.zomato.com/ncr/dine-out?dining_payment_enabled_flag=true",
        "https://www.zomato.com/ncr/restaurants?q=pizza",
        "https://www.zomato.com/mumbai",
        "https://www.zomato.com/ncr/delivery/dish-biryani",
        "https://www.zomato.com/ncr/delivery-in-connaught-place",
        "https://www.zomato.com/dubai/delivery",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/order",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/reviews",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/info",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/menu",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/photos",
        "https://m.zomato.com/ncr/kfc-connaught-place-new-delhi/order",
        "https://zomato.com/ncr/delivery"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/zomato-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 = {
    "mode": "search",
    "cities": ["ncr"],
    "urls": [
        "https://www.zomato.com/ncr/delivery",
        "https://www.zomato.com/ncr/dine-out",
        "https://www.zomato.com/ncr/restaurants",
        "https://www.zomato.com/ncr/restaurants/pizza",
        "https://www.zomato.com/ncr/dine-out?dining_payment_enabled_flag=true",
        "https://www.zomato.com/ncr/restaurants?q=pizza",
        "https://www.zomato.com/mumbai",
        "https://www.zomato.com/ncr/delivery/dish-biryani",
        "https://www.zomato.com/ncr/delivery-in-connaught-place",
        "https://www.zomato.com/dubai/delivery",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/order",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/reviews",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/info",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/menu",
        "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/photos",
        "https://m.zomato.com/ncr/kfc-connaught-place-new-delhi/order",
        "https://zomato.com/ncr/delivery",
    ],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/zomato-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 '{
  "mode": "search",
  "cities": [
    "ncr"
  ],
  "urls": [
    "https://www.zomato.com/ncr/delivery",
    "https://www.zomato.com/ncr/dine-out",
    "https://www.zomato.com/ncr/restaurants",
    "https://www.zomato.com/ncr/restaurants/pizza",
    "https://www.zomato.com/ncr/dine-out?dining_payment_enabled_flag=true",
    "https://www.zomato.com/ncr/restaurants?q=pizza",
    "https://www.zomato.com/mumbai",
    "https://www.zomato.com/ncr/delivery/dish-biryani",
    "https://www.zomato.com/ncr/delivery-in-connaught-place",
    "https://www.zomato.com/dubai/delivery",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/order",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/reviews",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/info",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/menu",
    "https://www.zomato.com/ncr/kfc-connaught-place-new-delhi/photos",
    "https://m.zomato.com/ncr/kfc-connaught-place-new-delhi/order",
    "https://zomato.com/ncr/delivery"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/zomato-scraper --silent --output-dataset

```

## MCP server setup

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