# Google Play Reviews Scraper (`scrapyx/googleplay-reviews-scraper`) Actor

Google Play reviews across as many languages as you name. Language is what widens coverage here - each one returns a different set - while country is inert and returns the same reviews twice. No upstream ceiling was found, unlike Apple.

- **URL**: https://apify.com/scrapyx/googleplay-reviews-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 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

## Google Play Reviews Scraper

Customer reviews from the **Google Play Store** — rating, text, author, date,
thumbs-up count and app version — across as many languages as you name.
HTTP-only, no API key, no login, no browser.

This is the Android half of a pair. Its sibling,
`appstore-reviews-scraper`, covers Apple — and **the two behave in opposite
ways on the axis that matters most:**

| | Apple App Store | Google Play |
| --- | --- | --- |
| Ceiling | **500** per storefront × sort (page 11 is a 400) | **none found** — 1,200 reviews over 12 pages with the cursor still live |
| What widens coverage | **country** (storefronts hold disjoint reviews) | **language** (`hl`) |
| What does nothing | — | **country** (`gl`) |

If you have used the Apple actor, the instinct it teaches — vary the country —
is the wrong one here, and it fails *silently*. See quirk 2.

### Three upstream quirks it corrects

#### 1. Every failure is an HTTP 200

This endpoint essentially never returns an error status. Measured against the
same request, same minute:

| Inner payload | Response |
| --- | --- |
| `[null,null,[2,s,[n,null,null]],[[pkg,7]]]` | **200**, 108 bytes, empty |
| `[[null,[[2,n,[...]],[pkg,7]]]]` | **200**, with `PlayDataError` inside it |
| `[null,null,[2,s,[n,null,null]],[pkg,7]]` | **200**, 35,624 bytes ✅ |

A wrong payload shape does not 400 — it answers 200 with an empty envelope, or
with an error object buried inside a successful response. An unknown package
does the same. So the status code carries almost no information here, and the
actor verifies it actually received reviews rather than trusting the transport.

#### 2. `hl` (language) decides which reviews you get. `gl` (country) does nothing

Measured on WhatsApp, 50 reviews:

```
hl=en gl=US  vs  hl=en gl=GB   ->  50 of 50 IDENTICAL   (country inert)
hl=de gl=US  vs  hl=de gl=DE   ->  50 of 50 IDENTICAL   (country inert)
hl=de gl=US  vs  hl=en gl=US   ->  ZERO overlap         (language decides)
```

So varying `country` returns you **the same reviews twice**, with no error and
nothing in the output to suggest it. `languages` is therefore the list-valued
input; `country` is a single value, recorded per row as `countryQueried` with
a standing note that it does not affect results.

#### 3. Reviews are undocumented positional arrays

Each review is a 17-element array with no keys: the id at index 0, rating at
2, text at 4, timestamp at 5, thumbs at 6, app version at 10 (frequently
null). Every index is a guess about a shape nobody publishes, so each read is
guarded — one moved field costs one column, not the run.

The response itself is a wire envelope, not JSON: a `)]}'` guard line, then a
batchexecute frame in which the payload is a JSON **string** nested inside a
JSON array. It is parsed twice, and the frame is located by its RPC id because
Google interleaves other frames alongside it.

### Output

One `SEARCH_SUMMARY` per run, one `REVIEW` per review, one `ERROR` per
language that failed.

`REVIEW`: `packageId`, `appName`, `language`, `countryQueried`, `sortUsed`,
`reviewId`, `authorName`, `authorAvatarUrl`, `rating`, `review`, `postedAt`,
`postedAtEpoch`, `thumbsUp`, `appVersion`, `pageFetched`, `resultRank`.

`SEARCH_SUMMARY` adds `appResolved`, `reviewsPerLanguage`, `emptyLanguages`,
`duplicateReviewsDropped` and a `ratingHistogram`.

### Limits

- **An unknown package returns an empty envelope under HTTP 200**, identical to
  a real app with no reviews in that language. The actor fetches the public app
  page once to tell them apart and reports `appResolved` — an empty result with
  `appResolved: false` almost certainly means the package name is wrong.
- Sorts are `mostRelevant`, `newest` and `rating`. A fourth value behaved
  identically to the first, so it is not offered.
- The `maxResults` cap is divided across the languages so one cannot consume
  it all.
- No WAF on this host; a proxy is offered but was not needed for any of the
  reconnaissance, and it does not change which reviews come back.

# Actor input Schema

## `packageId` (type: `string`):

An Android package like com.whatsapp, or a play.google.com/store/apps/details URL. A package that does not exist returns an EMPTY response under HTTP 200 — identical to a real app with no reviews — so the actor checks the app page and reports appResolved.

## `languages` (type: `array`):

THIS IS THE AXIS THAT WIDENS COVERAGE. Each language returns a completely different set of reviews (de and en shared none), while `country` returns the SAME reviews — en/US and en/GB were byte-identical. If you know the Apple sibling, note this is the reverse of it: there, country was the axis. An explicitly empty list is refused rather than silently defaulting to English.

## `country` (type: `string`):

Sent for completeness and recorded on every row, but measured INERT — it does not change which reviews come back. Use `languages` to widen coverage.

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

newest, mostRelevant, or rating (five-star first).

## `maxResults` (type: `integer`):

Set 0 for unlimited. No upstream ceiling was found — a cursor walk reached 1,200 unique reviews with the token still live — so this cap is what actually bounds the run. It is divided across the languages so one cannot consume it all.

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

Languages walked at once. Each language is an independent cursor.

## `minRequestInterval` (type: `integer`):

Politeness pacing shared across all workers. 0 uses the built-in default.

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

Apify Proxy on the shared datacenter pool. This is the default because it is included in your plan at no extra cost and this target works through it. If you start seeing blocks, challenges or empty results, switch the group here to Residential -- it uses real consumer IPs and gets through more, but Apify bills residential traffic per gigabyte, so leave it off unless you need it.

## Actor input object example

```json
{
  "packageId": "com.whatsapp",
  "languages": [
    "en",
    "de",
    "ja",
    "id"
  ],
  "country": "US",
  "sort": "newest",
  "maxResults": 500,
  "maxConcurrency": 3,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "packageId": "com.whatsapp",
    "languages": [
        "en"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/googleplay-reviews-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 = {
    "packageId": "com.whatsapp",
    "languages": ["en"],
}

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/googleplay-reviews-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 '{
  "packageId": "com.whatsapp",
  "languages": [
    "en"
  ]
}' |
apify call scrapyx/googleplay-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/googleplay-reviews-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/cjHAzNtX1YysNcqv0/builds/eOsthDTjSxC0pukOB/openapi.json
