Quick answer
Neither major app store has a public reviews API. Apple's customer-reviews RSS/JSON feed hard-caps at roughly 50 reviews per app per country storefront — page 2 and beyond are verifiably dead, confirmed across every path-segment ordering we tried, not something you can raise by asking harder. Google Play has no feed at all; its reviews come out of an internal RPC endpoint called batchexecute that returns arrays of arrays with zero field names — you get item[2] for the rating and item[7][1] for a developer reply, and if Google reshapes the response tomorrow, every index shifts silently. If your pipeline treats "app store reviews" as one JSON-fetch problem, it's actually two reverse-engineering problems wearing the same trench coat.
Why does Apple only give you ~50 reviews? 🍎
The Apple side looks deceptively normal at first: GET /rss/customerreviews/id={app_id}/sortby=mostrecent/json returns a real, documented-looking feed shape — feed.entry[], each with a label-wrapped field for author, rating, title, content. It looks pageable, because the URL pattern implies a page= segment exists elsewhere in Apple's RSS feed family.
It doesn't work here. During recon we tried every path-segment ordering page=2 could plausibly take on this specific endpoint, and every one came back empty or identical to page 1. That's not a bug in our request — it's Apple's actual limit on this feed. So rather than ship a pagination code path that silently returns duplicate or empty pages past 50 reviews, we scoped it out entirely:
# actors/app-store-reviews-aso-scraper/src/apple_client.py
"""Page 1 only — page=2 and higher were verified dead across every
path-segment ordering tried during the 2026-08-15 recon session.
This module intentionally has no code path that accepts, builds, or
appends a page parameter; Apple pagination is scoped out permanently,
not as a deferred feature."""
Setting maxReviewsPerApp to 500 in the input will not change Apple's answer. That's Apple's ceiling, not ours, and no scraper — including whatever you might build yourself — can raise it. The honest move is naming the limit in the README rather than let a customer discover it by getting confused about a low row count.
What does Google Play actually send back, structurally? 🤖
Google Play's review data comes from /PlayStoreUi/data/batchexecute, the same private RPC transport the Play Store web app itself uses to fetch reviews for its own UI — not a documented API, just the wire format their own frontend happens to use. The response isn't JSON. It's JSON wrapped in an XSSI-protection prefix, and the payload inside that is itself JSON-encoded as a string, so decoding it is two json.loads() calls with a prefix strip in between:
# actors/app-store-reviews-aso-scraper/src/play_client.py
ENVELOPE_PREFIX = ")]}'\n\n"
def decode_envelope(raw_text: str) -> Any:
stripped = raw_text[len(ENVELOPE_PREFIX):] if raw_text.startswith(ENVELOPE_PREFIX) else raw_text
outer = json.loads(stripped)
inner_json = outer[0][2]
return json.loads(inner_json)
What comes out the other end has no field names at all — every review is a bare array, and every field is a hardcoded index into it, confirmed by inspecting a real response against com.instagram.android and cross-checked byte-for-byte against another open-source Play scraper's known-good request body for the same package on the same day:
IDX_RATING = 2
IDX_CONTENT = 4
IDX_THUMBS_UP = 6
IDX_DEV_REPLY_BLOCK = 7 # item[7][1] = reply content, item[7][2][0] = reply timestamp
IDX_APP_VERSION = 10
Every index lives as a named module-level constant specifically so that the day Google reshapes this RPC — and there's no contract saying they won't, since it's not a published API — fixing it is a one-file diff against a re-captured fixture, not a design change. The request body itself is a pre-URL-encoded, positionally-templated string built the same way Google's own frontend builds it; there's no documented request schema to reference, only a working example to replicate exactly.
What happens when an app has zero reviews? 🧮
This is the detail that breaks naive pipelines on both stores: an app with genuinely no reviews yet is a normal, complete result — not a failure. is_empty_response() on the Play side and has_entries() on the Apple side both return a clean "zero, and that's correct" signal rather than raising, and the per-app ASO rollup row still gets computed — with an all-zero rating distribution and empty keyword maps — rather than being skipped, so total_reviews_sampled: 0 is always a real, present row you can query against, not a gap in your dataset you have to explain later.
The part that generalises 🧭
Two stores, two completely different reverse-engineering problems, and the one thing they share is that neither publishes what they actually send. Apple's feed looks like a real API and quietly enforces an undocumented ceiling; Google's RPC doesn't even pretend to be a stable API and requires positional decoding with no field names as a starting point. When a target's data doesn't come from a documented endpoint, the honest engineering move is naming exactly what was confirmed live, dating it, and building the narrowest client that matches — not a general-purpose one that assumes stability the target never promised.
What the Actor gives you
- One normalized
ResultRowschema across bothgoogle_playandapp_store— same field names, same date format, same 1-5 rating scale, no post-processing to reconcile them. - A per-app ASO rollup row: rating distribution, monthly rating trend, review velocity per day, and deterministic praise/complaint keyword frequency from the reviews actually sampled.
- Either store alone works fine — supply just a package name or just an Apple app ID.
- Per-app fault isolation: one bad app id is skipped and logged, the rest of the batch still ships.
Honest limitations 🚧
Apple review depth is capped near 50 per app per country by Apple itself, not by this Actor. The ASO rollup reflects only the reviews sampled in that run — total_reviews_sampled tells you exactly how many, deliberately, rather than implying a full-catalogue statistic that was never fetched.
FAQ
Do I need an API key or login for either store?
No — there is no public reviews API to get a key for on either side. That's the whole reason this Actor exists.
Why does Apple return so few reviews?
Apple's own customer-reviews feed caps out around 50 per app per country. maxReviewsPerApp can't raise it — that's Apple's limit, not ours.
What happens if one app in my list fails?
It's skipped, the run continues, and the log names which app failed and why. One bad id never costs you the batch.
Does an app with zero reviews fail the run?
No — a legitimately empty result is a valid answer. The run finishes SUCCEEDED and says so.
Is the ASO rollup computed from all of an app's reviews, or just what was sampled?
Only what was actually sampled in that run — total_reviews_sampled states the real number rather than implying a full-catalogue figure.
Pricing
$0.20 per run, $0.0012 per review row, $0.005 per ASO rollup row (once per app) — ≈$1.41 for 1,000 reviews on one app.
→ App Store Reviews & ASO Scraper on Apify
Built by Devil Scrapes. We handle the envelope decoding, the positional field maps, and the undocumented ceilings, so you get one flat table instead of two reverse-engineering projects. 😈
Top comments (0)