Apple App Store Scraper - App Metadata & Ratings by Country
Pricing
from $0.06 / 1,000 review scrapes
Apple App Store Scraper - App Metadata & Ratings by Country
Scrape Apple App Store and Google Play app metadata: ratings per country, install counts, pricing, current version and release notes. Feed it app IDs, store URLs or search terms and it profiles everything it finds across the storefronts you name. Reviews optional. From $X / 1,000 apps.
Pricing
from $0.06 / 1,000 review scrapes
Rating
0.0
(0)
Developer
Eimantas V
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Google Play Store Reviews Scraper
Pull Google Play reviews with the star rating, full text, the app version each one was written against, the developer's reply and how long it took — and Google's own topic classification of what the review is about.
Reviews on Play are partitioned by language. Asking for English and asking for Spanish returns two entirely different sets of reviews, not the same reviews translated. Measured on one app across six languages: zero overlap between any pair, 600 distinct reviews where English alone gave 100. Every other scraper in this category takes one language and presents it as the app's reviews. This one takes as many as you name and unions them.
No browser. One JSON request returns 200 reviews in about 200 ms.
What you get per review
| Field | Notes |
|---|---|
rating, text, textLength | text is null, never "", when a review carries no words |
appVersion | The release the review was written against. Present on ~98% |
qualityTags | Google's own classification, e.g. vaf_app_quality_stability, vaf_app_quality_ads_frequency. Present on 35-56% — see below |
authorId | Stable across apps and time — the only safe key for longitudinal work |
replyText, replyAt, replyLatencyHours | The developer's answer and how many hours it took. Latency is null — never negative — when the review was edited after the reply |
reviewEditedAfterReply | The user revised the review after support answered. True on ~11% of rows |
lastUpdatedAt, lastUpdatedAtEpoch | When the review was last edited — the only timestamp Play exposes. See below |
language | Which corpus this row came from |
thumbsUpCount, authorName, authorAvatarUrl, reviewId, appId, sourceUrl |
Plus one coverage summary row per app: how many reviews came back per language, how many duplicates and filtered rows were dropped, and whether the corpus was exhausted or the cap stopped it. A run says what it covered, not only what it found.
null means Google did not report it. It never means zero and never means
empty — an absent appVersion recorded as "" would let a GROUP BY invent a
release that never shipped.
Play does not expose when a review was posted
Only when it was last edited. That distinction is not cosmetic: across 300 live Spotify reviews, 33 carried a developer reply older than the review's own timestamp — the user came back and revised it after support answered. Explicit "Edit:" markers are 6.5x more common in those rows than in the rest.
So the field is called lastUpdatedAt rather than publishedAt, and
replyLatencyHours is null on those rows rather than a negative number
asserting that the developer answered before the review existed. The fact is
kept in reviewEditedAfterReply, which is a useful signal in its own right.
There is also no reviewer country. Play accepts a gl parameter and ignores
it — measured at 100/100 identical results between gl=US and gl=DE — so a
country field would be an assertion about the reviewer that the data does not
support.
Why the topic tags matter
Slot 12 of every review carries Google's own auto-classification — generic
quality axes (stability, performance, battery_efficiency,
ads_frequency, visual_appeal, usability) plus per-vertical topics. It is
free, pre-labelled topic data sitting in an obscure nested slot that naive
parsers skip. It is the thing buyers otherwise pay an LLM to derive.
Coverage depends on how old the review is, because Google's classifier runs behind the review. Measured on 300 Spotify reviews per ordering:
| Ordering | Median review age | Tagged |
|---|---|---|
newest | 1 day | 35% |
helpfulness | 33 days | 52% |
| — reviews under 30 days | 46% | |
| — reviews 30 days or older | 56% |
So a newest-sorted pull is the least tagged. If the tags are what you are
after, pull with helpfulness, or re-pull older windows later — an untagged
review can acquire tags after Google gets to it.
Input
{"apps": ["com.spotify.music"],"languages": ["en", "es", "de", "pt", "fr", "ja"],"maxReviewsPerApp": 600,"sortBy": "newest"}
apps takes bare package names or Play Store URLs. languages is the one that
matters — see above. maxReviewsPerApp is spent round-robin across the
languages you select, so six languages and a 600 cap gives you 100 from each,
not 600 English ones. Adding languages changes which reviews you get, not how
many you are charged for.
Filters — minRating, maxRating, updatedAfter, onlyWithReply — are
applied before billing. Filtered rows are never charged.
maxReviewsPerApp bounds the output; maxReviewsScanned (default 20,000)
bounds the work. With a selective filter those differ enormously — WhatsApp
has zero 1-2 star reviews carrying a developer reply in its first 3,000 — so
without the second bound a 200-review request paginates until the run times
out. When the budget runs out the summary row says so, with
stoppedOnScanLimit: true and the reviewsScanned count, because "none in the
first 20,000" is a different answer from "none exist".
Pricing
From $0.06 per 1,000 reviews — $0.00012 each on the Free plan, falling to $0.00006 on Business. Apps that cannot be resolved, duplicate reviews and filtered rows are never charged. Full workings in docs/PRICING.md.
Notes on the implementation
The request body's nesting is not guessable and gets this wrong silently. The
shape [2, count, [null, null, token]] — the one most published notes use —
returns HTTP 200 carrying a PlayDataError and zero rows, which is
indistinguishable from an app with no reviews. The working shape is
[2, null, [count, null, token], null, []], it is written once in
src/modules/rpc.ts, and there is a test whose only job is
to stop it drifting back.
The sort code is the second element of that block, not the first. Slot 0 is
inert — 2, 4, 5, 6 and 7 all return the byte-identical relevance-ordered page —
so a "newest" option built on it looks like it works and quietly returns Play's
own ordering forever. Measured, not assumed: [2][1] of 1 is relevance, 2
is date-descending and holds across pages, 3 is rating-descending.
The cap has the same property in reverse. It counts reviews kept, so a filter matching nothing means the loop never reaches it and paginates forever — bounded output, unbounded work. That one shipped, and the run it killed came back as TIMED-OUT.
The language round-robin has the same property. Drained one language at a time, the first one eats the whole cap and the rest never run — you select six languages and get 200 English reviews, with the feature you chose silently not happening. Clamping the loop is not enough, because Play returns whatever page size it is asked for; the request itself has to be clamped to the share. That also has a test.