# 2GIS Scraper - Places, Ratings & Review Count Monitor (`neverempty/2gis-rating-change-monitor`) Actor

Scrape 2GIS (2ГИС) places by city and search term or search URL: name, category, address, coordinates, rating, number of ratings and reviews, hours, business phone, website and 2GIS ID. Monitor mode returns only new places and places whose rating or review count moved, with old and new values.

- **URL**: https://apify.com/neverempty/2gis-rating-change-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Lead generation, Travel, Automation
- **Stats:** 2 total users, 1 monthly users, 90.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.70 / 1,000 place returneds

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

## 2GIS Scraper - Places, Ratings & Review Count Monitor

This 2GIS scraper (парсер 2ГИС) turns any 2GIS search into a clean table of places: name, category, address, coordinates, rating, number of ratings, number of reviews, opening hours, business phone, website and the 2GIS place ID and link. Turn on watch mode and it becomes a 2GIS rating and review count monitor: each later run returns only the places that are new in your search and the places whose rating or review count moved, with the old and the new value side by side.

Give it a city and search terms (`dubai` + `cafe`, `moscow` + `стоматология`, `almaty` + `кафе`), or paste 2GIS search URLs or place URLs. It works on 2gis.ae, 2gis.ru, 2gis.kz and the other 2GIS country sites, with no login, no API key and no 2GIS account.

### Who uses it

- **Lead lists for sales and agencies** - every cafe, clinic, salon or car service in a city with the business phone, website, Instagram and address, ready for a CRM.
- **Reputation and review monitoring** - watch your own branches, or a whole category in your district, and get told when a rating drops or when a competitor collects new reviews.
- **Local market research** - how many places a category has in Dubai or Moscow, how they are rated, where they are on the map, how many branches each chain has.
- **Franchise and chain tracking** - feed 2GIS place URLs of every branch and get one row per branch with its current rating and review count.

### What you get for each place

| Field | Example |
|---|---|
| `placeId`, `url` | `70000001006365610`, `https://2gis.ae/dubai/firm/70000001006365610` |
| `name`, `nameExtension`, `category`, `categories` | `Hard Rock`, `cafe`, `Cafe / Restaurants` |
| `address`, `addressDetails`, `building`, `district`, `city`, `region`, `country` | `Dubai Festival City Mall, 9, Crescent Drive`, `2 Floor`, `Al Kheeran`, `Dubai`, `UAE` |
| `latitude`, `longitude` | `25.22365`, `55.351308` |
| `rating`, `ratingCount`, `reviewCount` | `4.6`, `155` (the "155 ratings" on the page), `136` (written reviews) |
| `hours` | `{"mon": ["12:00-01:00"], …, "fri": ["12:00-02:00"]}` |
| `phone`, `phones`, `website`, `websites`, `socialLinks` | `+97142328900`, `http://cafe.hardrock.com`, Instagram, Facebook… |
| `branchCount`, `orgId` | `3` branches of the same business on 2GIS |
| `sponsored` | `true` when 2GIS showed the place in an advertising slot |
| `page`, `positionOnPage`, `searchQuery`, `searchCity` | where the place appeared in your search |

Phones, website and social links come from each place's own 2GIS page (the search page does not show them). They are the business's public contacts as listed on 2GIS. Turn `includeContacts` off for a faster run with everything else.

### Watch mode: rating and review count changes

Set `onlyNew` to true and schedule the run (daily or weekly):

1. **First run** - remembers every place in your searches and their rating, rating count and review count. It returns no place rows and charges nothing for them; one free row says how many places are now watched.
2. **Every later run** - returns only:
   - `changeType: "new-in-results"` - a place that appears in these results for the first time,
   - `changeType: "changed"` - a place whose `rating`, `ratingCount` or `reviewCount` changed. `changedFields` lists what moved and `previous` holds the old values, for example `{"ratingCount": 155, "rating": 4.6}` next to the new `ratingCount: 158` and `rating: 4.5`.
3. A search with nothing new and no change costs one small check fee (`search-checked`) and returns one free row that says so.

A change is confirmed on the place's own page before it is returned, so a flicker in the search results is not sold as a change. If a run cannot read a page of your search, the watch does not judge that search at all and the next run checks it again: nothing is lost and nothing is charged for it.

Use `watchName` to keep separate memories for separate schedules (for example one per client), and `resetMonitoringState` to start a watch again.

### Input

| Field | What to put in |
|---|---|
| `searchQueries` + `city` | Search terms as you would type them on 2GIS, and the 2GIS city name (`dubai`, `moscow`, `almaty`) |
| `searchUrls` | 2GIS search pages copied from the browser, with any filters you set there |
| `placeUrls` | 2GIS place pages or place IDs (up to 100) - your branches or named competitors |
| `maxResultsPerSearch` | 1 to 60 places per search (default 24) |
| `includeContacts` | Add business phone, website and social links from each place's page (default on) |
| `includeSponsored` | Keep places shown in 2GIS advertising slots (default on) |
| `onlyNew`, `watchName`, `resetMonitoringState` | Watch mode, separate watch memories, start a watch again |

### Input examples

Places in a city:

```json
{ "searchQueries": ["cafe", "coffee shop"], "city": "dubai", "maxResultsPerSearch": 60 }
```

Russian search on 2gis.ru:

```json
{ "searchQueries": ["стоматология"], "city": "novosibirsk", "maxResultsPerSearch": 36 }
```

Pasted search URLs and branches you want to watch:

```json
{
  "searchUrls": ["https://2gis.kz/almaty/search/кафе"],
  "placeUrls": ["https://2gis.ae/dubai/firm/70000001006365610"],
  "onlyNew": true,
  "watchName": "my-branches"
}
```

The `city` is the name 2GIS uses in its links: `dubai`, `sharjah`, `ajman`, `moscow`, `spb`, `novosibirsk`, `ekaterinburg`, `kazan`, `almaty`, `astana`, `tashkent`, `bishkek`, `yerevan`, `baku`… Open 2gis.ru or 2gis.ae, pick your city and copy the word after the domain. 2GIS sends each city to its own country site by itself.

### Pricing

Pay per event:

- **`place-returned`** - one returned place row (with phone and website when `includeContacts` is on).
- **`search-checked`** - watch mode only: one search (or one group of place URLs) that was compared with the last run and had nothing new and no change.

Free rows (`status` other than `ok`) explain everything that was not returned: an unknown city, no places for a search term, no changes, a place ID that does not exist, a page that could not be read, or a run that reached its maximum charge. They are never charged. The first run of a watch is not charged.

### Output in watch mode

```json
{
  "status": "ok",
  "changeType": "changed",
  "changedFields": ["ratingCount", "reviewCount"],
  "previous": { "ratingCount": 155, "reviewCount": 136 },
  "placeId": "70000001006365610",
  "name": "Hard Rock",
  "rating": 4.6,
  "ratingCount": 158,
  "reviewCount": 138,
  "phone": "+97142328900",
  "url": "https://2gis.ae/dubai/firm/70000001006365610"
}
```

### Good to know

- 2GIS shows at most 5 pages of 12 places (60 places) for one search, even when it says it has more. For a big city and a broad term, split the search: by district (`cafe Jumeirah`, `cafe Deira`), by cuisine or type (`coffee shop`, `bakery`), or paste search URLs with the 2GIS filters you set in the browser. Each search term or URL is one search, up to 10 per run.
- `ratingCount` is the number of ratings shown next to the stars on 2GIS ("155 ratings"); `reviewCount` is the number of written reviews in the 2GIS data. `rating` and `ratingCount` matched what the 2GIS results page shows on all 120 places we compared (5 pages of cafes in Dubai and in Moscow). The number on a place's Reviews tab is counted separately and can be a few higher than `reviewCount` (in our check of 6 places: equal on 3, higher by 2 to 5 on 3). A place that 2GIS shows without stars has `rating` and `ratingCount` empty.
- Review texts and the names of the people who wrote reviews are not collected - only the numbers.
- "New in results" means the place appeared in your search results for the first time, which is usually a new place but can also be a place that moved up into the first pages. Places in 2GIS advertising slots rotate on the first page, so an advertised place seen for the first time is remembered without being returned as new.
- If a place's own page cannot be read, the place is still returned from the search page with `contactsRead: false` and empty phone and website.
- When 2GIS answers with its page for unsupported browsers, the Actor asks again from another IP address and never clicks through that page. If it keeps answering that way, the run stops with a free row that says so.
- 2GIS robots.txt is read at the start of each run and every page is checked against it.
- This is an unofficial tool. It reads only the public 2GIS pages that anyone can open in a browser, and it is not affiliated with or endorsed by 2GIS (DoubleGIS).

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

## `searchQueries` (type: `array`):

What you would type into the 2GIS search box, for example cafe, dentist, кафе, стоматология. Each term is searched in the city below. Leave empty to use only search URLs or place URLs; if everything is empty, the run searches cafe in dubai as an example.

## `city` (type: `string`):

The 2GIS city name as it appears in 2GIS links (2gis.ae/dubai, 2gis.ru/moscow), for example dubai, sharjah, moscow, spb, novosibirsk, almaty, astana, tashkent. Required with search terms; 2GIS picks the country site (2gis.ae, 2gis.ru, 2gis.kz) by itself.

## `searchUrls` (type: `array`):

Optional. 2GIS search pages copied from your browser, for example https://2gis.ae/dubai/search/cafe or https://2gis.ru/moscow/search/кафе/rubricId/161. Place pages pasted here are read as places. Leave empty to use only search terms.

## `placeUrls` (type: `array`):

Optional. 2GIS place pages or place IDs, for example https://2gis.ae/dubai/firm/70000001006365610 or 70000001006365610 (up to 100). Use this to watch the rating of your own branches or of named competitors. Leave empty to use only searches.

## `maxResultsPerSearch` (type: `integer`):

How many places to return for each search term or search URL, from 1 to 60 (2GIS shows 12 places a page and 5 pages). In watch mode this is how many places of each search are watched. Empty means 24.

## `includeContacts` (type: `boolean`):

On (default): open each returned place's own 2GIS page and add the business's public phone numbers, website and social links (the search page does not show them). Off: faster, without phone and website.

## `includeSponsored` (type: `boolean`):

On (default): also return places shown in 2GIS advertising slots, marked sponsored true. Off: leave them out (they are counted in a free row).

## `onlyNew` (type: `boolean`):

Off (default): return every place. On: the first run only remembers the places and their rating, rating count and review count (no rows, no charge). Later runs return only places that appear in the results for the first time and places whose rating, rating count or review count changed, with the old values. A search with no change costs one small check fee.

## `watchName` (type: `string`):

Optional, watch mode only. Keeps separate memories for separate schedules of the same search, for example client-a. Letters, digits, dot, dash and underscore, up to 40. Empty means one shared memory.

## `resetMonitoringState` (type: `boolean`):

Watch mode only. On: forget what this watch remembered for these searches and places, so this run becomes a new first run. Off (default): keep the memory.

## Actor input object example

```json
{
  "searchQueries": [
    "cafe"
  ],
  "city": "dubai",
  "maxResultsPerSearch": 12,
  "includeContacts": true,
  "includeSponsored": true,
  "onlyNew": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per 2GIS place: 2GIS place ID and link, name, category, address, district, city, country, coordinates, rating, number of ratings, number of reviews, opening hours by day, business phone numbers, website and social links (from the place's own page), branch count and advertised flag. Watch mode marks rows new-in-results or changed, with the changed fields and the old values. A search with no places, an unknown city, no changes, a place ID that does not exist, a refused request or a run that hit its maximum charge comes back as a free row that says why.

# 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 = {
    "searchQueries": [
        "cafe"
    ],
    "city": "dubai",
    "maxResultsPerSearch": 12
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/2gis-rating-change-monitor").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 = {
    "searchQueries": ["cafe"],
    "city": "dubai",
    "maxResultsPerSearch": 12,
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/2gis-rating-change-monitor").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 '{
  "searchQueries": [
    "cafe"
  ],
  "city": "dubai",
  "maxResultsPerSearch": 12
}' |
apify call neverempty/2gis-rating-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/2gis-rating-change-monitor"
        }
    }
}
```

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/BqhAY8fcd87JU3pUc/builds/v5MXxACBf6msgZ6jd/openapi.json
