For years the way to read App Store reviews without being the app's owner was one URL:
https://itunes.apple.com/us/rss/customerreviews/id=310633997/sortBy=mostRecent/page=1/json
It still answers 200 OK. It just doesn't return any reviews any more.
import requests
r = requests.get("https://itunes.apple.com/us/rss/customerreviews/"
"id=310633997/sortBy=mostRecent/page=1/json", timeout=30)
print(r.status_code, "entry" in r.json()["feed"]) # 200 False
I checked Skype (284862083), WhatsApp (310633997) and YouTube (544007664) in both us and gb, from a home connection and from datacenter IPs: every response was a well-formed feed with no entry array. Occasionally one app in one storefront still returns data — which is worse than a clean failure, because your pipeline keeps running and silently writes zero rows.
That is exactly how I found it: a monitoring run of my own scraper reported "success, 0 items" one morning.
What still works
The App Store website is a normal web app, and it fetches reviews from its own API:
GET https://apps.apple.com/api/apps/v1/catalog/{country}/apps/{appId}/reviews
?platform=web&limit=20&offset=0&sort=recent
No token, no key, no account. It needs browser-ish headers, returns 20 reviews per request and a cursor in next.
import time, requests
HEADERS = {
"User-Agent": "Mozilla/5.0",
"Origin": "https://apps.apple.com",
"Referer": "https://apps.apple.com/",
"Accept": "application/json",
}
def app_store_reviews(app_id, country="us", want=200):
out, offset = [], 0
while len(out) < want:
r = requests.get(
f"https://apps.apple.com/api/apps/v1/catalog/{country}/apps/{app_id}/reviews",
params={"platform": "web", "limit": 20, "offset": offset, "sort": "recent"},
headers=HEADERS, timeout=30,
)
if r.status_code == 429: # it rate-limits hard
time.sleep(int(r.headers.get("Retry-After", 15)))
continue
r.raise_for_status()
data = r.json()
out += [x["attributes"] for x in data["data"]]
if not data.get("next"):
break
offset += 20
time.sleep(3) # ~1 request / 3s is sustainable
return out[:want]
for rev in app_store_reviews("310633997", "us", want=40)[:3]:
print(rev["date"][:10], rev["rating"], rev["title"])
Things worth knowing before you build on it:
-
Pacing matters. The limit is per IP and it is not published: from a home connection six back-to-back requests went through, from a datacenter IP the third one already returned
429(sometimes withRetry-After, sometimes as an HTML error page). One request every ~3 seconds has been sustainable for me in production; 200 reviews take about half a minute. -
sort=recentgives newest first. Without it you get Apple's default mix, which is not stable between calls — painful if you deduplicate by position instead of by id. - The fields are
date,rating,title,review,userName,isEdited. No app version, no helpful-vote count — the old RSS had both. If you need the version a review refers to, that data is no longer public. - Each storefront is a separate pool:
us,gb,de,brreturn different reviews, and that is the only supported way to slice by market. - App Store Connect API is not an alternative unless you own the app — it only exposes your own apps' reviews.
The Google Play half
Google Play has no equivalent RSS legacy to mourn: google-play-scraper (Python or Node) reads the same endpoints the store's web player uses.
from google_play_scraper import Sort, reviews
res, token = reviews("com.whatsapp", lang="en", country="us",
sort=Sort.NEWEST, count=200)
print(len(res), res[0]["at"], res[0]["score"], res[0]["content"][:60])
One trap that costs people an afternoon: country is not the filter you think it is. Google serves largely the same review pool per app; what actually changes the result set is lang, which filters by the language of the review text. If you want German reviews, ask for lang="de", not just country="de".
Google Play does give you things Apple no longer does: the app version a review refers to, thumbs-up counts, and the developer's reply.
Doing it without maintaining any of this
I package both halves as a pay-per-result Actor on the Apify Store — this is my tool, and the reason this post exists is that I had to fix it in production:
curl -X POST "https://api.apify.com/v2/acts/chorelet~app-reviews-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN&format=csv" \
-H "Content-Type: application/json" \
-d '{
"apps": ["https://apps.apple.com/us/app/whatsapp-messenger/id310633997", "com.whatsapp"],
"countries": ["us", "de"],
"maxReviewsPerApp": 500,
"sort": "newest"
}' > reviews.csv
Both stores come back in one schema — platform, appId, appName, country, reviewId, author, rating, title, text, date, version, helpfulVotes, developerReply — with the Apple-only-nulls where Apple no longer publishes a field. It costs $0.10 per 1,000 reviews, and Apify's free plan ($5 a month, no card) covers 50,000 of them.
There is a companion Actor for charts and keyword rankings on both stores, if what you actually want is "where does my app rank for meditation in Brazil": App Store & Google Play Rankings.
Takeaway
If you have a review pipeline built on itunes.apple.com/…/rss/customerreviews, it is probably returning zeros right now and not telling you. Two lessons I took from it:
- Alert on empty, not just on errors. A silent zero looks exactly like a quiet week.
- When a public feed dies, the website that replaced it usually has an endpoint you can read — open the network tab before you reach for a headless browser.
Top comments (0)