Apple App Store Scraper - App Metadata & Ratings by Country avatar

Apple App Store Scraper - App Metadata & Ratings by Country

Pricing

from $0.06 / 1,000 review scrapes

Go to Apify Store
Apple App Store Scraper - App Metadata & Ratings by Country

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

Eimantas V

Maintained by Community

Actor 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

FieldNotes
rating, text, textLengthtext is null, never "", when a review carries no words
appVersionThe release the review was written against. Present on ~98%
qualityTagsGoogle's own classification, e.g. vaf_app_quality_stability, vaf_app_quality_ads_frequency. Present on 35-56% — see below
authorIdStable across apps and time — the only safe key for longitudinal work
replyText, replyAt, replyLatencyHoursThe developer's answer and how many hours it took. Latency is null — never negative — when the review was edited after the reply
reviewEditedAfterReplyThe user revised the review after support answered. True on ~11% of rows
lastUpdatedAt, lastUpdatedAtEpochWhen the review was last edited — the only timestamp Play exposes. See below
languageWhich 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:

OrderingMedian review ageTagged
newest1 day35%
helpfulness33 days52%
— reviews under 30 days46%
— reviews 30 days or older56%

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.