If you ship an iOS app, you probably read your reviews in App Store Connect or on the public store page for one country. That hides a lot. A crash that only shows up on a German locale, a pricing complaint from Brazil, a translation bug in Japan: those reviews live in their own storefronts and never show up in the US list.
This post shows two ways to get them:
- The free, do-it-yourself way using Apple's public review RSS feed (fine for one or two countries).
- Sweeping every storefront in one call with an Apify Actor I built, then filtering to the reviews that matter (1–2 stars, recent, mentioning "crash" or "subscription").
How Apple exposes reviews
Apple publishes a public JSON feed of customer reviews per app, per country:
https://itunes.apple.com/{country}/rss/customerreviews/page={1-10}/id={appId}/sortby=mostrecent/json
Each page holds up to 50 reviews and there are 10 pages, so you get at most 500 reviews per country per sort order. No key, no login.
A minimal DIY version for one country:
import requests
APP_ID = "570060128" # Duolingo, the number after "id" in the App Store URL
COUNTRY = "de"
reviews = []
for page in range(1, 11):
url = f"https://itunes.apple.com/{COUNTRY}/rss/customerreviews/page={page}/id={APP_ID}/sortby=mostrecent/json"
entries = requests.get(url, timeout=20).json().get("feed", {}).get("entry", [])
if isinstance(entries, dict): # a single entry comes back as an object, not a list
entries = [entries]
if not entries:
break
for e in entries:
if "im:rating" not in e: # older feeds put the app itself in the first entry
continue
reviews.append({
"rating": int(e["im:rating"]["label"]),
"title": e["title"]["label"],
"text": e["content"]["label"],
"version": e.get("im:version", {}).get("label"),
})
print(len(reviews), "reviews from", COUNTRY.upper())
That works. The pain starts when you want every country: 155 storefronts × 10 pages is 1,550 requests, with throttling, empty storefronts, the dict-vs-list quirk, retries, and de-duplication to handle. That part is what I packaged up.
Sweep all 155 storefronts in one call
App Store Reviews Scraper – All Countries runs the loop above across every storefront in parallel and gives you one clean dataset. Install the client:
pip install apify-client
export APIFY_TOKEN=... # from Apify Console > Settings > API & Integrations
Then ask for the negative, recent reviews from everywhere:
import os
from collections import Counter
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("olsols4/app-store-reviews-all-countries").call(run_input={
"apps": ["https://apps.apple.com/us/app/duolingo/id570060128"],
"countries": ["ALL"], # or a list like ["us", "gb", "de", "br", "jp"]
"maxReviewsPerCountry": 500,
"sortBy": "mostrecent",
"minRating": 1,
"maxRating": 2, # only 1- and 2-star reviews
"reviewsSince": "2026-09-01", # e.g. the date of your last release
"keywords": ["crash", "subscription", "refund", "login"],
"includeAppDetails": True,
})
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
reviews = [i for i in items if i["type"] == "review"]
apps = [i for i in items if i["type"] == "app"]
print(apps[0]["name"], apps[0]["averageRating"], "stars")
print(len(reviews), "matching reviews")
# Which countries are complaining the most?
for country, n in Counter(r["country"] for r in reviews).most_common(10):
print(country, n)
# Which app version are the complaints about?
for version, n in Counter(r["appVersion"] for r in reviews).most_common(5):
print(version, n)
You can also pass app names instead of URLs with "searchTerms": ["duolingo"] (it takes the top store match), which is handy for competitor research.
What a row looks like
One row per review, plus one type: "app" summary row per app. Here is a real one from a test run today (author removed):
{
"type": "review",
"appId": "570060128",
"country": "DE",
"reviewId": "14665011333",
"rating": 1,
"title": "Nicht mehr was es war",
"text": "Leider richtig schade, das war mal eine gute App aber mittlerweile ...",
"appVersion": "7.144.0",
"voteCount": 0,
"voteSum": 0
}
Note the keyword filter matches words as they appear, so for non-English storefronts add the local words too ("absturz" for German crashes, "assinatura" for Portuguese subscriptions), or drop keywords and filter afterwards with a translation step or an LLM.
Turn it into a weekly report
The useful version of this is a recurring job, not a one-off:
- In Apify Console, open the Actor, set the input above, and Save as a task.
- Add a Schedule (e.g. every Monday 08:00) for that task.
- Pick the latest results up from your own code:
run = client.actor("olsols4/app-store-reviews-all-countries").last_run(status="SUCCEEDED")
items = run.dataset().list_items().items
Or skip code entirely and use Apify's built-in integrations to push each run to Google Sheets, Slack, a webhook, n8n, Make or Zapier.
Set reviewsSince to roughly a week back each time (or just de-duplicate on reviewId on your side) so you only look at what's new.
Ideas for what to do with the data
-
Release QA: after every release, run with
maxRating: 2andreviewsSince= release day. A cluster of 1-star reviews from one country on the new version is usually a localisation or regional-payment bug. - Competitor research: run the same query on two or three rival apps and compare what users in each market dislike.
-
ASO copy: pull 4–5 star reviews (
minRating: 4) and mine the phrases real users use for your store description. -
Feed an LLM: the
textfield is clean, so it drops straight into a sentiment or topic-clustering prompt.
Cost and limits (being upfront)
- It's pay-per-result: $0.25 per 1,000 reviews, plus $0.002 per app summary row. Countries with no reviews cost nothing. One app, all countries, newest 50 per country is typically a few thousand reviews, so around a dollar or two.
- The 500-per-country ceiling is Apple's, not the Actor's. If you need a full historical archive of a huge app, this won't give it to you; for "what are people saying right now, everywhere", it's plenty.
- It's App Store only, no Google Play.
- Reviews include public nicknames; handle them in line with GDPR and your local rules.
If you try it and a field is missing or something breaks, open an issue on the Actor's Issues tab and I'll take a look.
Top comments (0)