Google Play Store Review Scraper By Country & Language
Pricing
from $4.99 / 1,000 results
Google Play Store Review Scraper By Country & Language
Google Play Store Review Scraper extracts public app reviews, ratings, review text, reviewer details, dates, and app metadata. Filter reviews by country and language to analyze user feedback, app performance, sentiment, and regional trends.
Pricing
from $4.99 / 1,000 results
Rating
0.0
(0)
Developer
API Empire
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
0
Monthly active users
a day ago
Last modified
Categories
Share
Google Play Review Scraper — Reviews, Market Stats, Coverage
Extract Google Play Store reviews from up to 54 country and language storefronts in a single run. Every review row is stamped with its real storefrontKey, storefrontCountry and storefrontLanguage, de-duplicated across markets, and returned as typed, normalized JSON — no HTML, no selectors, no parsing. Alongside the reviews you get type="market" rollup rows (star spread, date span, top keywords) and one type="coverage" row per app. By the end of this page you will know exactly which storefront codes are accepted, what every output key contains, and which rows you are billed for.
What is the Google Play Store Review Scraper By Country & Language?
The Google Play Store Review Scraper By Country & Language is an Apify Actor that reads public Google Play review data through Play's own batchexecute RPC surface (rpcids=oCPfdb) and returns it as structured JSON. Google Play does not serve one global review pool — it serves a different pool per hl (language) and re-selects it per gl (country). This Actor runs one full bootstrap and pagination pass per storefront you select, so en-US, de-DE and ja-JP come back as three genuinely separate review sets for the same package, each labelled with the market it came from.
No Google account, no Play Console access, no OAuth token and no cookie are required. The Actor fetches the public app page, harvests the live session id (FdrFJe) and build tag (cfb2h) from window.WIZ_global_data, and then paginates the review RPC directly. Everything it returns is data any logged-out visitor can see on the store page.
Key capabilities
- Scrape reviews —
reviewId,rating,reviewer,body,appVersion,helpfulCounts,reviewedAtand the full storefront stamp, one row per review. - Scrape per-market rollups — one
type="market"row per storefront withsampleSize,avgRatingInSample,oneStarCount…fiveStarCount,oneStarSharePct…fiveStarSharePct,oldestReviewDate/newestReviewDateandtopKeywordsInSample. - Scrape coverage accounting — one
type="coverage"row per app withmarketsRequested,marketsWithData,marketsEmpty,totalReviewsSaved,crossMarketDuplicatesDroppedandfilteredOut. - Export as JSON, CSV, Excel, XML or RSS from the Apify dataset, or read it straight from the Apify API.
- No proxy management and no parsing — session bootstrap, cursor pagination, soft-block detection and proxy escalation are all handled internally.
🌍 What data does this Google Play review scraper collect?
The Actor writes four structurally different row types into a single dataset, distinguished by the type field: review, market, coverage and error. Every row also carries scrapedAt.
| Data Type | Key Fields | JSON Field Names |
|---|---|---|
Reviews (type="review") | Review id, star rating, reviewer name and avatar, review body, post date, helpful votes, reported app version, deep link, and the market it came from | reviewId, rating, reviewer, userImage, body, date, reviewedAt, timestamp, helpfulCounts, appVersion, reviewUrl, appId, position, language, reviewedIn, storefrontKey, storefrontCountry, storefrontCountryName, storefrontLanguage, storefrontLanguageName |
Market rollups (type="market") | Sample size, cap flag, average rating, 1★–5★ counts and shares, market-unique count, top app version, date span, local-language keywords | sampleSize, isCappedSample, avgRatingInSample, oneStarCount, twoStarCount, threeStarCount, fourStarCount, fiveStarCount, oneStarSharePct, twoStarSharePct, threeStarSharePct, fourStarSharePct, fiveStarSharePct, uniqueToThisMarket, topAppVersionInSample, oldestReviewDate, newestReviewDate, topKeywordsInSample |
App coverage (type="coverage") | Markets requested vs returning data, empty markets, rows saved, duplicates dropped, rows filtered out | marketsRequested, marketsWithData, marketsEmpty, storefrontsCovered, totalReviewsSaved, crossMarketDuplicatesDropped, filteredOut, dedupeAcrossMarkets |
Diagnostics (type="error") | Machine-readable failure class, human-readable detail, and the app + storefront that failed | errorReason, message, appId, storefrontKey, storefrontCountry, storefrontLanguage |
The default dataset view (.actor/actor.json → storages.dataset.views.default) exposes exactly the union of these four shapes — 50 columns — with human-readable labels, so a CSV export contains every key the Actor can write.
Need more Google Play or app-store data?
If you are benchmarking the same product on both stores, pair this Actor with the API Empire Apple App Store Review Scraper With Version Quality Trends — it returns the iOS side of the same feedback loop, so one pipeline can compare Android and iOS sentiment per release. For discovery work upstream of reviews, the Google Search Autocomplete Scraper By Buyer Intent Filter surfaces the queries people actually type before they reach your listing, and the Google News Scraper (Real URL & Source Metadata) picks up the press coverage that tends to move a rating before the reviews do.
How does this differ from the official Google Play Developer API?
Google's own review endpoint, androidpublisher.reviews.list in the Play Developer API, is a publisher tool: per Google's Play Developer API reference it returns reviews only for apps in your own developer account, only reviews that include a written comment, and only reviews submitted within the last week. This Actor is a public web data tool: it reads the same review stream any visitor sees on the store page, for any package, in any storefront you name.
| Feature | Google Play Developer API (reviews.list) | This Actor |
|---|---|---|
| Apps you can query | Only apps in your own Play Console developer account, per Google's reference | Any public Google Play package, by store URL or package name |
| Authentication | OAuth 2.0 / service account tied to a Play Console account | None — no Google account, no token, no cookie; the Actor sends no auth header |
| Recency window | Documented as reviews from the last week only | Whatever Play's public review RPC will paginate, bounded by your maxReviews / pagesToScrape settings |
| Reviews without text | Excluded — only reviews with comments are returned | Whatever the public RPC returns; rows with an empty body still carry rating and appVersion |
| Country / language segmentation | Locale is a property of a returned review, not a query dimension | First-class: one full pass per hl-gl storefront, with storefrontKey, storefrontCountry and storefrontLanguage on every row |
| Aggregate statistics | Not provided by reviews.list | type="market" rollup rows with star counts, star shares, date span and top keywords, computed locally from the scraped sample |
| Developer replies | Returned as part of the review resource | Not extracted by this Actor — see the FAQ |
Use the Play Developer API when you own the app, need developer replies, and only care about the last week. Use this Actor when you need competitor apps, deeper history, or the country-by-country breakdown that the publisher API does not expose as a query dimension at all.
Why do developers and teams scrape Google Play reviews?
For AI engineers and agent builders
Review text is one of the few large, timestamped, natively multilingual corpora that maps cleanly onto a single product. A typical RAG build indexes body as the chunk and uses storefrontLanguage, storefrontCountry, appVersion, rating and date as metadata filters, so a retrieval query can be scoped to "1★ German reviews on version 9.1.64.1676". Every row is typed JSON with stable key names, so there is no HTML-stripping step before embedding. An agent tool can call the Actor with sortBy: "newest" and recentDays: 1 and hand the rows straight into a context window.
For product and release managers
appVersion plus the type="market" rollups make regression hunting concrete. Run one app across en-US, de-DE, pt-BR and ja-JP with deviceType: "tablet", and the market rows show which storefront's avgRatingInSample and oneStarSharePct moved after a release, while topAppVersionInSample tells you which build that market is actually running. The input schema notes appVersion is populated on roughly 89–100% of rows depending on sort order, so treat a null appVersion as unattributed rather than as the current build.
For localization and market-research teams
This is the workload the Actor was built for. Fan out across es-ES, es-MX, es-AR, es-CO and es-CL and you get five separate Spanish-language pools rather than one blended sample. uniqueToThisMarket tells you how much of each pool no other storefront returned, and topKeywordsInSample gives a stop-word-filtered term list computed in the review's own language — the Actor ships a multilingual stop-word set covering English, Spanish, Portuguese, German, French and Italian. All of it is public store data; no reviewer contact details are collected.
For developers building data products
Every run is a fresh live fetch, so the Actor slots into a scheduled pipeline without a cache-invalidation story. The type="coverage" row exists for exactly this: marketsRequested vs marketsWithData, marketsEmpty, crossMarketDuplicatesDropped and filteredOut let a downstream job assert on run quality before loading anything into a warehouse. Filter on type = "review" for the payload and type = "coverage" for the run manifest.
🗺️ Country and language coverage — the exact storefront codes
The storefronts parameter is the differentiator, so here is the complete accepted list. The dropdown in .actor/actor.json offers 54 language–country pairs across 32 languages and 52 countries:
| Language | Storefront codes offered |
|---|---|
English (en) | en-US, en-GB, en-CA, en-AU, en-IN, en-IE, en-NZ, en-ZA, en-NG, en-PH, en-SG |
Spanish (es) | es-ES, es-MX, es-AR, es-CO, es-CL |
German (de) | de-DE, de-AT, de-CH |
French (fr) | fr-FR, fr-CA, fr-BE |
Arabic (ar) | ar-SA, ar-AE, ar-EG |
Portuguese (pt) | pt-PT, pt-BR |
Chinese (zh) | zh-TW, zh-HK |
| One storefront each | nl-NL Dutch, it-IT Italian, pl-PL Polish, ru-RU Russian, uk-UA Ukrainian, tr-TR Turkish, sv-SE Swedish, no-NO Norwegian, da-DK Danish, fi-FI Finnish, cs-CZ Czech, el-GR Greek, ro-RO Romanian, hu-HU Hungarian, he-IL Hebrew, ja-JP Japanese, ko-KR Korean, th-TH Thai, vi-VN Vietnamese, id-ID Indonesian, ms-MY Malay, hi-IN Hindi, bn-BD Bengali, ur-PK Urdu, sw-KE Swahili |
Beyond the dropdown, the Actor's internal name tables also recognise the languages sk, bg, hr, sr, sl, et, lv, lt, fa, ta, te and fil, so a custom storefront built on any of those still gets a readable storefrontLanguageName.
How a storefront code is parsed — and what happens to an unsupported one
There is no allow-list validation in the code. resolve_storefronts() normalizes whatever string it is handed and sends it straight to Google:
_is rewritten to-, then the value is split on-.- The first segment is lower-cased and used as
hl(language). - The last segment is upper-cased and used as
gl(country) — but only if the value has at least two segments. - Duplicate normalized keys are dropped, preserving the order you selected them in.
That produces three behaviours you need to know about:
- A code outside the dropdown is passed through, not rejected. Submit
sk-SKorfa-IRthrough the API and the Actor requestshl=sk&gl=SKfrom Play and returns whatever comes back — no hard fail, no substitution of the language or country. Only the display names degrade:storefrontLanguageNameandstorefrontCountryNameecho the raw code when it is not in the internal name tables, whilestorefrontLanguageandstorefrontCountrystay correct. - A single-segment code silently becomes a US storefront. Submitting
"de"with no country does not error — it is normalized tode-USand the rows are stampedstorefrontCountry: "US",storefrontCountryName: "United States". That is a silent substitution, so always pass the fullhl-glpair. - An empty or missing
storefrontslist silently becomes["en-US"]. This is the documented default and reproduces single-storefront behaviour.
If a market genuinely returns nothing — the app is not distributed there, or the bootstrap failed — you do not get a fabricated empty review row. The storefront key appears in marketsEmpty on the type="coverage" row, and a bootstrap or HTTP failure additionally emits an uncharged type="error" row naming that storefront.
The language input is a separate, second filter
storefronts decides which review pools are fetched; language is an optional post-fetch filter on each row's language field (set from the storefront's hl). The language dropdown offers 37 entries in code - Name form, eight of which — sk, bg, hr, sr, sl, et, lv, lt — have no matching entry in the storefronts dropdown. Selecting one of those without also supplying a matching custom storefront code filters every row out and produces an empty run.
How to scrape Google Play reviews by country (step by step)
This Actor runs on the Apify platform. There is no separate signup, no API key of its own and no credential parameter anywhere in the input schema — you start it from the Apify Console or by calling the Apify API with your own Apify token.
- Open the Actor on its Apify Store listing and click Try for free, or add it to your account so it appears under Actors in the Apify Console.
- Fill in
startUrls— the only required parameter. Paste Google Play URLs (https://play.google.com/store/apps/details?id=com.spotify.music) or bare package names (com.spotify.music). Both forms work, mixed freely in the same list, and every app you list is scraped across every storefront you select. - Pick your storefronts and filters — choose entries in
storefronts, then optionally setsortBy,ratingFilter,deviceType,keywords,appVersion,recentDaysandendDate. SetmaxReviews(total budget per app) andmaxReviewsPerMarket(per-storefront cap) so no single market consumes the whole run. - Start the run. The log prints a market coverage plan up front — app count × storefront count, the storefront list, the resolved sort, device, page size, whether a server-side star filter was applied and the dedupe setting — then one line per page per storefront showing new vs duplicate rows.
- Download the results from the dataset tab as JSON, CSV, Excel, XML or RSS, or fetch them through the Apify API. Filter on
typeto separate reviews from rollups, coverage and diagnostics.
What to do when Google Play changes its structure
Nothing on your side. The Actor is maintained, and the output schema is the contract: field names and types stay stable across engine updates, so a pipeline keyed on reviewId, rating, body and storefrontKey keeps working. The engine itself is written to absorb Play's churn — the next section explains how.
What changed in Google Play review scraping recently?
The most disruptive recent change is that Play's review RPC moved slot inside the app page's inlined request table. The engineering notes in this Actor's source record that the oCPfdb request template, long addressed by the hardcoded index ds:11, has since drifted to ds:10, with ds:11 now holding an unrelated jRco8 entry carrying request:[]. Any scraper still anchored on that index POSTs an empty template and returns zero reviews on every run — silently, behind a 200 response.
- What changed technically. The review pool is served by
POST /_/PlayStoreUi/data/batchexecute?rpcids=oCPfdb, and the request template is inlined into the app page HTML rather than being a stable documented shape. Its position in that table is not fixed. - What this means for DIY scrapers. Index-based extraction breaks without raising an error. So does a hardcoded build tag: the
blrequest parameter is a dated Play UI server build string — the fallback constant in this Actor isboq_playuiserver_20251112.07_p0, a 2025-11-12 build — and it rotates with every Play deploy. - What this Actor does instead. It anchors on the literal
id:'oCPfdb'marker and walks a balanced-bracket scan to the request array, never on a slot index, and it harvests the livecfb2hbuild tag andFdrFJesession id fromwindow.WIZ_global_dataon every bootstrap. The hardcoded build tag is only a last-resort fallback. - Anti-bot posture. Google answers suspicious traffic with an interstitial rather than a clean status code, so the Actor's block detector is body-aware:
401,403,429and503count as blocks, and so does an empty body, a page containing "our systems have detected unusual traffic", or a/sorry/indexpage that lackswiz_global_data. - What remains publicly accessible. Everything this Actor returns — review text, star rating, reviewer display name and avatar, helpful votes, reported app version, and the storefront segmentation — is visible to a logged-out visitor on the store page. No login-gated surface is touched.
Maintenance is ongoing, and the engine is written so the common failure modes are recoverable without a schema change on your side.
⬇️ Input
Every input below comes directly from .actor/actor.json. Only startUrls is required; every other parameter has a schema default, and the Apify platform applies those defaults when you omit a key.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
startUrls | Yes | array of string, editor stringList | Google Play apps to cover. Full store URLs or bare package names, mixed freely. Every app is scraped across every selected storefront. Prefilled with com.spotify.music. Objects of the form {"url": "…"} are also accepted by the normalizer. | ["com.spotify.music", "https://play.google.com/store/apps/details?id=com.whatsapp"] |
storefronts | No | array of string, editor select, default ["en-US"] | Language–country pairs to fan out across; 54 options in the dropdown. Each entry gets its own bootstrap + pagination pass. Empty ⇒ a single en-US pass. | ["en-US", "de-DE", "ja-JP", "pt-BR"] |
maxReviewsPerMarket | No | integer, default 0, minimum 0 | Per-storefront review cap, so no single market eats the whole run. 0 means no per-market cap — the overall maxReviews budget decides. | 100 |
dedupeAcrossMarkets | No | boolean, default true | Save each reviewId only once, from the first storefront that returned it. The input schema records a measured overlap of 37 of 100 rows between en-US and en-GB. | true |
emitMarketRollups | No | boolean, default true | Append one type="market" row per storefront that returned rows, plus one type="coverage" row per app. Set false for a reviews-only dataset. | true |
minReviewsForRollupStats | No | integer, default 5, minimum 1 | Minimum sample size before avgRatingInSample and the …SharePct fields are computed. Below it they are null; the raw …Count fields are always emitted. | 20 |
sortBy | No | string, default "mostRelevant", enum mostRelevant | newest | rating | Review ordering, pushed into the Play request itself. mostRelevant = Play's default ranking, newest = strictly chronological, rating = highest rating first. | "newest" |
ratingFilter | No | array of string, editor select, item enum "1"–"5", no default | Star ratings to keep. Selecting exactly one star pushes the filter server-side into the Play request; selecting several filters locally after fetching. | ["1", "2"] |
rating | No | integer, default null, minimum 1, maximum 5, nullable | Legacy single-star alternative to ratingFilter. Ignored whenever ratingFilter is a non-empty list. | 1 |
deviceType | No | string, default "mobile", enum mobile | tablet | chromebook | Device segment, pushed into the Play request. Play keeps a separate review bucket per form factor, each with its own app-version mix. | "tablet" |
language | No | array of string, editor select, 37 options in code - Name form, no default | Extra post-fetch filter on the row's language field. Only the part before - is used, matched case-insensitively. Leave empty to keep every language your storefronts returned. | ["de - German", "fr - French"] |
keywords | No | array of string, editor stringList, no default | Keep only reviews whose body contains at least one of these substrings, case-insensitive. Review text is in the storefront's own language — add local wording too. | ["crash", "absturz", "batería"] |
appVersion | No | array of string, editor stringList, no default | Keep only reviews whose reported appVersion matches one of these exactly, compared lower-cased and trimmed. | ["9.1.64.1676"] |
recentDays | No | integer, default 0, minimum 0 | Keep only reviews posted within the last N days. 0 = no lower date bound. Pair with sortBy: "newest" for monitoring runs. | 7 |
endDate | No | string, default "", editor datepicker, dateType: absoluteOrRelative | Keep only reviews posted on or before this date. Supply an absolute YYYY-MM-DD value — the filter takes the first 10 characters and compares date strings directly. Empty = no upper bound. | "2026-07-01" |
maxReviews | No | integer, default 100, minimum -1, maximum 1000000 | Total review budget per app across all storefronts. Storefronts are consumed in selection order. -1 = no limit. | 2000 |
reviewsPerPage | No | integer, default 200, minimum 1, maximum 2000 | Rows requested per batchexecute call. The schema notes 1000 has been verified to return in a single request; larger values mean fewer requests per storefront. | 500 |
uniqueOnly | No | boolean, default true | Remove repeated reviewId values within a storefront's own pagination as well as across storefronts. | true |
startPage | No | integer, default 1, minimum 1 | Start saving from this page of each storefront. Earlier pages are still fetched to reach the cursor; they are simply not saved. | 1 |
pagesToScrape | No | integer, default -1, minimum -1 | Hard page cap per storefront, counted from startPage. -1 = unlimited; the run then stops at the review caps instead. | 10 |
proxyConfiguration | No | object, editor proxy, prefilled {"useApifyProxy": false} | Optional Apify Proxy settings. Independently of this setting, the Actor escalates to a residential IP once if Google soft-blocks a request. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
Example JSON input
A four-market localization sweep across two apps — newest first, negative reviews only, last 30 days:
{"startUrls": ["com.spotify.music","https://play.google.com/store/apps/details?id=com.duolingo"],"storefronts": ["en-US", "de-DE", "pt-BR", "ja-JP"],"maxReviewsPerMarket": 150,"dedupeAcrossMarkets": true,"emitMarketRollups": true,"minReviewsForRollupStats": 20,"sortBy": "newest","ratingFilter": ["1", "2"],"rating": null,"deviceType": "mobile","language": [],"keywords": [],"appVersion": [],"recentDays": 30,"endDate": "","maxReviews": 600,"reviewsPerPage": 200,"uniqueOnly": true,"startPage": 1,"pagesToScrape": -1,"proxyConfiguration": { "useApifyProxy": false }}
Common pitfalls
maxReviewsis a fetch budget, not a delivery guarantee. The per-storefront cap is applied to fetched rows, whilekeywords,appVersion,recentDays,endDate,languageand multi-starratingFilterare applied after that cap. With tight filters you will routinely save far fewer rows thanmaxReviews. Raise the budget, or use a single-starratingFilterso the filter runs server-side and none of the budget is wasted.maxReviews: 0produces an empty run. The schema allows0because its minimum is-1, but a zero budget makes every storefront skip immediately and land inmarketsEmpty. Use-1for unlimited, never0.dedupeAcrossMarkets: falsealone does not disable cross-market dedupe. The dedupe branch fires whendedupeAcrossMarketsoruniqueOnlyis true, anduniqueOnlydefaults totrue. To genuinely keep the samereviewIdin several markets, set bothdedupeAcrossMarkets: falseanduniqueOnly: false.endDateneeds an absolute date. DespitedateType: absoluteOrRelativein the schema, the filter does a plain string comparison against each row'sYYYY-MM-DDdate. A relative expression is not resolved and will not filter anything.ratingFilteroverridesrating. Setting both is not additive — a non-emptyratingFilterreplaces theratingvalue entirely.- Rows with no parsable date behave asymmetrically. A review whose timestamp could not be read has
date: null;recentDaysdrops it,endDatekeeps it. pagesToScrape: 0still fetches one page. The page cap is evaluated after the first page is processed, so0behaves like1. Use-1for unlimited.
⬆️ Output
Every run writes typed, normalized JSON to the Apify dataset — stable key names, no HTML fragments, no selector output. Export as JSON, JSONL, CSV, Excel, XML or RSS, or read the dataset over the Apify API. The four row types share one dataset and are told apart by type. The default view lists all 50 possible columns, so a CSV export stays column-complete even though each row type populates only its own subset.
Scraped reviews — type="review"
Every field below is written by the row builder for each review. 22 keys, always present; values may be null.
{"type": "review","reviewId": "8f3b2a1c-4d5e-4f60-9a7b-2c3d4e5f6071","rating": 2,"reviewer": "Markus H.","date": "2026-07-19","reviewedIn": "de","body": "Seit dem letzten Update stürzt die App im Offline-Modus ständig ab. Playlist-Downloads verschwinden nach jedem Neustart.","userImage": "https://play-lh.googleusercontent.com/a/ACg8ocK7Yc2QpF9","position": 143,"helpfulCounts": 27,"appId": "com.spotify.music","timestamp": 1784563200,"language": "de","storefrontKey": "de-DE","storefrontCountry": "DE","storefrontCountryName": "Germany","storefrontLanguage": "de","storefrontLanguageName": "German","appVersion": "9.1.64.1676","reviewedAt": "2026-07-19T12:00:00Z","reviewUrl": "https://play.google.com/store/apps/details?id=com.spotify.music&reviewId=8f3b2a1c-4d5e-4f60-9a7b-2c3d4e5f6071","scrapedAt": "2026-07-25T09:14:02Z"}
| Field | Type | Notes |
|---|---|---|
type | string | Always "review" for this row shape. |
reviewId | string | Play's own review identifier; the dedupe key. |
rating | integer | 1–5, as returned by Play. |
reviewer | string | Reviewer display name. |
date | string | YYYY-MM-DD UTC, derived from timestamp. null when no timestamp was present. |
reviewedIn | string | The storefront's hl code, kept for compatibility with the single-market base Actor. |
body | string | Full review text, in the storefront's own language. |
userImage | string | Avatar URL, or null when the record has no avatar block. |
position | integer | 1-indexed order within the app, continuous across storefronts. Rewritten at save time, after dedupe and filtering. |
helpfulCounts | integer | "Helpful" vote count. |
appId | string | Package name, parsed from the resolved store URL. |
timestamp | integer | Unix epoch seconds. |
language | string | Same value as reviewedIn; this is what the language input filter matches against. |
storefrontKey | string | Normalized hl-gl key, e.g. "de-DE". |
storefrontCountry / storefrontLanguage | string | Upper-cased gl code and lower-cased hl code. |
storefrontCountryName / storefrontLanguageName | string | Human-readable names; each echoes the raw code when it is outside the Actor's name tables. |
appVersion | string | App version reported on the review record, or null. |
reviewedAt | string | ISO 8601 UTC, YYYY-MM-DDTHH:MM:SSZ. null when no timestamp was present. |
reviewUrl | string | Deep link to the review, or null when reviewId is missing. |
scrapedAt | string | ISO 8601 UTC stamp taken once at run start, applied to every row in the run. |
Scraped market rollups — type="market"
One row per storefront that saved at least one review, emitted when emitMarketRollups is true. 26 keys. Statistics are computed locally from the saved sample only — after dedupe and after all filters — not from Play's global rating histogram.
{"type": "market","appId": "com.spotify.music","storefrontKey": "de-DE","storefrontCountry": "DE","storefrontCountryName": "Germany","storefrontLanguage": "de","storefrontLanguageName": "German","sampleSize": 150,"isCappedSample": true,"avgRatingInSample": 1.393,"oneStarCount": 91,"twoStarCount": 59,"threeStarCount": 0,"fourStarCount": 0,"fiveStarCount": 0,"oneStarSharePct": 60.67,"twoStarSharePct": 39.33,"threeStarSharePct": 0.0,"fourStarSharePct": 0.0,"fiveStarSharePct": 0.0,"uniqueToThisMarket": 150,"topAppVersionInSample": "9.1.64.1676","oldestReviewDate": "2026-06-26","newestReviewDate": "2026-07-24","topKeywordsInSample": ["absturz", "offline", "playlist", "premium", "werbung", "songs", "downloads", "fehler", "abo", "neustart"],"scrapedAt": "2026-07-25T09:14:02Z"}
| Field | Type | Notes |
|---|---|---|
type | string | Always "market". |
appId | string | Package name the rollup belongs to. |
storefrontKey, storefrontCountry, storefrontCountryName, storefrontLanguage, storefrontLanguageName | string | Same storefront stamp as on the review rows. |
sampleSize | integer | Number of saved review rows for this storefront. |
isCappedSample | boolean | true when the storefront hit its fetch budget — maxReviewsPerMarket, or the remaining slice of maxReviews — so the sample is truncated rather than exhaustive. |
avgRatingInSample | number | Mean star rating, rounded to 3 decimals. null when sampleSize is below minReviewsForRollupStats. |
oneStarCount … fiveStarCount | integer | Raw counts per star. Always emitted, even for tiny samples. |
oneStarSharePct … fiveStarSharePct | number | Percentage of the sample per star, rounded to 2 decimals. null when sampleSize is below minReviewsForRollupStats. |
uniqueToThisMarket | integer | How many of this storefront's saved rows carried a reviewId not already seen in an earlier storefront for the same app. |
topAppVersionInSample | string | Most frequent non-null appVersion in the sample, or null. |
oldestReviewDate / newestReviewDate | string | Earliest and latest date in the sample, or null. |
topKeywordsInSample | array of string | Up to 10 most frequent words of 3+ letters, with digits and a multilingual stop-word list removed. Only computed when the sample has at least 20 rows — a hard threshold in the code, independent of minReviewsForRollupStats. null otherwise. |
scrapedAt | string | Run-start timestamp, ISO 8601 UTC. |
App coverage manifest — type="coverage"
One row per app, emitted when emitMarketRollups is true and at least one storefront resolved an appId. 11 keys. Use it as the run-quality assertion in a pipeline.
{"type": "coverage","appId": "com.spotify.music","marketsRequested": 4,"marketsWithData": 3,"marketsEmpty": ["ja-JP"],"storefrontsCovered": ["en-US", "de-DE", "pt-BR", "ja-JP"],"totalReviewsSaved": 431,"crossMarketDuplicatesDropped": 62,"filteredOut": 907,"dedupeAcrossMarkets": true,"scrapedAt": "2026-07-25T09:14:02Z"}
type is always "coverage" and appId names the package. marketsRequested (integer) is the number of resolved storefronts and marketsWithData (integer) how many of them saved at least one review row. marketsEmpty (array of string, or null when none were empty) lists storefront keys that saved nothing — bootstrap failure, HTTP failure, budget exhaustion, or every row filtered away — while storefrontsCovered (array of string) is the full requested list in order. totalReviewsSaved (integer) counts the charged review rows for this app, crossMarketDuplicatesDropped (integer) the rows discarded because their reviewId had already been saved from an earlier storefront, and filteredOut (integer) the rows discarded by ratingFilter / rating / language / keywords / endDate / recentDays / appVersion. dedupeAcrossMarkets (boolean) echoes the input setting so the numbers are self-describing, and scrapedAt (string) is the run-start timestamp.
Diagnostics — type="error"
Emitted per failing storefront, plus once at run start if startUrls resolved to no usable targets. The per-storefront row carries 8 keys; the no-targets row carries 4 (type, errorReason, message, scrapedAt) and no storefront stamp.
{"type": "error","errorReason": "bootstrap_failed","message": "bootstrap_failed: app page fetch failed (status=429, len=0)","appId": "com.spotify.music","storefrontKey": "ja-JP","storefrontCountry": "JP","storefrontLanguage": "ja","scrapedAt": "2026-07-25T09:14:02Z"}
errorReason is the part of message before the first colon. The values the code can emit are:
errorReason | Meaning |
|---|---|
no_targets | startUrls was empty or contained nothing usable. Emitted once, without storefront fields. |
bootstrap_failed | The app page could not be fetched, WIZ_global_data was missing, the oCPfdb request template was not found, or the app id could not be parsed from the URL. The bootstrap is retried once before this row is written. |
batchexecute_http_<status> | The review RPC returned a non-200 on the first page for that storefront. <status> is None when every attempt raised a network error instead of returning a response. |
parse_failed | The RPC response could not be decoded on the first page — empty body, an HTML page instead of JSON, or no oCPfdb payload in the chunked response. |
A failure that happens after at least one page was already collected does not produce an error row. It is logged as a warning and that storefront stops early with whatever it had.
💰 What actually gets charged
This Actor is pay-per-event and declares a single charged event: row_result. It is attached to exactly one push in the source — the review row push inside the save loop.
| Row type | Charged? |
|---|---|
type="review" | Yes — one row_result per saved review row |
type="market" | No — pushed without a charged event |
type="coverage" | No — pushed without a charged event |
type="error" | No — pushed without a charged event |
Three consequences worth stating plainly:
- You are not billed for failures. A storefront that fails to bootstrap, returns a non-200, or fails to parse writes a diagnostic row at no event cost.
- You are not billed for discarded rows. Cross-market duplicates and rows removed by any filter are dropped before the push, so they never fire a
row_result. The counts still show up ascrossMarketDuplicatesDroppedandfilteredOuton the coverage row. - You are not billed for the analytics. Market rollups and coverage manifests are free additions to the dataset.
To isolate exactly the rows you paid for, filter the dataset on type. In the Apify dataset API you can trim columns with ?clean=true&fields=type,reviewId,rating,body,storefrontKey and keep rows where type == "review" (equivalently, drop type in ("market", "coverage", "error")). In pandas: df = df[df["type"] == "review"]. In jq: jq '[.[] | select(.type == "review")]'.
The per-event price is set on the Apify Store listing for this Actor. Apify platform usage — compute units, dataset storage, proxy traffic — is billed separately by Apify under your own plan.
How does this compare to other Google Play review scrapers?
All competitor figures below were read from those Actors' own public Apify Store listings and READMEs on 2026-07-25, and are quoted as their listings state them — none of it was measured here.
| Feature | This Actor | Generic Google Play review scraper |
|---|---|---|
| Country / language handling | storefronts is a list; one full bootstrap + pagination pass per hl-gl pair, 54 offered, with storefrontKey, storefrontCountry and storefrontLanguage stamped on every row | Typically single-valued. solidcode/google-play-store-reviews-scraper documents scalar language and countryCode inputs — its README advertises "55 country codes and 30 interface languages", one market per run. neatrat/google-play-store-reviews-scraper documents no country or language input at all. |
| Cross-market de-duplication | Built in and accounted for: dedupeAcrossMarkets, crossMarketDuplicatesDropped, uniqueToThisMarket | Not applicable to single-market scrapers; merging several runs yourself leaves the overlap in place. This Actor's input schema records 37 of 100 rows shared between en-US and en-GB. |
| Aggregate statistics in the dataset | type="market" rows (star counts, star shares, average rating, date span, top keywords) plus a type="coverage" manifest, both uncharged | None of the three listings checked document rollup or coverage row types |
| Output fields | 22 keys per review row; 50 columns across all four row types in the default view | solidcode advertises "24 typed fields per review" including 8 app-metadata fields and developer replies; neatrat documents 11 review fields; moving_beacon-owner1/my-actor-1 documents 4 (User, Rating, Date, Review) |
| Run-quality visibility | Uncharged diagnostic rows with a typed errorReason, plus marketsEmpty and marketsWithData on the coverage row | neatrat's README describes error handling in the run logs; none of the three document machine-readable per-market failure rows inside the dataset |
If you're building an AI agent or RAG pipeline, the output format row is the decision-maker — parsing HTML inside an agent loop is a reliability failure mode, not a feature. And the trade is explicit in both directions: if you need developer replies and app metadata joined onto every review row, solidcode's listing advertises both and this Actor does not extract them; if you need country-by-country segmentation inside a single run, this one does it and that one does not.
How many Google Play reviews can you scrape?
There is no hard result cap inside the Actor. maxReviews — the total review budget per app across all storefronts — defaults to 100, accepts up to 1000000, and accepts -1 for no limit at all. maxReviewsPerMarket defaults to 0, meaning no per-storefront cap. With maxReviews: -1, maxReviewsPerMarket: 0 and pagesToScrape: -1, each storefront paginates until Play stops handing back a cursor.
Pagination runs one storefront at a time. The Actor bootstraps the app page for that hl/gl, then POSTs the oCPfdb RPC repeatedly, carrying the continuation token from the previous response into the next request payload. reviewsPerPage (default 200, maximum 2000) controls how many rows each POST asks for; the input schema notes that 1000 has been verified to return in a single request, so raising it materially reduces the number of round trips. A storefront's loop ends when any of these happens: the budget is reached, pagesToScrape pages have been saved, Play returns an empty batch, no continuation cursor comes back, the cursor repeats, or two consecutive pages return only reviewId values already seen.
Two platform-side realities to plan around. First, Play genuinely serves different pools per language, so five storefronts do not return five copies of one pool — that is the whole point of the fan-out, and uniqueToThisMarket quantifies it per market. Second, relevance-ranked listings on Play are shallower than chronological ones: solidcode's README states that "Most Relevant is capped at ~50–150 reviews per app", as read on its Apify Store listing on 2026-07-25 and not measured here. For deep exports, set sortBy: "newest".
🔌 Integrate this Google Play review scraper into your workflow
This Actor works with any language or tool that can send an HTTP request to the Apify API. It runs on the Apify platform only — you authenticate with your Apify API token, and there is no separate service key or credential parameter anywhere in the input schema.
REST API integration
Start a run, wait for it, and read the reviews back with the official Apify Python client:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_API_TOKEN>")run = client.actor("<YOUR_USERNAME>/google-play-store-review-scraper-by-country-language").call(run_input={"startUrls": ["com.spotify.music"],"storefronts": ["en-US", "de-DE", "ja-JP"],"sortBy": "newest","maxReviewsPerMarket": 100,"maxReviews": 300,})for item in client.dataset(run["defaultDatasetId"]).iterate_items():if item["type"] == "review":print(item["storefrontKey"], item["rating"], item["appVersion"], (item["body"] or "")[:80])elif item["type"] == "market":print(item["storefrontKey"], "avg", item["avgRatingInSample"], item["topKeywordsInSample"])
Works in Python, Node.js, Go, Ruby, cURL — the same run-and-fetch pattern applies to every Apify client and to plain HTTP calls against api.apify.com.
Automation platforms (n8n, Make, LangChain)
n8n — use the official Apify node. Add a Run Actor operation, select this Actor, paste the JSON input, and chain a Get Dataset Items operation after it. An IF node on the item's type field splits reviews from market rollups, so the 1★ German rows can go to Slack while the rollups go to a spreadsheet.
Make — the Apify app on Make exposes Run an Actor and Get Dataset Items modules. Schedule Run an Actor daily with sortBy: "newest" and recentDays: 1, then filter the dataset-items module on type = "review" and rating <= 2 to open one ticket per new negative review, per market.
LangChain — the langchain-apify integration provides ApifyDatasetLoader and an Apify Actors tool. Point the loader at the run's defaultDatasetId and map each type="review" item into a Document with body as the page content and storefrontKey, storefrontCountry, rating, appVersion and date as metadata; that yields a market-filterable vector index with no extra transformation step. Any other agent framework can call the Actor as a plain HTTP endpoint and get the same typed JSON back.
Is it legal to scrape Google Play reviews?
Scraping publicly available Google Play review data is generally lawful in most jurisdictions, but how you store and use it is what carries the obligations. This Actor returns only content that is visible to a logged-out visitor on a public Play Store listing — no login-gated surface is touched, and no Google account or credential is used at any point.
Review rows do contain personal data: reviewer is a display name and userImage is an avatar URL. Under GDPR and CCPA that makes you a controller of that data once you store it, so you need a lawful basis (commonly legitimate interest for product research), a retention policy, and a way to honour deletion requests. If you only need sentiment and version signal, drop reviewer and userImage at ingest — the type="market" rollup rows contain no personal data at all and are often enough on their own.
Consult legal counsel for commercial use cases involving bulk personal data.
Frequently asked questions
Does this Google Play review scraper work without a Google account?
Yes. No Google account, Play Console access, OAuth token or cookie is required, and there is no credential parameter in the input schema. The Actor fetches the public app page for each storefront, reads the session id and build tag out of window.WIZ_global_data, and calls Play's public review RPC. The only credential involved anywhere is your own Apify API token, which authenticates you to Apify — not to Google.
How often is the scraped data updated?
Every run is a live fetch; nothing is cached between runs. Each storefront is bootstrapped and paginated fresh, and scrapedAt records the UTC moment the run started. For monitoring, schedule the Actor in the Apify Console with sortBy: "newest" and recentDays: 1, then use the type="coverage" row to confirm each run actually reached every market.
What happens if an app is delisted, or a storefront returns nothing?
You get an explicit, uncharged diagnostic instead of a silent gap. If the app page can't be fetched or parsed for a storefront, the Actor retries the bootstrap once, then writes a type="error" row with errorReason: "bootstrap_failed", the failing storefrontKey, and the underlying status in message. That storefront also appears in marketsEmpty on the coverage row. If the app simply isn't distributed there, the run continues through the remaining storefronts — one dead market never kills the run.
Can I scrape private or login-gated Google Play content?
No. Only publicly accessible store content is returned. The Actor cannot reach Play Console analytics, reviewer email addresses, private developer dashboards or anything behind a Google login, because it never authenticates. Developer replies are also not extracted by this Actor, even though they are public — the row builder does not read that slot.
What does a run actually cost me?
The Actor charges a single event, row_result, fired once per saved review row. Market rollups, the coverage manifest and every diagnostic row are pushed without a charged event, and rows dropped by dedupe or filters are discarded before the push, so they cost nothing either. The per-event price is shown on the Apify Store listing; Apify platform usage — compute units, storage, proxy traffic — is billed separately under your plan.
Does it work for AI agent workflows and LLM pipelines?
Yes. The Actor is callable as an HTTP endpoint by any agent framework — LangChain, LlamaIndex, a custom tool loop, or a plain requests call to the Apify API. Every row is typed JSON with stable key names, so there is no parsing step before you hand it to a model. A practical pattern: filter to type="review", embed body, and keep storefrontLanguage, storefrontCountry, rating, appVersion and date as retrieval metadata, so an agent can answer "what are Brazilian users complaining about on the current build" in one hop.
How does the Actor handle Google's anti-bot defences?
With three concrete mechanisms. First, a body-aware block detector: 401, 403, 429 and 503 count as soft blocks, and so does an empty body, a page containing "our systems have detected unusual traffic", or a /sorry/index page without wiz_global_data. Second, a one-time proxy escalation — on the first soft block the Actor requests an Apify residential proxy URL and uses it for the rest of the run, whether or not you enabled proxyConfiguration. Third, retry and pacing: up to 4 attempts per request with exponential backoff plus jitter, a second full bootstrap attempt, sequential requests with a randomized pause between pages, and a self-consistent Linux Chrome fingerprint (matching sec-ch-ua-platform, UA string and client hints) instead of a mismatched Windows UA from inside a Linux container.
How does this compare to other Google Play review scrapers on the Apify Store?
Country and language are a list here, not a scalar. Checked on the Apify Store on 2026-07-25: solidcode/google-play-store-reviews-scraper documents single-valued language and countryCode inputs and advertises 24 typed fields per review including developer replies and app metadata that this Actor does not extract. neatrat/google-play-store-reviews-scraper documents 11 review fields and no country or language input at all; its listing showed 1,652 users and a 4.76 rating on that date. moving_beacon-owner1/my-actor-1 documents four output fields (User, Rating, Date, Review). None of the three document per-market rollups, cross-market de-duplication or a coverage manifest. Pick this Actor for market-by-market breadth; pick one of those if a single market plus developer replies is what you need.
Does it return data in a format LLMs can use directly?
Yes. Typed, normalized JSON with stable field names — no HTML, no selectors, no parsing. Pass rows straight into an LLM context window, index them into a vector store, or route them through an agent tool. The type discriminator means one dataset can feed both a document index (type="review") and a numeric dashboard (type="market") with no transformation layer in between.
Can I use it without managing proxies?
Yes. proxyConfiguration is optional and prefilled with {"useApifyProxy": false}. The input schema notes that Google Play served every test request from an Apify datacenter IP with no proxy, so proxy-off is the recommended starting point. If a run does get soft-blocked, the Actor escalates itself to an Apify residential IP once and continues — nothing to configure or rotate. Note that this escalation uses Apify Proxy, so it consumes proxy usage on your plan even when the proxy toggle is off.
What happens when Google Play changes its page structure?
The Actor is maintained, and the output schema is the stable contract: reviewId, rating, body, storefrontKey and every other key documented above keep their names and types across engine updates, so your integrations don't need edits. The engine also resists the most common breakage — it anchors on the oCPfdb rpcid rather than a positional ds:N index, and harvests the live cfb2h build tag on every bootstrap instead of shipping a frozen one. If Play changes something the Actor can't absorb, you see it as a typed type="error" row rather than a silent zero-row run.
Your feedback
Found a bug, hit a storefront that behaves oddly, or need a field this Actor doesn't return yet? We want to know. Open an issue on the Issues tab of this Actor's Apify Store listing — include the run id and any type="error" rows from the dataset, since errorReason and message usually pin the problem down immediately. Field requests and new storefront codes are welcome too; the country and language tables are the part of this Actor most likely to grow next.