kestrel actors › Apps
Google Play Reviews Scraper — Android App Reviews & Ratings
A Google Play reviews scraper that returns every user review of any Android app as structured rows: the 1–5 star rating, review text, reviewer name, thumbs-up count, app version, date and the developer's public reply — google play reviews for every language and country you name, in a single run, each row tagged with its language and country. It makes the same review call the Play Store's own web page makes, follows the continuation token to the end of the list, and works for android app reviews in the hundreds of thousands per app.
Run Google Play Reviews Scraper on Apify →
Google play reviews scraper pricing: what one row costs
| Event | What it is | Price | Charged for |
|---|---|---|---|
review | Review row | $0.004 | One Google Play review: 1-5 rating, review text, reviewer, thumbs-up count, app version, date and the developer's reply. App rows, status rows, filtered reviews and apps with no reviews are free. |
You pay per delivered review row — $0.004 a review — and nothing else: app rows, status rows, filtered reviews, apps with no reviews, unknown packages and failed jobs are free. A run that reads 300 reviews per locale for two locales and keeps the 1–2★ ones with text typically delivers 60–120 rows, so it costs 25–50 cents. The full corpus of a big app is priced by the row too: 100,000 reviews are $400, and maxReviewsPerApp or a spending limit on the run caps it wherever you like.
Compute is tiny: one review call returns up to 100 reviews in about a second, so a thousand-review job is ten calls.
Google play reviews scraper input reference
Every field of Google Play Reviews, straight from the actor's input schema.
| Field | Type | Default | What it does |
|---|---|---|---|
appIds | array | [] | Android package names, e.g. com.whatsapp — the id= part of any Google Play URL, and what an earlier run's app_id gives you. |
appUrls | array | [] | App pages, e.g. https://play.google.com/store/apps/details?id=com.whatsapp — the package name is read from the URL for you. |
languages | array | ["en"] | Play language codes (hl), one per line: en, de, fr, es, pt-BR, ja, zh-TW… Google Play lists reviews per language, so a global app needs several. Every review row carries its language. |
countries | array | ["us"] | Two-letter Play country codes (gl), one per line: us, gb, de, in, br… Every review row carries its country; a review Google lists under two locales is delivered and billed once. |
maxReviewsPerApp | integer | 100 | 0 = every review Google pages out (big apps run into the hundreds of thousands). N = the first N in the order below. The main cost control. Range 0-100000. |
sort | string (enum) | "most_recent" | Most recent is the one to schedule; most relevant is Google Play's own default order; rating puts the 5-star reviews first. One of: most_recent, most_relevant, rating. |
minRating | integer | 0 | 0 = keep every review. 1-5 = keep only reviews at or above this many stars. Filtered reviews are never charged; when minRating and maxRating name the same star value Google filters server-side and the others are never even fetched. Range 0-5. |
maxRating | integer | 0 | 0 = keep every review. 1-5 = keep only reviews at or below this many stars — set 2 for a complaints feed that pays nothing for the happy ones. Range 0-5. |
sinceDate | string | "" | Leave empty for all. YYYY-MM-DD, or relative so a schedule never goes stale: "7 days", "2 weeks", "3 months". Combine with "Most recent" for a daily new-reviews feed. |
requireText | boolean | false | Drop reviews that are a star rating with no words, so you deliver and pay for only the ones that say something. |
includeAppRow | boolean | true | Also emit one free row per app and locale: name, developer, category, content rating, average rating, rating count, price and icon. |
sessions | integer | 4 | How many proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately. Range 1-16. |
perIp | number | 1 | Pace for each session. Google answers about 1/s per IP; faster earns refusals and retries, not speed. Range 0.1-3. |
proxyConfiguration | object | {"useApifyProxy": true} | Google Play's review call is public and needs no login, but it throttles each IP at about one request a second. Apify Proxy (the default datacentre group) spreads a large run over several IPs; small runs work without it. |
Example input
{ "appIds": ["com.whatsapp"], "languages": ["en", "es", "pt-BR", "hi"], "countries": ["us", "br", "in"], "maxReviewsPerApp": 500 }
{ "appIds": ["com.spotify.music"], "languages": ["en"], "countries": ["us", "gb"], "sort": "most_recent", "maxRating": 2, "requireText": true, "maxReviewsPerApp": 300 }
Google play reviews scraper output fields
Three row types share one dataset: review (one user review, charged), app (context for an app in a language and country, free) and status (per app, language and country, free).
Dataset views in the Apify Console: Overview, Reviews, Complaints, Apps, Status.
| Field | Type | What it holds |
|---|---|---|
type | string | Row type: review, app or status. |
app_id | string | Android package name, e.g. com.whatsapp. |
app_name | string | App name in that language. |
language | string | Play language the row was read in (hl), e.g. en, pt-BR. |
country | string | Play country the row was read in (gl), two-letter code. |
review_id | string | Google's id for the review. |
rating | integer | Stars the reviewer gave, 1-5. |
text | string | Review text; null for a star-only review. |
author | string | Reviewer's display name. |
author_id | string | Google's id for the reviewer. |
thumbs_up | integer | How many users marked the review helpful. |
app_version | string | App version the review was written against. |
review_date | string | When the review was posted, ISO 8601 UTC. |
review_day | string | Review date as YYYY-MM-DD. |
reply_text | string | The developer's public reply, when there is one. |
reply_date | string | When the developer replied, ISO 8601 UTC. |
url | string | Google Play page deep-linked to the review (reviewId). |
developer | string | Developer name (app rows). |
developer_url | string | Developer website (app rows). |
category | string | Play category, e.g. COMMUNICATION (app rows). |
content_rating | string | Content rating, e.g. Everyone (app rows). |
rating_avg | number | Average rating on Google Play (app rows). |
rating_count | integer | How many ratings the app has (app rows). |
price | number | List price, 0 for free apps (app rows). |
currency | string | Currency of the price (app rows). |
icon | string | App icon URL (app rows). |
description | string | Short description (app rows). |
app_url | string | Google Play page URL (app rows). |
reviews_fetched | integer | Reviews read for this app and locale before filtering (app rows). |
status | string | ok, no_reviews, not_found or error (status rows). |
target | string | The package name you gave (status rows). |
reviews | integer | Review rows delivered for this job (status rows). |
filtered | integer | Reviews dropped by minRating, maxRating, sinceDate or requireText — never billed (status rows). |
pages | integer | Review calls made for this job (status rows). |
duplicates | integer | Reviews already delivered under another locale — never billed twice (status rows). |
error | string | Why the job failed, when it did (status rows). |
fetched_at | string | When the row was read, ISO 8601 UTC. |
Example row from a real run
Taken from the actor's README — this is the shape your dataset holds.
{
"type": "review",
"review_id": "22aacbd1-267e-445b-948c-629ad3d87a2a",
"app_id": "com.whatsapp",
"app_name": "WhatsApp Messenger",
"language": "en",
"country": "us",
"rating": 5,
"text": "so strict",
"author": "Emmanuel Shimison",
"author_id": "105709602236647094141",
"thumbs_up": 0,
"app_version": "2.26.21.75",
"review_date": "2026-08-28T06:25:25Z",
"review_day": "2026-08-28",
"reply_text": null,
"reply_date": null,
"url": "https://play.google.com/store/apps/details?id=com.whatsapp&hl=en&gl=us&reviewId=22aacbd1-267e-445b-948c-629ad3d87a2a",
"fetched_at": "2026-08-29T10:25:00Z"
}
Google play reviews scraper in Python, JavaScript and curl
Python
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/google-play-reviews-scraper").call(run_input={
"appIds": ["com.whatsapp"],
"languages": ["en", "de"],
"countries": ["us", "de"],
"sort": "most_recent",
"maxRating": 2,
"requireText": True,
"maxReviewsPerApp": 200,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["type"] == "review":
print(row["language"], row["country"], row["rating"], row["review_day"], (row["text"] or "")[:80])
JavaScript (Node)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('kestrel/google-play-reviews-scraper').call({
appIds: ['com.spotify.music'], languages: ['en'], countries: ['us', 'gb'],
sort: 'most_recent', sinceDate: '7 days', maxReviewsPerApp: 500,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const reviews = items.filter(r => r.type === 'review');
console.log(reviews.length, 'new reviews this week;', reviews.filter(r => r.reply_text).length, 'answered');
curl
curl -X POST "https://api.apify.com/v2/acts/kestrel~google-play-reviews-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"appIds": ["com.whatsapp"], "languages": ["en"], "countries": ["us"], "maxReviewsPerApp": 50}'
The same three calls are runnable files in the examples repository: examples/google-play-reviews-scraper.py|.js|.sh.
Limits and honest notes
- Locale is a list, not a filter. Google decides which reviews it shows under a given
hl/gl; a review can appear under several locales (delivered once) and a few may appear under none. For a complete corpus, name every language the app has meaningful traffic in. most_relevantis Google's ranking, not a date order. It surfaces the reviews Play itself promotes — good for sentiment, wrong for "what changed since yesterday". Schedulemost_recent.- Text can be empty. Many Android reviews are a star rating with no words;
requireTextdrops them before billing. - The reviewer's name is what they chose to display, and
author_idis Google's opaque id for that account — useful for de-duplicating across apps, not for identifying people. - Throttling. Google answers refusals to bursts from one IP. The fetcher paces each session at
perIprequests a second, treats an answer without a review payload as a refusal — never as "no reviews" — rotates the session and backs off. Large runs finish; they do not fail silently. - Not supported: app names as input (Play has no open search API; use the package name from the URL), review images, and the per-version breakdown of ratings.
FAQ
Does it need a Google account or API key?
No. Google Play has no public google play review api for reading reviews; this actor uses the public review call that the Play Store web page itself makes, without a login. You only need an Apify account.
Can I download Google Play reviews as CSV or Excel?
Yes. Every run writes an Apify dataset; open the Reviews view and export it as CSV, Excel, JSON or XML, or fetch it from the API. Google play reviews csv exports are the most common use — the columns are flat, so nothing needs cleaning.
How do I get only Google Play negative reviews?
Set maxRating to 2 (or minRating and maxRating both to 1 for one-star only, which Google filters server-side), add requireText: true so star-only reviews are dropped, and sort by most_recent. You pay only for the rows that survive — google play negative reviews for a fraction of the price of reading everything.
Can I scrape reviews in many languages and countries at once?
Yes — languages and countries are lists, every combination is one job, and each row carries language and country. That is the difference between an English-only export and google play reviews by language that reflect the whole user base.
How many reviews can one app return?
Everything Google pages out. There is no fixed cap: the actor follows the continuation token until the list ends, which for the biggest apps is hundreds of thousands of reviews per locale. maxReviewsPerApp and a spending limit on the run are the two ways to stop earlier.
Does it include the developer's reply?
Yes. reply_text and reply_date carry the google play developer reply when there is one — the basis of a response-rate report or a "which complaints went unanswered" view.
Can I use it for competitor app reviews?
Yes. Give several package names, one language and country set, and sort: "most_relevant"; you get competitor app reviews side by side with the same columns, ready for app review analysis — theme counting, sentiment, feature requests — in the tool of your choice.
Can I scrape Google Play reviews without an API?
That is exactly what this does: scrape google play reviews through the store's own web call, no SDK, no quota to apply for, no browser to run.
What does bulk work cost?
$0.004 a review row, nothing for anything else. 10,000 reviews are $40; a scheduled feed that delivers 20 new reviews a day is $2.40 a month.
Which order should I schedule?
most_recent with a relative sinceDate. most_relevant for a one-off portrait of what users think; rating when you want the praise first.
How to use Google Play Reviews Scraper
Related scrapers
Run Google Play Reviews Scraper
The actor lives on the Apify Store, where the pricing tab carries the live per-row price and the input form has every field above: apify.com/kestrel/google-play-reviews-scraper