# 2GIS Scraper — Businesses and Their Reviews (`thenetaji/2gis-scraper`) Actor

Both 2GIS datasets behind one mode picker: search 206 cities for businesses by name or category, or export a business's written reviews. Search rows carry a flag saying whether 2GIS matched your query at all; review rows name which source each review came from.

- **URL**: https://apify.com/thenetaji/2gis-scraper.md
- **Developed by:** [The Netaji](https://apify.com/thenetaji) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.17 / 1,000 results

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 2GIS Scraper

Two 2GIS datasets behind one mode picker: **business search** across the 206 cities 2GIS
covers, and **written reviews** for any business in them. Pick a mode, fill in that mode's
section, and the run produces the rows for it.

They are one Actor rather than two because they cost the same thing. Both surfaces return
fifty rows per request — the largest page 2GIS allows on either — so one result price is
honest for both and neither mode subsidises the other. The business-detail export, which
costs a whole request for a single organisation, is deliberately a separate Actor for the
same reason inverted: [2GIS Business
Scraper](https://apify.com/thenetaji/2gis-place-scraper).

### Accepted input

`scraperType` chooses the mode and is required.

#### Business search — `searchPlaces`

`queries` takes one search term per line, in the city's own language — `кафе`, `pharmacy`,
`kavárna`. A term is never split on a comma, because a business name contains one: `Му-Му,
кафе` is a single term.

`cities` takes one 2GIS city slug per line — `moscow`, `spb`, `dubai`, `almaty`, `praha`. The
slug is the whole key: 2GIS is organised by city rather than by country, and its internal id
for one cannot be derived from a name. [2GIS Covered
Cities](https://apify.com/thenetaji/2gis-cities-scraper) lists all 206.

**Every term is searched in every city.** Three terms across five cities is fifteen searches,
and the run log says so before it makes any of them.

`startPage` is where each search's walk begins. Leave it at `1` to start from the top.

#### Reviews — `placeReviews`

`placeIds` takes one 2GIS branch id per line — exactly the `id` every search row above
publishes, which is how the two modes compose inside one Actor. A 2GIS place link and the
decorated id from a raw 2GIS search response both reduce to the same value.

`sort` chooses the ordering: `date_created` (newest, the default), `date_edited`, `friends`
or `trust`. That is 2GIS's own allowed set — it refuses anything else outright.

#### Both modes

`locale` is optional and takes one of the city's own locales (`ru_RU`, `en_AE`). Leave it
empty for the city's default; a locale a city does not offer is refused rather than falling
back.

`maxItems` defaults to `100` and caps each search, or each business, separately. `0` means no
limit.

```json
{
  "scraperType": "searchPlaces",
  "queries": ["кафе"],
  "cities": ["moscow", "spb"],
  "maxItems": 100
}
```

```json
{
  "scraperType": "placeReviews",
  "placeIds": ["70000001007179642"],
  "sort": "date_created",
  "maxItems": 200
}
```

### Response fields

The two modes produce different rows, and each carries the fields for what it collected. The
other mode's columns are null.

**Search rows** carry the branch `id`, `name`, `street_address` with `address_city` beside it,
`latitude` and `longitude`, `primary_rubric` and the full `rubrics` list, `schedule`,
`rating` and `review_count` with the chain's `org_rating` beside them, `is_advertisement`,
`relevance` — and `is_partial_match`.

That last one is the field to filter on first. **A query 2GIS cannot match does not come back
empty**: it answers with a healthy response and a list of unrelated real businesses in
identical row shapes, at a relevance around `1e-09` instead of the 10 to 8,800 a real match
scores. Measured over 15 queries in 5 cities the flag separated them perfectly — false on 10
of 10 real queries, true on 5 of 5 nonsense ones.

`phones`, `websites` and `emails` are empty on every search row, and that is a property of the
endpoint rather than of the businesses: 2GIS's own search request omits contact fields
entirely. [2GIS Business Scraper](https://apify.com/thenetaji/2gis-place-scraper) takes a
row's `id` for those.

**Review rows** carry `review_id`, `review_rating`, `review_text`, `date_created`,
`date_edited`, `likes_count`, `comments_count`, `photo_count`, the author's name, public id
and own review count, the business's `official_answer_text` where it has replied, and
`provider`.

`provider` is the unusual one: 2GIS aggregates its own reviews with Flamp, Booking and others
under a single listing, and they are not the same corpus. Every row names its source and
`providers` lists all of them for the branch.

Review rows also carry two counts that disagree on purpose. `branch_review_count` is what the
place card shows and `total_reviews` is how many written reviews this Actor can return —
measured 1,091 against 1,039 on one branch, the difference being star-only ratings.

Fields absent from a record are returned as null rather than omitted, so every row of a given
mode has the same shape.

### Behaviour on partial results

A search term 2GIS cannot match produces rows, flagged. They are returned rather than hidden —
a near miss is occasionally what a caller wants — but never unlabelled, and the run log warns
about it.

Search rows repeat a little across pages: 2GIS honours its own page exclusion loosely, and
over six five-page walks 0 to 12 rows of 250 repeated an organisation already returned. This
Actor drops the repeats before they reach the dataset and says how many it dropped, so a run
that fetched five pages can legitimately save fewer than 250 rows.

A search walk goes to the end of the stated total, which is unusual: pages run to exactly
`ceil(total / 50)` and the last one is short. A reviews walk goes to the end too, by offset
rather than by cursor, so nothing about it expires and an interrupted run resumes exactly
where it stopped.

A city or a business that no longer resolves is skipped with a line in the run log and the
rest of the list still runs.

### Frequently asked questions

**Why is the business-detail export not one of the modes?**
Because it costs fifty times as much per row. A search request returns fifty organisations
and a detail request returns one; a combined Actor here carries one result price for every
mode behind it, so folding that in would sell a whole request for a fiftieth of what it
costs. It ships as [2GIS Business
Scraper](https://apify.com/thenetaji/2gis-place-scraper) instead, priced on its own driver.

**Can one run do both modes?**
No — one run is one mode. Chain two runs: search first, then feed the `id` column from its
dataset into a reviews run.

**Why is there no phone number on a search row?**
Because 2GIS's own search request does not ask for one. The same organisation returns no
contact fields from search and four phone numbers, a website and two emails from the detail
endpoint. Paging further will never produce one.

**My query returned businesses that have nothing to do with it.**
Check `is_partial_match` on the rows — true means 2GIS could not match the term and returned
unrelated real businesses anyway. The fix is usually the language: search in the city's own.

**Where do I get city slugs?**
From [2GIS Covered Cities](https://apify.com/thenetaji/2gis-cities-scraper), which returns
all 206 with their region ids, countries and accepted locales in a single request.

**Is a 2GIS account or API key required?**
No. No account, session cookie or key of any kind is supplied to the Actor or needed by it.

### Related Actors

[2GIS Search Scraper](https://apify.com/thenetaji/2gis-search-scraper) and [2GIS Reviews
Scraper](https://apify.com/thenetaji/2gis-reviews-scraper) are these two modes as separate
Actors, if you would rather not pick a mode on every run.

[2GIS Business Scraper](https://apify.com/thenetaji/2gis-place-scraper) reads one organisation
in full — the phone numbers, websites and email addresses no search row carries.

[2GIS Covered Cities](https://apify.com/thenetaji/2gis-cities-scraper) is the city vocabulary
the search mode needs.

# Actor input Schema

## `scraperType` (type: `string`):

Choose the dataset for this run, then fill in the section for that mode below.

## `queries` (type: `array`):

What to search for, one per line, in the city's own language — `кафе`, `pharmacy`, `kavárna`. Every term is searched in every city listed below, so three terms across five cities is fifteen searches. A term is never split on commas: `Му-Му, кафе` is one business name.

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

The 2GIS cities to search, one slug per line. A city is a slug — the segment in 2GIS's own URL, such as `moscow`, `spb`, `dubai`, `almaty` or `praha`. It is not guessable from a country, because 2GIS is organised by city; the Covered Cities Actor publishes all 206. A pasted 2GIS link is reduced to the city in it.

## `startPage` (type: `integer`):

Which page of 50 results to begin each search at. Useful for resuming a harvest; leave it at 1 to start from the top. Pages run to exactly `ceil(total / 50)` and then stop.

## `placeIds` (type: `array`):

2GIS branch ids, one per line — exactly the `id` every search row publishes, which is how these Actors compose. A full 2GIS place link and the decorated id from a raw 2GIS search response are both accepted and reduce to the same value.

## `sort` (type: `string`):

How 2GIS should order the reviews. This is 2GIS's own allowed set, quoted from the refusal it returns for anything else — `friends` and `trust` are its social and trust-weighted orderings.

## `locale` (type: `string`):

One of the city's own locales, written as `ru_RU` or `en_AE`. Leave it empty for the city's default. This is not a translation layer: a locale a city does not offer is refused by 2GIS outright — Dubai serves `en_AE` and refuses `en_US` — and the Covered Cities Actor lists each city's set.

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

Maximum number of rows to save. Set 0 for no limit.

## Actor input object example

```json
{
  "scraperType": "searchPlaces",
  "queries": [
    "кафе"
  ],
  "cities": [
    "moscow"
  ],
  "startPage": 1,
  "placeIds": [
    "70000001007179642"
  ],
  "sort": "date_created",
  "locale": "ru_RU",
  "maxItems": 20
}
```

# Actor output Schema

## `dataset` (type: `string`):

All records scraped by this run

# 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 = {
    "scraperType": "searchPlaces",
    "queries": [
        "кафе"
    ],
    "cities": [
        "moscow"
    ],
    "startPage": 1,
    "placeIds": [
        "70000001007179642"
    ],
    "sort": "date_created",
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("thenetaji/2gis-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 = {
    "scraperType": "searchPlaces",
    "queries": ["кафе"],
    "cities": ["moscow"],
    "startPage": 1,
    "placeIds": ["70000001007179642"],
    "sort": "date_created",
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("thenetaji/2gis-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 '{
  "scraperType": "searchPlaces",
  "queries": [
    "кафе"
  ],
  "cities": [
    "moscow"
  ],
  "startPage": 1,
  "placeIds": [
    "70000001007179642"
  ],
  "sort": "date_created",
  "maxItems": 20
}' |
apify call thenetaji/2gis-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thenetaji/2gis-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/iZMnTzW6awp7nOjgM/builds/fca3qjq8O9vt7HYYh/openapi.json
