Slide·Easy Download freeDownload

Scrape App Store reviews with Python (and the RSS feed most people miss)

When I want to know what people actually say about an app, I read its App Store reviews. Reading them one by one is slow and copying them into a spreadsheet is worse, so here is a small script that pulls them into a CSV.

It uses only Python's standard library (urllib, json, csv), needs no API key and no login, and runs in under half a minute. It works because Apple still serves a public RSS feed of customer reviews for every app. A lot of scraping tutorials drive a headless browser or call App Store Connect, which only gives you reviews for your own apps. The feed is simpler than both.

Below: how the feed works, its limits (checked live on 7 October 2026), the full script, and a real run against one public app.

1. Get the app id

Every App Store URL ends with the app's numeric id:

https://apps.apple.com/us/app/things-3/id904237743

The id is the number after id: 904237743. The two letters after apps.apple.com/ are the storefront (country). The script accepts either the full URL or the bare number and pulls the digits out with one regex.

2. The feed URL

https://itunes.apple.com/{country}/rss/customerreviews/page={page}/id={app_id}/sortby=mostrecent/json

What I found when I tested it:

Each review comes back with the rating, title, text, app version, date and a vote count. The fields are nested one level deeper than you'd expect: the rating lives at entry["im:rating"]["label"], the text at entry["content"]["label"].

To see how much of the picture 500 reviews is, the iTunes lookup API has the total number of ratings:

https://itunes.apple.com/lookup?id=904237743

For Things 3 that returned userRatingCount: 27990. That includes star-only ratings with no text, so the pool of written reviews is smaller. Still, 500 reviews is a sample, not the whole history.

3. Storefronts matter more than you'd think

Reviews are per country. I ran the script for the US and the UK stores and compared the two CSVs: zero overlapping reviews. The UK set was also much older: its 500 most recent reviews went back to May 2017, while the US set (400 reviews, with the two empty pages) reached back to October 2022, because the US store gets more reviews.

So if you want more than 500, loop over country codes (us, gb, ca, au, in...). For an English-language app, those five give you up to 2,500 reviews.

4. The script

import csv, json, re, sys, time, urllib.error, urllib.request

FEED = "https://itunes.apple.com/{cc}/rss/customerreviews/page={page}/id={app_id}/sortby=mostrecent/json"


def app_id_from(url_or_id):
    m = re.search(r"id(\d+)", url_or_id) or re.fullmatch(r"(\d+)", url_or_id)
    if not m:
        sys.exit(f"No app id in {url_or_id!r}")
    return m.group(1)


def fetch_page(app_id, cc, page):
    url = FEED.format(cc=cc, page=page, app_id=app_id)
    req = urllib.request.Request(url, headers={"User-Agent": "review-export/1.0"})
    for attempt in range(3):
        try:
            with urllib.request.urlopen(req, timeout=20) as r:
                entries = json.load(r).get("feed", {}).get("entry", [])
            if entries:
                return entries
            time.sleep(2 ** attempt)  # an empty page is sometimes a hiccup: ask again
        except urllib.error.HTTPError as e:
            if e.code in (400, 404):  # past the last page
                return []
            time.sleep(2 ** attempt)
        except urllib.error.URLError:
            time.sleep(2 ** attempt)
    return []


def main(url_or_id, cc="us", out="reviews.csv"):
    app_id = app_id_from(url_or_id)
    rows = []
    for page in range(1, 11):  # the feed stops at page 10 (50 reviews a page)
        entries = fetch_page(app_id, cc, page)
        if isinstance(entries, dict):  # defensive: a lone review could come back as a dict
            entries = [entries]
        entries = [e for e in entries if "im:rating" in e]
        if not entries:
            continue  # a page can come back empty while later ones are full
        for e in entries:
            rows.append({
                "date": e["updated"]["label"][:10],
                "rating": int(e["im:rating"]["label"]),
                "version": e.get("im:version", {}).get("label", ""),
                "title": e["title"]["label"],
                "review": e["content"]["label"],
            })
        time.sleep(1)  # be polite
    with open(out, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["date", "rating", "version", "title", "review"])
        w.writeheader()
        w.writerows(rows)
    print(f"{len(rows)} reviews from {cc.upper()} store -> {out}")


if __name__ == "__main__":
    main(*sys.argv[1:])

A shorter 22-line version is export_reviews.py in sumantop/app-store-tools (MIT). A few choices in this one worth explaining:

5. Be polite

The feed is public, but it's Apple's server. The script sleeps a second between pages, sends a plain User-Agent and stops at page 10. Ten requests per app per country is nothing. Don't run it in a tight loop across thousands of apps, and don't republish other people's reviews wholesale. I use the text for research (what people praise, what makes them angry), not as content.

6. A real run

I ran it against Things 3 on 7 October 2026, a well-known paid app with a lot of reviews:

$ python3 reviews.py "https://apps.apple.com/us/app/things-3/id904237743" us things_us.csv
400 reviews from US store -> things_us.csv

It took 26 seconds, most of it the pauses and the retries on the two empty pages. The first rows of the CSV:

date,rating,version,title,review
2026-10-04,5,3.24.2,Balance,A good balance between being simple to use while still giving you flexibility in how you want to organize your tasks
2026-09-30,4,3.24.2,"Great, but possibly overpriced","It’s a great app that does everything I want and more, though Apple Reminders has caught up to it in many ways, so I’m not sure it’s worth $10."

The star split of those 400: 235 five-star, 57 four-star, 36 three-star, 26 two-star and 46 one-star. The newest was from 4 October 2026 and the oldest from 13 October 2022.

Then the laziest useful analysis there is: take the 72 reviews with one or two stars and count a few keywords. $ appeared in 7, "widget" in 6, "windows" in 4 and "sync" in 4. That's crude (a review that says "I wish it synced faster but I love it" counts the same as an angry one), but in two minutes it shows what Things 3's unhappy users keep asking for: more platforms, better widgets, and a price they can justify. One five-star review from 28 September says "No subscription, no AI, no BS." That's the kind of line you'd never think to write in your own marketing, and a customer wrote it for free.

7. Next step: tag themes, don't just count words

Keyword counts get you started. The next step is grouping reviews into themes (price, sync and lost data, reminders, looks) and reading the happy and angry versions of each. I did that for 5,920 reviews across 20 app categories, with plain regular expressions and no model, and wrote up what came out: What 5,920 App Store reviews say users actually want to hear. Price talk (trials, subscriptions, being charged) was the biggest topic I counted, and the most likely to come with one or two stars.

No-code version

If you don't want to run Python, the free App Store review exporter on this site exports up to 200 of the newest reviews to CSV, with a star histogram, in your browser and with no login. I built it. The script above goes further (500 per country, plus the app version), so use whichever fits. There's also a wider guide to all three ways to export App Store reviews, including App Store Connect and its API.

Point the script at your own app or a competitor's, and read the one-star reviews first. They're where the useful sentences are.

How this post was made

I drafted this with AI help and checked it the same day: the script was run live against Things 3 (US and UK) on 7 October 2026, and every number above comes from that run.

7 posts free · $49 once

Download freeSee the price