--- name: google-play-reviews-scraper description: Scrape Google Play app reviews via the Apify Actor arman-bd/google-play-reviews-scraper. Takes package names or store URLs; returns one record per review with the stable reviewId, reviewer, whole-number star rating, full text, ISO 8601 timestamp, the app version it was written against, helpful-vote count, per-aspect criteria ratings, and the developer's reply with its own timestamp. Sorts by newest, the store's relevance ranking, or rating; filters by exact star ratings at the source (a 1-star pull never pages through 5-star reviews), by language edition and storefront country, and by absolute or relative date, where the newest sort stops paging at the boundary. Use for complaint monitoring, release quality tracking by version, competitor review research, support auditing, or a labelled review corpus. Not for the app's overall rating or installs (that is an app-details scraper), not for Apple App Store reviews, and not for detecting each review's written language. --- # Google Play Reviews Scraper Apify Actor `arman-bd/google-play-reviews-scraper`. Give it apps by package name or store URL, get one dataset record per review. No credentials are involved. ## When to use it - Complaint monitoring: 1-star and 2-star reviews of the last week, on a schedule. - Release tracking: did ratings move because of the version that shipped Tuesday? Each row carries the `appVersion` it was written against. - Competitor research: the reviews users actually see and upvote, via the store's own relevance ranking. - Support auditing: which complaints a developer answers, and how fast, from `replyText` and `replyDate`. - Corpus building: dated, versioned, star-labelled review text at scale, in any of the store's language editions. ## When not to use it - **The app's overall rating, install count or description.** This Actor returns the reviews themselves; app metadata is an app-details scraper's job. - **Apple App Store reviews.** Different store entirely. - **Per-review language detection.** `language` selects the store's language edition of the review stream; it does not detect what language each text is written in, and an occasional other-language review appears in every edition. - **Review history.** The store shows only the latest version of an edited review, under the same `reviewId` with a new `date`. History has to be accumulated by running on a schedule and diffing. - One specific review by ID. This reads streams; it does not look up single reviews. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/google-play-reviews-scraper').call({ appIds: ['com.headway.books'], sort: 'newest', starRatings: ['1', '2'], maxReviewsPerApp: 500, dateFrom: '30 days', }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const summary = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('RUN_SUMMARY'); ``` One call, no SDK, records straight back: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~google-play-reviews-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H 'content-type: application/json' \ -d '{"appIds":["com.duolingo"],"sort":"newest","maxReviewsPerApp":100}' ``` ## Input | Key | Type | Required | Default | Notes | |---|---|---|---|---| | `appIds` | array | yes | | Package names (`com.spotify.music`), store URLs (`https://play.google.com/store/apps/details?id=...`), or `market://` links, in any mix. Each app is read independently and unrecognised entries are named in the summary, not silently dropped. | | `sort` | string | | `newest` | `newest`, `mostRelevant` (the store's default view: detailed, heavily-upvoted reviews first), or `rating` (highest first). | | `starRatings` | array | | all | Strings `"1"` to `"5"`. Filtered at the source, so each selected star is fetched directly. The cap applies per app, not per star. | | `maxReviewsPerApp` | integer | | `200` | Cap per app and the main cost control; charging is per review saved. Up to 100000. | | `language` | string | | `en` | Store language code: `en`, `de`, `ja`, `pt-BR`. Selects the store's language grouping of reviews. | | `country` | string | | `us` | Two-letter storefront code. The review pool overlaps but is not identical across storefronts. | | `dateFrom` | string | | | Oldest review to keep. `2026-01-15`, or relative `30 days` / `36 hours` / `6 months`. Exact, applied to each review's own timestamp. With `sort: "newest"` the run stops paging at the boundary. | | `dateTo` | string | | | Newest review to keep, same formats. Ignored unless `dateFrom` is set. | ## Output One record per review. Every key is present on every record; `null` means the store has nothing there. | Field | Type | Notes | |---|---|---| | `appId` | string | Package name the row belongs to. | | `reviewId` | string | Stable GUID. **Use this as the join and diff key.** | | `url` | string | Store link straight to this review. | | `userName` | string | Reviewer's display name. | | `userImage` | string | Avatar URL. | | `rating` | number | Stars 1 to 5, always a whole number. | | `text` | string\|null | Full review text. `null` on rating-only reviews. | | `date` | string | ISO 8601 with millisecond precision. | | `appVersion` | string\|null | Version the review was written against. `null` on roughly a third of rows. | | `thumbsUp` | number | Helpful votes. Never null. | | `replyText` | string\|null | The developer's public reply. | | `replyDate` | string\|null | When the reply was posted. Present exactly where `replyText` is. | | `criteria` | array | Per-aspect ratings as `{name, rating}` pairs, e.g. ad frequency or battery use, in the store's own tag names. Empty on most rows. | | `language` | string | The language edition the run read. Not a detection. | | `country` | string | The storefront the run read. | | `sort` | string | Which sort produced the row. | | `position` | number | Rank within its app for this run. Reshuffles between runs; do not diff on it. | | `scrapedAt` | string | ISO 8601. | ```json { "appId": "com.headway.books", "reviewId": "57cbf30c-6ad9-433e-b378-d7b761b93876", "url": "https://play.google.com/store/apps/details?id=com.headway.books&reviewId=57cbf30c-6ad9-433e-b378-d7b761b93876", "userName": "Roger Juman", "userImage": "https://play-lh.googleusercontent.com/a-/ALV-UjVNOlcL9EnNV55b3skzplYTfdHEZjAe4O9mWa_sUOmz5ubhx_Sfag", "rating": 1, "text": "bad.it can not open.", "date": "2026-08-04T00:25:24.007Z", "appVersion": "3.171.2", "thumbsUp": 0, "replyText": "Hi! Thanks for flagging this. If the issue persists after reinstalling the app, email support@get-headway.com, and we'll look into it.", "replyDate": "2026-08-04T12:57:43.346Z", "criteria": [ { "name": "vaf_app_quality_usability", "rating": 2 }, { "name": "vaf_app_quality_battery_heat", "rating": 3 } ], "language": "en", "country": "us", "sort": "newest", "position": 10, "scrapedAt": "2026-08-09T14:17:34.719Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under `RUN_SUMMARY`. ```json { "appsRequested": ["com.headway.books"], "invalidInputs": [], "appsNotFound": 0, "filters": { "sort": "newest", "starRatings": [1], "maxReviewsPerApp": 15, "language": "en", "country": "us", "dateRange": { "from": null, "to": null, "dateToIgnored": false, "earlyStopApplied": false } }, "requestsMade": 1, "reviewsSaved": 15, "duplicatesCollapsed": 0, "outOfDateRange": 0, "starMismatches": 0, "itemsDroppedReasons": {}, "perApp": { "com.headway.books": { "pages": 1, "fetched": 15, "saved": 15, "duplicatesCollapsed": 0, "outOfDateRange": 0, "starMismatches": 0, "notFound": false, "exhausted": false, "ceilingHit": true, "stalled": false } }, "malformedFields": {}, "failures": [], "finishedAt": "2026-08-09T14:39:15.361Z" } ``` `perApp[...].exhausted` means the store ran out of matching reviews before the cap; `ceilingHit` means the opposite, more reviews existed and the run stopped first, whether at `maxReviewsPerApp` or at the run's own page budget. `stalled: true` means a date-filtered pass read 20 consecutive pages with nothing in range and gave up rather than spend the full budget; use sort `newest` with a date range to avoid it. `outOfDateRange` counts rows fetched but outside `dateFrom`/`dateTo`; they are never charged. `appsNotFound` and the named entries in `failures` are package names the store does not know. `malformedFields` is normally empty; entries there mean the source shifted shape and those fields should be treated as unreliable until the Actor is updated. `starMismatches` counts rows the source returned in disagreement with the star filter, dropped and not charged. ## Behaviour to plan around - **`reviewId` is the identity of a review.** Diff runs on it. `position` reshuffles between runs, and an edited review keeps its `reviewId` while its `date` and text move. That is also how to catch edits: same id, new date. - **A capped run's contents depend on `sort`.** `newest` reads back from now, `mostRelevant` reads what the store's ranking surfaces (heavily-upvoted, detailed, often months old), `rating` reads highest-first. For "the last N days" always use `newest` with `dateFrom`, which also stops paging at the boundary instead of reading to the cap. - **Star filtering happens at the source.** `starRatings: ["1"]` on a well-loved app costs what it returns, not a crawl through everything else. Expect reviews-per-request efficiency to be the same as an unfiltered pull. - **`appVersion` is null on roughly a third of rows**, and `text` is null on rating-only reviews. Filter on presence rather than assuming it. - **`criteria` names are the store's own tags** (for example `vaf_app_quality_ads_frequency`) and coverage varies review to review; most rows carry an empty array. Treat it as bonus signal, not a promised dimension. - **Language editions are separate streams.** Two languages are two runs, and the union is not the complete review set; the store groups reviews its own way. - **An unknown package name fails loudly, an unreviewed app quietly.** The first is named in `failures` as not found; the second returns zero rows with a clean exit. A run whose every app failed exits with an error naming each one. - **Charging is per review saved.** Duplicates, rows outside the date range, star mismatches and failed apps are never charged. One bad app does not end the run. ## Recipes **Weekly complaint digest for your own app** ```json { "appIds": ["com.your.app"], "sort": "newest", "starRatings": ["1", "2"], "dateFrom": "7 days", "maxReviewsPerApp": 2000 } ``` Diff against last week on `reviewId`; rows with a previously-seen id and a new `date` are edited reviews, usually someone updating after a fix or a failed fix. **Did the new release cause the complaints?** ```json { "appIds": ["com.your.app"], "sort": "newest", "dateFrom": "14 days", "maxReviewsPerApp": 5000 } ``` Group rows by `appVersion` and compare rating distributions between the old and new version strings. Null versions stay in their own bucket. **What a competitor's users actually see** ```json { "appIds": ["com.competitor.app"], "sort": "mostRelevant", "maxReviewsPerApp": 200 } ``` The store's own ranking, which favours detailed and heavily-upvoted reviews. Sort by `thumbsUp` for the most socially-validated complaints and praise. **Support responsiveness audit** ```json { "appIds": ["com.competitor.app"], "starRatings": ["1"], "maxReviewsPerApp": 1000 } ``` `replyText !== null` gives the answer rate; `replyDate` minus `date` the response time. Compare against your own app with a second run. **A labelled corpus in one language** ```json { "appIds": ["com.some.app"], "language": "de", "country": "de", "maxReviewsPerApp": 20000 } ``` Star labels come with every row. Expect an occasional non-German text; the edition is the store's grouping, not a language detector.