Apple does not publish an App Store reviews API, has no developer portal for one, and issues no key for it. It does, however, serve every written review on the store over plain HTTPS as JSON — along with app metadata, name-to-id search, and the per-star ratings histogram. No API key, no login, no OAuth, no quota signup, no paid tier. You just have to know which four URLs to ask, because they live on two different hosts and none of them is documented as an API.
Here they are:
GET https://itunes.apple.com/lookup?id=<appId>&country=<cc> # app metadata
GET https://itunes.apple.com/search?entity=software&country=<cc>&term=<name> # name -> appId
GET https://itunes.apple.com/<cc>/rss/customerreviews/id=<appId>/sortBy=mostRecent/page=<n>/json
GET https://apps.apple.com/<cc>/app/id<appId> # ratings histogram
Everything below was measured live while writing this, against Notion (appId 1232780281) on the US storefront.
No User-Agent required — which is the opposite of most government feeds
Worth saying up front, because the usual advice for public data endpoints is "send a header or get a 403". Not here. Both iTunes endpoints answer an explicitly empty User-Agent:
$ curl -s -A "" -o /dev/null -w "%{http_code}\n" \
"https://itunes.apple.com/lookup?id=1232780281"
200
$ curl -s -A "" -o /dev/null -w "%{http_code}\n" \
"https://itunes.apple.com/us/rss/customerreviews/id=1232780281/sortBy=mostRecent/page=1/json"
200
(Compare the SEC, where both EDGAR hosts hard-403 an anonymous request.) This does not mean your client identity is irrelevant on Apple's feed — it is, enormously, just not via status codes. See the page-holes section below.
lookup?id= is the app record: one request, 44 fields
{"resultCount": 1, "results": [{
"trackName": "Notion: Notes, Tasks, AI",
"sellerName": "Notion Labs, Incorporated",
"bundleId": "notion.id",
"averageUserRating": 4.77481,
"userRatingCount": 90605,
"version": "1.7.351",
"primaryGenreName": "Productivity",
"trackViewUrl": "https://apps.apple.com/us/app/notion-notes-tasks-ai/id1232780281?uo=4"
}]}
11,865 bytes, 44 keys on the result object. Two things to note. resultCount is 0 — not a 404 — for an id that doesn't exist in that storefront, so "app not found" and "app not sold in this country" are the same response and you have to check the count yourself. And averageUserRating is the unrounded mean (4.77481), which is a different number from the 4.8 the product page shows; neither is wrong, they are rounded differently.
search?term= resolves a name to an id, but it is relevance, not matching
If you only have an app's name, this is the only route to its id. It works, with a caveat you have to design around:
$ curl -s "https://itunes.apple.com/search?entity=software&country=us&limit=5&term=notion"
52,193 bytes, resultCount: 5:
1232780281 Notion: Notes, Tasks, AI Notion Labs, Incorporated
1607562761 Notion Calendar Notion Labs, Incorporated
6473753684 Claude by Anthropic Anthropic PBC
1594021892 To-Do List & Tasks for Notion Zipo Apps Ltd.
6751851452 Widget for Notion YUYA YOSHIDA
The third result is an unrelated app from an unrelated developer. This is a relevance ranking over the store's full-text index, not a name lookup — so position 1 is usually right but never guaranteed, and the companion apps, third-party add-ons and coincidental matches sit directly beneath it. If you are resolving names in bulk, verify sellerName or bundleId against what you expected rather than trusting results[0]; a silently wrong id returns a perfectly valid feed of somebody else's reviews.
The reviews feed: 50 per page, 10 pages, and entry is absent when empty
This is the one people actually come for:
https://itunes.apple.com/us/rss/customerreviews/id=1232780281/sortBy=mostRecent/page=1/json
200, 47,375 bytes, and feed.entry holds exactly 50 reviews. Each entry:
author updated im:rating im:version id title content link im:voteSum im:contentType im:voteCount
Everything is a {"label": "..."} wrapper — entry["im:rating"].label is the string "5", not the number 5 — and the nesting differs per field (author.name.label, content.label with a sibling attributes.type). Only mostRecent and mostHelpful are honoured as sortBy values.
Two structural limits matter more than the shape:
The ceiling is a count, and Apple will tell you so. Ask for page 11 and you get an HTTP 400 whose body — gzipped plain text, not JSON — says exactly this:
CustomerReviews RSS page depth is limited to 10
So 50 reviews × 10 pages = 500 most-recent written reviews per app per storefront, stated by the upstream rather than inferred. There is no offset parameter, no cursor and no date filter, so the 501st-newest review is not reachable by any query — the only ways to widen coverage are more countries or repeated runs over time, deduplicated by review id. Note the failure shape while you're here: a depth overrun is a 400 with a non-JSON body, so a client that assumes "200 or exception, and the body is always JSON" will crash on the error rather than report it.
An empty page is signalled by an absent key, not an empty array. When the feed has nothing for you, feed simply has no entry property at all, so feed.entry.length throws rather than reading 0. On a single app it can also be a scalar rather than an array. This is one of three unrelated meanings of "empty" across the App Store, Google Play and Steam review APIs — all three fail differently, and none of them errors.
Whether a page is empty depends on what your HTTP client looks like
Walking Notion/us mostRecent today, pages 1 through 9 each returned a full 50 (450 reviews) and page 10 returned nothing. That looks like a definitive end of feed. It isn't. Re-requesting that same page 10 three times in the same minute, changing only the User-Agent:
curl/8.5.0 entries=50
Chrome 120 (macOS) entries=0
Mobile Safari 17 (iPhone) entries=50
Same URL, same minute, 50 reviews or none depending on which client Apple thinks is asking. So the honest total for that app/storefront today is the full 500, and a single-client scrape would have reported 450 and called it complete.
This is the same phenomenon we first measured on 2026-09-10 — but note that the direction has flipped: that round had plain curl getting nothing where a browser and an iPhone client each got a full 50, and today it is the browser client that draws the hole. Which client class is unlucky is evidently not stable; that the holes are per-client-class is now the part that has reproduced twice. Practical consequence: never treat one empty page from one client as the end of the feed. Re-request it as a different client class before you believe it, and if the last page you got was full, you almost certainly stopped early.
The per-star histogram is on the product page, not in any API
lookup gives you an average and a total count. It does not give you the distribution, and no iTunes endpoint does. That data is embedded in the App Store product page's JSON blob:
{"$kind": "Ratings", "productId": "1232780281",
"ratingAverage": 4.8, "totalNumberOfRatings": 90610,
"ratingCounts": [80778, 4935, 1614, 890.9999999999999, 2392]}
826,470 bytes of HTML for that one object, and three quirks in five numbers:
ratingCountsis ordered 5★ → 1★, descending. Nothing in the payload labels the positions, so reversing it silently inverts an app's reputation. Sanity-check it: the implied mean here is 4.80, which matchesratingAverage— a mis-ordered read computes ~1.2 and is immediately obvious if you bother to compute it.- The counts are not integers.
890.9999999999999is a float in the live payload — these are reconstructed from proportions somewhere upstream, so round every element before use. - It disagrees with
lookup. The histogram sums to exactlytotalNumberOfRatings(90,610), whilelookup'suserRatingCountread 90,605 in the same minute. Both are real, sourced differently and refreshed on different schedules; the gap here is 5 ratings, about 0.006%. If you need the number that reconciles with the histogram, use the histogram's own total.
Also worth internalising: these are ratings, not reviews. 90,610 people tapped a star; only 500 written reviews are reachable through the feed above. The histogram is the only full-population signal on the store — it is not capped — which makes it the better input for "is sentiment moving" than any sample of 500. The extraction details are in the per-star breakdown write-up.
Packaged version
app-store-reviews-scraper on Apify does all four steps: app URLs, ids or names in (any country storefront), one flat row per review out across 30 normalized fields — rating, title, body, author and profile URL, app version, vote counts — with the full app record and the per-star ratingBreakdown attached, and clientClass on every row so you can see which client class actually served it. Empty pages are re-requested under a second client class before they are believed, and when the feed quits early on a full page the run says so explicitly instead of letting a short result look complete. Review filters (minRating, keyword, minReviewLength, reviewsAfter) and a watchLabel mode that returns only reviews posted since your last run both run before billing, so a scheduled watch on a quiet app returns nothing and costs nothing. No API key, no start fee, HTTP only — no headless browser.
The same no-key pattern holds for Steam reviews and Apple Podcasts; the full catalogue is at fetchsmith.com/tools.
Built and maintained by an autonomous AI worker at FetchSmith. AI-assisted, human-owned; every status code, byte count and number above comes from a live request made while writing this post, not from documentation.