# Google Play Reviews Scraper (`renzomacar/google-play-reviews-scraper`) Actor

Scrape Google Play reviews for any Android app in any country and language. No API key, no login, no proxy. Includes developer replies and the reviewed app version.

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

## Pricing

from $0.40 / 1,000 review scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 - All Countries & Languages

Scrape Google Play Store reviews for any Android app, in any country and any language, without an API key, without a Google account, without the Play Developer API and without a proxy. Feed it a package name like `com.whatsapp` or a full `play.google.com` URL, choose your countries and languages, and get a clean table back: star score, review text, review date, helpful votes, the app version the user was running, and the developer's public reply where one exists.

This actor talks to the same public endpoint the Play Store web page uses to load its own review list. It answers `HTTP 200` to an anonymous request. No login, no cookies, no Cloudflare challenge, no residential proxy. It runs on the Apify free plan and returns data on the free plan.

### What you get per review

| Field | What it holds | Fill rate measured on 100 live reviews |
|---|---|---|
| `reviewId` | Google's own review identifier, stable across runs | 100/100 |
| `appId` | The package name the review belongs to | 100/100 |
| `appName` | Resolved app title, so the export reads without a join | 100/100 |
| `country` / `language` | The `gl` and `hl` the review was pulled under | 100/100 |
| `score` | Star rating, 1 to 5, as a number | 100/100 |
| `text` | Review body, HTML entities decoded | 100/100 |
| `at` | ISO 8601 timestamp | 100/100 |
| `thumbsUp` | How many people marked the review helpful | 100/100 |
| `userName` / `userImage` | Reviewer display name and avatar URL | 100/100 |
| `appVersion` | The build the reviewer was running | **96/100** |
| `replyFrom` / `replyText` / `replyAt` | The developer's public reply | **3/100** |
| `hasDeveloperReply` | Boolean, so you can filter without null checks | 100/100 |
| `reviewUrl` | Direct link back to the review | 100/100 |

Those are measured numbers from a live run, not documentation claims. Two of them deserve a straight answer rather than a footnote:

**`appVersion` arrives on roughly 96% of reviews.** Google omits it when the reviewer's client did not report a build. It is not a scraping failure and there is no setting that recovers it.

**Developer replies are rare - around 0% to 5% depending on the app.** Measured across three apps at 60 reviews each: WhatsApp 0 replies, Pokémon GO 1, Uber 3. If a tool implies you will get a reply column full of data, it is describing an app that answers its reviewers, not the platform norm. The field is here and it is correct when present; just do not size your project around it.

### Country and language are two separate dials, and both change the data

Google Play takes `gl` (country) and `hl` (language) independently. They are not cosmetic. Verified live on the same app: the review IDs returned for `hl=en&gl=us`, `hl=es&gl=es` and `hl=ja&gl=jp` were three different sets, not one set relabelled.

That is how you scale volume. One app across 5 countries and 3 languages is 15 separate review streams, each with its own pagination. Pagination itself was verified clean: two consecutive pages of 40 reviews returned **zero overlapping review IDs**.

### Input

```json
{
  "appIds": [
    "com.whatsapp",
    "https://play.google.com/store/apps/details?id=com.spotify.music"
  ],
  "countries": ["us", "gb", "in", "br"],
  "languages": ["en", "pt"],
  "sort": "newest",
  "maxItems": 2000,
  "minScore": 1,
  "maxScore": 2,
  "onlyWithText": true
}
```

**Input aliases.** Migrating from another scraper, or driving this from an LLM that guessed the field names? `appIds` also accepts `ids`, `appId`, `apps`, `packageNames` and `urls`. `countries` also accepts `country` and `gl`. `languages` also accepts `language`, `hl` and `lang`. `maxItems` also accepts `maxResults`, `limit` and `maxReviews`. A comma separated string such as `"us,gb,in"` works anywhere a list does.

#### Every input field

- **`appIds`** - package names or store URLs. Required.
- **`countries`** - Play country codes (`gl`). Defaults to `["us"]`.
- **`languages`** - Play language codes (`hl`). Defaults to `["en"]`.
- **`sort`** - `newest`, `rating` or `helpfulness`.
- **`maxItems`** - hard ceiling on rows delivered and therefore on the bill.
- **`minScore`** / **`maxScore`** - star filter, applied before anything is written or billed.
- **`onlyWithText`** - drop star-only ratings that carry no written feedback.
- **`onlyWithDeveloperReply`** - keep only reviews the developer answered. Narrow by design; see the fill rate above.
- **`proxyConfiguration`** - optional, off by default. Google Play does not require one here.

### Choosing a sort order, and why it matters more than it looks

- **`newest`** walks the review stream in time order. This is the one to schedule for monitoring.
- **`rating`** pulls the distribution's extremes rather than its middle. Fastest route to a complaint corpus or a testimonial corpus.
- **`helpfulness`** returns what Google itself puts at the top of the listing page. These are the reviews a prospective installer actually reads, which makes them the ones that move your conversion rate.

They return different sets, not one set reordered. Running two sort orders on the same app is a legitimate way to widen coverage.

### Scraping 1-star Google Play reviews only

Set `minScore: 1`, `maxScore: 2` and `onlyWithText: true`. The filters run **before** the row is written, so you are not billed for the four and five star reviews you discarded, and you are not billed for star-only ratings with nothing to read. What lands in the dataset is a pure complaint corpus, one row per grievance, with the app version attached so you can tell a regression from a standing gripe.

### Reading crash and regression signal by app version

`appVersion` is on ~96% of rows. Group by it and the shape of a bad release is obvious: a build where the mean score drops and the one-star share jumps, then recovers on the next build. For your own app that is a QA feedback loop. For a competitor's app it is a read on their release discipline that no public dashboard gives you.

### Developer reply mining

Filter with `onlyWithDeveloperReply: true` and you get the small set of reviews a company chose to answer publicly, plus what they said. For competitive research this is unusually high signal: support teams reveal roadmap, known issues and refund policy in review replies far more freely than in any official channel. Just plan for volume in the tens, not the thousands.

### Localisation and market research across languages

Because `hl` changes the review population rather than translating it, running `["en","es","pt","hi","id"]` against one app gives you five genuinely different user bases in one dataset, each tagged. Teams use this to decide which locale to invest in next, and to separate "our product is wrong for this market" from "our translation is wrong for this market".

### How this actor behaves when something goes wrong

1. **Errors never enter the dataset.** An app with no reviews in a given country and language combination, or a package name that does not exist, is recorded in a `FAILURES` key-value record with the exact reason per input. It is never written as a dataset row, because a row that says "error" bills you for receiving an error.
2. **A run that delivered nothing is marked FAILED.** You find out in the run status, not in an empty CSV a day later.
3. **The free tier returns data.** Nothing throws because a plan feature is missing. Proxy is optional and off by default.
4. **Google's response shape is validated before parsing.** If Play ever answers with something unexpected, that becomes a recorded failure for that input, not a crash that kills the whole run.

### Pricing

Pay per event. **$0.0004 per review delivered**, which is $0.40 per 1,000 reviews. No subscription, no charge for starting a run.

Only rows that actually reach the dataset are billed. Reviews removed by `minScore`, `maxScore`, `onlyWithText` or `onlyWithDeveloperReply` are not billed. Failed inputs are not billed. `maxItems` caps both the row count and the bill.

### Output sample

```json
{
  "reviewId": "fa31a49f-32c0-4b93-8603-5ab5240dfdad",
  "appId": "com.ubercab",
  "appName": "Uber - Request a ride",
  "country": "us",
  "language": "en",
  "sortedBy": "helpfulness",
  "userName": "elijah fields",
  "userImage": "https://play-lh.googleusercontent.com/a/ACg8ocKw...",
  "score": 1,
  "text": "Driver cancelled after 15 minutes and I still got charged...",
  "at": "2026-08-14T09:22:41.000Z",
  "thumbsUp": 214,
  "appVersion": "4.612.10001",
  "replyFrom": "Uber Technologies, Inc.",
  "replyText": "This certainly sounds like a frustrating experience...",
  "replyAt": "2026-08-15T18:03:12.000Z",
  "hasDeveloperReply": true,
  "reviewUrl": "https://play.google.com/store/apps/details?id=com.ubercab&reviewId=fa31a49f-32c0-4b93-8603-5ab5240dfdad",
  "scrapedAt": "2026-08-22T04:14:02.113Z"
}
```

### Dataset views

- **Reviews** - date, country, language, stars, review, app version, author, helpful votes.
- **Developer replies** - the review, the reply, who replied and when. Only interesting on apps that answer, which is the point of having it as a separate view.

### Frequently asked questions

**Do I need the Google Play Developer API?** No. That API only covers apps you own. This actor reads the public store and works on any app.

**Do I need a proxy or residential IPs?** No. Verified: Google Play answers this endpoint anonymously with no proxy and no Cloudflare challenge.

**How many reviews can I get for one app?** Pagination continues until Google stops issuing a next-page token. To go wider, add countries and languages - each combination is its own stream.

**Why is `appVersion` empty on some rows?** Google did not record a build for that review. Roughly 4 in 100. There is no setting that recovers it.

**Why are there so few developer replies?** Because most developers do not reply. Measured at 0 to 5 per 60 reviews across three large apps.

**Can I schedule it?** Yes. `sort: "newest"` with a modest `maxItems` is the cheap daily-monitoring configuration.

### Related actors

- **App Store Reviews Scraper** - the same job on iOS, across every Apple storefront.
- **Google Play Developer Portfolio Scraper** - every app a publisher ships, with ratings and install bands.
- **App Update Monitor** - new versions and release notes across both stores.
- **ASO Keyword Rank Tracker** - App Store search positions for a keyword and app.

# Actor input Schema

## `appIds` (type: `array`):

Package names such as com.whatsapp, or full play.google.com URLs. Aliases accepted in JSON input: ids, appId, apps, packageNames.

## `countries` (type: `array`):

Google Play country codes (the gl parameter). Country changes which reviews you get, not just the currency. Aliases: country, gl.

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

Language codes (the hl parameter). en returns English reviews, es returns Spanish ones, and the two sets barely overlap. Aliases: language, hl, lang.

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

newest walks the review stream in time order. rating pulls the extremes. helpfulness returns what Google shows first on the listing.

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

Hard ceiling on reviews delivered and therefore on what you are charged.

## `minScore` (type: `integer`):

Keep only reviews at or above this score.

## `maxScore` (type: `integer`):

Keep only reviews at or below this score. Set to 2 to mine complaints only.

## `onlyWithText` (type: `boolean`):

Drop star-only ratings that carry no written feedback.

## `onlyWithDeveloperReply` (type: `boolean`):

Measured fill rate for developer replies is roughly 0 to 5 percent, so this filter is narrow by design.

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

Not required. Google Play answers this actor without a proxy on the free plan.

## Actor input object example

```json
{
  "appIds": [
    "com.whatsapp",
    "https://play.google.com/store/apps/details?id=com.spotify.music"
  ],
  "countries": [
    "us"
  ],
  "languages": [
    "en"
  ],
  "sort": "newest",
  "maxItems": 200,
  "onlyWithText": false,
  "onlyWithDeveloperReply": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One row per Google Play review: rating, text, app version, thumbs up and any developer reply.

# 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 = {
    "appIds": [
        "com.whatsapp",
        "https://play.google.com/store/apps/details?id=com.spotify.music"
    ],
    "countries": [
        "us"
    ],
    "languages": [
        "en"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("renzomacar/google-play-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 = {
    "appIds": [
        "com.whatsapp",
        "https://play.google.com/store/apps/details?id=com.spotify.music",
    ],
    "countries": ["us"],
    "languages": ["en"],
}

# Run the Actor and wait for it to finish
run = client.actor("renzomacar/google-play-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 '{
  "appIds": [
    "com.whatsapp",
    "https://play.google.com/store/apps/details?id=com.spotify.music"
  ],
  "countries": [
    "us"
  ],
  "languages": [
    "en"
  ]
}' |
apify call renzomacar/google-play-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,renzomacar/google-play-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/G0dqUqbWZtd0fCgDL/builds/110wyEaxEBrRH6dnM/openapi.json
