DEV Community

Ben Barazandeh
Ben Barazandeh

Posted on

Collecting App Store Reviews by Country and Version With Apple's Public Feed

Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.

A star average tells you almost nothing. "4.6 stars" hides the fact that version 7.144.0 broke streak freezes for half your users in the UK. What product teams actually want is reviews by version and by country, for their own app and for competitors.

For your own app, App Store Connect has the reviews and lets you reply. For any other app, there's no official reviews API, but Apple does publish a public customer-reviews feed per storefront, and the iTunes Search/Lookup API covers the app metadata.

How the feed works

https://itunes.apple.com/{country}/rss/customerreviews/page={n}/id={appId}/sortby={mostrecent|mosthelpful}/json
Enter fullscreen mode Exit fullscreen mode
  • country is a storefront code (us, gb, de, jp, br…). Every storefront has its own reviews.
  • Each page has up to 50 reviews, and you get at most 10 pages per sort order. That's 500 for mostrecent and 500 for mosthelpful.
  • Each entry has the rating, title, text, author, app version and date.

The app ID is the number after id in the App Store URL (389801252 for Instagram). If you only have a name, the iTunes Search API turns it into an ID, and Lookup gives you the average rating, rating count and current version.

Step 1: A collector you can run today

This pulls both sort orders for one storefront and dedupes on review ID. It ran as-is on 25 Oct 2026:

import requests, time

def reviews(app_id, country="us"):
    seen = {}
    for sort in ("mostrecent", "mosthelpful"):
        for page in range(1, 11):  # Apple serves at most 10 pages of 50 per sort order
            url = (f"https://itunes.apple.com/{country}/rss/customerreviews/"
                   f"page={page}/id={app_id}/sortby={sort}/json")
            entries = requests.get(url, params={"_": int(time.time())}, timeout=30) \
                              .json().get("feed", {}).get("entry", [])
            if isinstance(entries, dict):  # defensive: normalise a lone object to a list
                entries = [entries]
            for e in entries:
                if "im:rating" not in e:
                    continue  # skip anything that isn't a review
                seen[e["id"]["label"]] = {
                    "rating": int(e["im:rating"]["label"]), "version": e["im:version"]["label"],
                    "title": e["title"]["label"], "date": e["updated"]["label"]}
            if not entries:
                break
            time.sleep(0.5)
    return list(seen.values())

rows = reviews(389801252, "gb")   # Instagram, UK storefront
print(len(rows), "unique reviews")
print(sum(r["rating"] <= 2 for r in rows), "are 1-2 stars")
Enter fullscreen mode Exit fullscreen mode

I ran it twice, a few minutes apart. The first run printed 749 unique reviews / 492 are 1-2 stars. The second printed:

996 unique reviews
611 are 1-2 stars
Enter fullscreen mode Exit fullscreen mode

Same app, same storefront, same code. The difference is the feed's habit of returning an empty page before the real end: my loop treats an empty page as "done", so the first run stopped early on at least one sort order. That's the most important pitfall in this whole post, and the fix is to retry empty pages (with a fresh cache-buster) before giving up.

Also notice the skew: the newest and "most helpful" reviews lean negative (611 of 996 here). That describes this sample, not every review ever written.

Step 2: The pitfalls

  • The cap is per sort order and per country. To see more, add storefronts. A US-only pull misses most of the feedback for an app that's big in Germany, Japan or Brazil.
  • History needs polling. You only ever see roughly the newest 500. To build a full history, run on a schedule and keep what you've already stored.
  • Empty pages that aren't the end. As above. Retry an empty page a couple of times with a new cache-busting parameter before you treat it as the last page.
  • Structure. Every value is wrapped in {"label": ...}, and dates come with a UTC offset (e.g. -07:00). Normalise to UTC before you compare countries.
  • Version is the useful field. Group 1★ reviews by im:version and release-related spikes jump out.

Step 3: Or use the packaged version

I packaged this as an Apify Actor, App Store Reviews Scraper. It takes App Store URLs, numeric IDs or plain app names, any number of countries, and sort set to most recent, most helpful or both (deduplicated, up to about 1,000 unique per app per country).

{
  "apps": ["https://apps.apple.com/us/app/instagram/id389801252", "duolingo"],
  "countries": ["us", "gb"],
  "maxReviewsPerApp": 50
}
Enter fullscreen mode Exit fullscreen mode

You can filter by min/max stars, "only reviews since" a date, and keywords in the title or text. Filtered-out reviews aren't charged. Optional app details go on every row: name, developer, bundle ID, genre, average rating, rating count, current version.

That input returned 200 reviews from 4 app/country combinations in about 22 seconds on 18 Oct 2026. Some rows:

App Store Rating Review title Version Date
Instagram GB ★★★★★ Like a digital scrap book 451.0.0 2026-10-17
Instagram GB ★ Suspended exile 451.0.0 2026-10-16
Duolingo: Learn Languages US ★ Annoying streak freezes 7.144.0 2026-10-17
Duolingo: Language & Chess GB ★★★★★ So good 7.144.0 2026-10-17

Note the name difference: the same Duolingo app has a different title in the US and UK storefronts.

The workflow I'd use

  1. Your app plus 3–5 competitors, in the 5–10 countries that matter to you.
  2. maxRating: 2 for a complaints feed, or no filter for everything.
  3. A weekly schedule with "only reviews since" set to last week.
  4. Send the rows to a sheet, or to an LLM step that clusters complaints by version.

Limits, honestly

  • Apple's cap is the cap. About 500 per sort order per country, so around 1,000 per country at best. Anything older is only reachable if you started polling earlier.
  • No Google Play. There's no official public reviews API for other people's apps on Play.
  • Not affiliated with Apple.

What it costs

$0.0002 per review ($0.20 per 1,000). App lookups and filtered-out reviews are free, there's no start fee, and runs stop at your "maximum cost per run". The 200-review test above came to 4 cents. Some Store Actors are slightly cheaper per review. The script above is free.

Play fair

Reviews are public, but they're written by people. Don't republish them with names attached, and follow Apple's terms and privacy laws.

How do you analyse reviews today: by hand, with tags, or with an LLM? I'm curious what's actually worked for you, so tell me in the comments.

Top comments (0)