Zomato holds one of the largest pools of restaurant feedback and contact data in India, the UAE and beyond. Popular restaurants carry tens of thousands of reviews, and every restaurant page also exposes a phone number, full address and GPS location. That combination (reviews for reputation work, plus contact fields for lead generation) is rare, and almost none of it is available through a supported channel. There is no public reviews API, the review data loads from a separate front-end route that expects a real browser, and the listing pages paginate behind a session.
This post shows the honest Python starting point, why it stalls, and a no-code shortcut that returns clean rows with one call. There is a longer how to scrape Zomato reviews writeup on our site too.
Prefer to watch? Here is a 60-second demo:
What you can pull
Per review: the rating on Zomato's 1 to 5 scale, the experience type (ratingType: dining or delivery), the full reviewText, the reviewer name and profile, the restaurant's ownerResponse, an approximate reviewDate, a short permalink, and a self-contained markdownContent block that drops straight into an LLM prompt.
Per restaurant lead: name, phones (one or more numbers), full address, locality, city, zipcode, latitude/longitude, cuisines, costForTwo, aggregate rating and total reviewCount. In Discovery mode you get one lead like this for every restaurant in a city or cuisine.
Why scraping Zomato is hard
- No public reviews API. Zomato does not offer guest reviews through a supported API, so reviews are simply not available on a documented channel.
- Browser-fingerprinted front. The site is behind an Akamai/Cloudflare layer; a plain HTTP client gets an "Access Denied", and the review text is not in the first HTML response anyway (it loads from a separate JSON route).
- Listing pagination behind a session. Discovering every restaurant in a city means driving a paginated search that needs a CSRF token and a cookie session, and it caps quietly if you page it naively.
- Two different shapes. Reviews and the per-restaurant lead record come from different places and have to be stitched together per restaurant.
- No restaurant email exists. Phone and address are the contact ceiling; anything promising Zomato emails is inventing them.
Three ways to get the data
| Approach | Setup | Handles fingerprint + pagination | Coverage | Cost model |
|---|---|---|---|---|
| DIY Python | You write and maintain it | You build and keep fixing it | Whatever you can reach | Your time + proxies |
| Zomato Restaurant Reviews Scraper (Apify) | Paste a URL/ID, or pick a city and cuisine | Yes, done for you | Full reviews per restaurant, or every restaurant in a city | Pay per result |
| Official Zomato API | Not offered for reviews | N/A | None | Not available |
Option A: DIY in Python
Start where everyone starts, a request to the restaurant page. Two problems show up at once: a plain client is blocked, and even when it is not, the reviews are not in the HTML.
import requests
# A plain client is blocked by the anti-bot front, and the review text is not in the first response.
r = requests.get("https://www.zomato.com/ncr/warehouse-cafe-connaught-place-new-delhi")
print(r.status_code, "guest review" in r.text) # -> often blocked; reviews load from a separate JSON route
To get real data you would impersonate a real browser's TLS fingerprint to get past the anti-bot layer, resolve the restaurant's internal id, page the review route until it reports no next page (handling the silent cap), and for city-wide discovery drive a paginated search that needs a CSRF token and a live cookie session. Then you stitch the reviews to the per-restaurant lead record. It is doable, but it is weeks of work and ongoing maintenance.
Option B: the no-code shortcut
The Zomato Restaurant Reviews Scraper on Apify does the fingerprinting, pagination and session handling for you. It has two modes.
Reviews mode takes restaurant URLs or a bare restaurant ID and returns every review plus a lead record:
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("factden/zomato-restaurant-reviews-scraper").call(run_input={
"mode": "reviews",
"startUrls": ["https://www.zomato.com/ncr/warehouse-cafe-connaught-place-new-delhi"],
"maxReviews": 200,
"sortBy": "newest",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
print(row["rating"], row["reviewerName"], row["reviewText"])
Discovery mode takes a city and an optional cuisine and returns one lead row per restaurant (name, phone, address, GPS, cuisines, cost for two, rating):
run = client.actor("factden/zomato-restaurant-reviews-scraper").call(run_input={
"mode": "discovery",
"city": "bangalore",
"cuisine": "north-indian",
"maxRestaurants": 200,
})
You can also sort newest, oldest, highest or lowest rating, set a from-date cutoff for cheap incremental syncs, filter a rating range, and export to JSON, CSV, Excel, or pull from the API on a schedule. Field docs and copy-paste snippets are in the GitHub repo.
What does it cost?
From about $2 per 1,000 reviews plus a small per-run start fee, and a charge per restaurant lead, pay per result. Discovery bills one lead per restaurant, never per review. New Apify accounts get free platform credit to try it end to end. No login, no Zomato API key.
Wrap-up
Zomato is a rare source that gives you both the reviews and the contact data (phone, address, GPS) for a whole city of restaurants. Rolling your own scraper means fighting a browser-fingerprinted front, session-gated pagination and two different data shapes. If you just want clean, structured rows, the Zomato Restaurant Reviews Scraper gets you there in one run, by URL or by city and cuisine. Happy scraping.
Top comments (0)