DEV Community

Chorelet
Chorelet

Posted on Originally published at chorelet.app AI-assisted

Apple's app review RSS feed is empty now — here's what the App Store itself uses

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

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 with Retry-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=recent gives 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, br return 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])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Alert on empty, not just on errors. A silent zero looks exactly like a quiet week.
  2. 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)