How to scrape App Store and Google Play reviews
Neither store has a global review list. Apple serves reviews per storefront country, Google per language and country locale, and both decide what a given locale sees. A US-only export of a global app is a minority of its reviews — here is how to get the rest, and the one sort setting that quietly breaks daily runs.
The short version
- List every storefront or locale that matters. Apple takes two-letter storefront codes; Google takes language and country pairs. Both are lists, so one run covers them all.
- Choose the sort that matches the job. Most recent for monitoring, most critical for a complaints feed. Google's "most relevant" is a ranking, not a date order.
- Filter before billing. Star band, text required and a date cut all run before the charge, so a daily complaints feed pays only for complaints.
- Key on the review id. Both stores can serve the same review under more than one locale. Deduplicating on the review id is what keeps a multi-locale run from double-counting.
Storefronts and locales are multipliers, not filters
Apple. Reviews live per storefront country. Forty-eight storefronts are addressable by two-letter code, and one run takes a list:
{ "appIds": ["310633997"], "countries": ["us", "gb", "de", "fr", "jp", "br", "in"],
"maxReviewsPerApp": 200, "sort": "most_recent" }
That is one run, seven app rows — each with its own rating histogram for that storefront — and up to 1,400 review rows, every one tagged with its country. Apple occasionally lists the same review on two storefronts; the first to deliver it keeps it, so a review is never billed twice.
Google. Reviews live per hl/gl locale, and Google decides which reviews it shows under a given locale. A review can appear under several locales, and a few appear under none. There is no single call that returns "all reviews".
The practical consequence is the same for both: name every language or country the app has meaningful traffic in, or accept that your corpus is a sample of unknown shape. A US-only export of a global app is not "the reviews" — it is the US storefront's reviews.
Working code
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
apple = client.actor("kestrel/app-store-reviews-scraper").call(run_input={
"appIds": ["310633997"], "countries": ["us", "gb", "de"],
"sort": "most_recent", "maxReviewsPerApp": 100,
})
play = 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 run, store in ((apple, "ios"), (play, "android")):
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["type"] == "review":
print(store, row["country"], row["rating"], row["review_day"], (row["text"] or "")[:70])
The two row shapes line up on purpose — rating, text, author, review_day, country, the developer's reply — so a union of both is a table, not a merge project.
{
"type": "review", "review_id": "13657831289", "app_id": "310633997",
"app_name": "WhatsApp Messenger", "country": "us", "rating": 5,
"title": "WhatsApp not bad",
"text": "WhatsApp's not bad at all - it's actually great for what it does...",
"author": "Ed Bradway", "version": null, "is_edited": false,
"review_date": "2026-01-21T04:48:38Z", "review_day": "2026-01-21",
"developer_response": null, "developer_response_date": null
}
{
"type": "review", "review_id": "22aacbd1-267e-445b-948c-629ad3d87a2a",
"app_id": "com.whatsapp", "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
}
The sort that breaks daily runs
Google Play's default sort is most relevant, and it is a ranking, not a date order. It surfaces the reviews Play itself promotes: long, upvoted, often months old. Schedule a daily job on it and you re-read broadly the same set every morning, conclude nothing changed, and miss the release-day pile-up entirely.
For monitoring, sort by most recent and add a date cut (sinceDate: "2 days"). With newest-first order the walk stops at the first page older than the cut, so a daily run is one or two requests per locale.
Most relevant is the right choice for exactly one job: sentiment work where you want the reviews other users actually read.
The version field, and why it matters more than it looks
app_version on the Play row is the app version the reviewer was running. It is the single most useful column in the dataset for a release-quality workflow: group one-star reviews by version and a bad build announces itself within hours, before it shows in the aggregate rating, which is smoothed by every historical rating.
Apple's current endpoint does not always attach a version to a review — the column is null when it is absent, rather than filled with a guess.
Complaints feeds cost cents
{ "appIds": ["310633997"], "countries": ["us", "gb"], "sort": "most_critical",
"maxRating": 2, "requireText": true, "maxReviewsPerApp": 300 }
Most-critical order plus a two-star ceiling plus a text requirement means the run reads critical reviews and bills only the ones with words in them. The status row's filtered count shows what was dropped for free. At $0.004 an App Store review and $0.004 a Play review, watching a portfolio of ten apps across four storefronts for new one- and two-star reviews is a few dollars a month.
The honest limitations
- Ratings are not reviews. Most stars come with no text, in both stores.
- Pace. Apple answers roughly one request per second per IP and returns 429 beyond that, so a large run wants several IPs.
- Play has no app-name search. Package names only.
- Play review images and per-version rating breakdowns are not available through the review endpoint.
- The Play author id is Google's opaque account id — useful for de-duplicating a reviewer across apps, not for identifying a person.
Run it
App Store Reviews Scraper and Google Play Reviews Scraper — $0.004 per delivered review each, with the app rows, status rows and filtered reviews free. Related: building a voice-of-customer dataset that puts app reviews next to Trustpilot and marketplace reviews, and scheduling with relative dates.
FAQ
Why does an app with 2 million ratings only have a few thousand reviews?
Because ratings and reviews are different things. Most people tap stars and never write anything. Apple's app row carries written_reviews — how many the storefront actually holds — next to the rating count, so the gap is visible instead of implied.
Can I search apps by name?
On the App Store yes: names resolve to the top search hit on each storefront through Apple's own search. On Google Play no — Play has no open search API, so give the package name from the URL (com.whatsapp). A tool that claims name search on Play is guessing.
How deep can I go?
Apple pages ten reviews at a time by offset and keeps answering deep into an app's history — tens of thousands of reviews for the biggest apps. Google pages 100 at a time through the full corpus for the locale. In both cases the practical limit is your budget and your patience, not a wall.
Why did my daily Play run return the same reviews as yesterday?
Almost certainly because it is sorted by most relevant, which is Google's own ranking of reviews it wants to surface. It barely changes day to day. For "what is new since yesterday", sort by most recent and add a date cut.
Run Apple App Store Reviews Scraper on Apify →
Other actors used on this page: apify.com/kestrel/google-play-reviews-scraper.