kestrel actors › Apps
Apple App Store Reviews Scraper — App Reviews & Ratings
An Apple App Store reviews scraper that returns every customer review of any iOS app as structured rows: the 1–5 star rating, title, review text, reviewer nickname, app version, date, edited flag and the developer's public reply — apple app reviews from every country storefront in a single run, each row tagged with its country. It reads the same JSON endpoint the App Store's own web page calls, so it pages far past the 500-review cap of Apple's old RSS feed (which now answers empty for most apps) and works for ios app reviews in the tens of thousands per app.
Run Apple App Store Reviews Scraper on Apify →
Apple app store reviews scraper pricing: what one row costs
| Event | What it is | Price | Charged for |
|---|---|---|---|
review | Review row | $0.004 | One App Store customer review: 1-5 rating, title, review text, reviewer name, app version, date, edited flag and the developer's reply. App rows, status rows, filtered reviews and apps with no reviews are free. |
Pay per delivered review row, and nothing else: app rows, status rows, filtered reviews, apps with no reviews, unknown apps and failed jobs are free. A 200-review pull of one app on one storefront is 200 rows; a complaints feed that reads 300 reviews and keeps 40 bills 40. Apple pages ten reviews per request, so cost tracks rows, not requests. The price per row is on the pricing tab, and a spending limit on the run caps the total — whatever the limit refuses is never delivered.
Apple app store reviews scraper input reference
Every field of App Store Reviews, straight from the actor's input schema.
| Field | Type | Default | What it does |
|---|---|---|---|
appIds | array | [] | Apple's numeric app ids, e.g. 310633997 (the digits after /id in any App Store URL) — the fastest input, and what an earlier run's app_id gives you. |
startUrls | array | [] | App pages, e.g. https://apps.apple.com/us/app/whatsapp-messenger/id310633997 — the id is read from the URL for you. |
appNames | array | [] | App names as you would type them into App Store search, e.g. "Duolingo". Each is resolved to the top search hit on each storefront through Apple's own search API — no id hunting. |
countries | array | ["us"] | Two-letter storefront codes, one per line: us, gb, de, fr, jp, br, in… Reviews live per storefront, so a global app needs several. Every review row carries its country; a review Apple lists on two storefronts is delivered and billed once. |
maxReviewsPerApp | integer | 100 | 0 = every review Apple pages out (big apps run into the tens of thousands per storefront). N = the first N in the order below. The main cost control. Range 0-100000. |
sort | string (enum) | "most_recent" | Most recent is the one to schedule; most critical puts the 1-star reviews first; most helpful is the App Store's own default order. One of: most_recent, most_helpful, most_critical, most_favorable. |
minRating | integer | 0 | 0 = keep every review. 1-5 = keep only reviews at or above this many stars. Filtered reviews are never charged. Range 0-5. |
maxRating | integer | 0 | 0 = keep every review. 1-5 = keep only reviews at or below this many stars — set 2 for a complaints feed that pays nothing for the happy ones. Range 0-5. |
sinceDate | string | "" | Leave empty for all. YYYY-MM-DD, or relative so a schedule never goes stale: "7 days", "2 weeks", "3 months". Combine with "Most recent" for a daily new-reviews feed. |
requireText | boolean | false | Drop reviews that are a star rating with no words, so you deliver and pay for only the ones that say something. |
includeAppRow | boolean | true | Also emit one free row per app and storefront: name, developer, genre, price, version, average rating, rating count and the 1-5 star histogram. |
sessions | integer | 4 | How many proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately. Range 1-16. |
perIp | number | 1 | Pace for each session. Apple answers about 1/s per IP; faster earns 429 replies and retries, not speed. Range 0.1-3. |
proxyConfiguration | object | {"useApifyProxy": true} | Apple's endpoints are public and need no login, but they cap each IP at about one request a second. Apify Proxy (the default datacentre group) spreads a large run over several IPs; small runs work without it. |
Example input
{ "appIds": ["310633997"], "countries": ["us", "gb", "de", "fr", "jp", "br", "in"], "maxReviewsPerApp": 200 }
{ "appIds": ["310633997"], "countries": ["us", "gb"], "sort": "most_critical", "maxRating": 2, "requireText": true, "maxReviewsPerApp": 300 }
Apple app store reviews scraper output fields
Three row types share one dataset: review (one customer review, charged), app (context for an app on a storefront, free) and status (per app and country, free).
Dataset views in the Apify Console: Overview, Reviews, Complaints, Apps, Run status.
| Field | Type | What it holds |
|---|---|---|
type | string | Row type: review, app or status. |
app_id | string | Apple's numeric app id. |
app_name | string | App name on that storefront. |
country | string | Storefront the row came from (two-letter code). |
review_id | string | Apple's id for the review. |
rating | integer | Stars the reviewer gave, 1-5. |
title | string | Review headline. |
text | string | The review body. |
author | string | Reviewer's public nickname. |
version | string | App version the review was written against, when Apple reports it. |
is_edited | boolean | True when the reviewer edited the review after posting. |
review_date | string | When the review was posted (ISO 8601, UTC). |
review_day | string | The same as a plain YYYY-MM-DD date, for filtering and charts. |
developer_response | string | The developer's public reply, when there is one. |
developer_response_date | string | When the developer replied. |
url | string | The app's reviews page on that storefront. |
developer | string | App rows: developer (artist) name. |
seller | string | App rows: seller of record. |
bundle_id | string | App rows: bundle identifier. |
genre | string | App rows: primary genre. |
price | number | App rows: price on that storefront (0 = free). |
currency | string | App rows: currency of the price. |
age_rating | string | App rows: content advisory rating, e.g. 12+. |
rating_avg | number | App rows: average star rating on that storefront. |
rating_count | integer | App rows: how many ratings the app has there. |
rating_avg_current | number | App rows: average rating of the current version. |
rating_count_current | integer | App rows: rating count of the current version. |
hist_1 | integer | App rows: number of 1-star ratings. |
hist_2 | integer | App rows: number of 2-star ratings. |
hist_3 | integer | App rows: number of 3-star ratings. |
hist_4 | integer | App rows: number of 4-star ratings. |
hist_5 | integer | App rows: number of 5-star ratings. |
written_reviews | integer | App rows: how many written reviews the storefront holds (ratings without text are not reviews). |
released | string | App rows: first release date. |
updated | string | App rows: current version's release date. |
icon | string | App rows: icon image URL. |
app_url | string | App rows: the App Store page. |
reviews_fetched | integer | App rows: how many reviews were read for the app on that storefront before filtering. |
status | string | Status rows: ok, no_reviews, not_found or error. |
target | string | Status rows: the id, URL or name this job was given. |
reviews | integer | Status rows: review rows delivered. |
filtered | integer | Status rows: reviews dropped by minRating, maxRating, sinceDate or requireText, never charged. |
pages | integer | Status rows: pages of reviews read. |
duplicates | integer | Status rows: 1 when another input already covered this app on this storefront. |
error | string | Status rows: why the job failed, null when it did not. |
fetched_at | string | UTC timestamp when the row was fetched. |
Example row from a real run
Taken from the actor's README — this is the shape your dataset holds.
{
"type": "review",
"review_id": "13657831289",
"app_id": "310633997",
"app_name": "WhatsApp Messenger",
"country": "us",
"rating": 5,
"title": "WhatsApp not bad",
"text": "WhatsApp's not bad at all—it's actually great for what it does. End-to-end encryption keeps your actual messages and calls private…",
"author": "Ed Bradway",
"version": null,
"is_edited": false,
"review_date": "2026-01-21T04:48:38Z",
"review_day": "2026-01-21",
"developer_response": null,
"developer_response_date": null,
"url": "https://apps.apple.com/us/app/id310633997?see-all=reviews",
"fetched_at": "2026-08-29T06:10:04+00:00"
}
Apple app store reviews scraper in Python, JavaScript and curl
Python
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/app-store-reviews-scraper").call(run_input={
"appIds": ["310633997"], "countries": ["us", "gb", "de"],
"sort": "most_recent", "maxReviewsPerApp": 100,
})
rows = client.dataset(run["defaultDatasetId"]).list_items().items
reviews = [r for r in rows if r["type"] == "review"]
for r in reviews[:5]:
print(r["country"], r["rating"], r["title"], "—", (r["text"] or "")[:80])
JavaScript (Node)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('kestrel/app-store-reviews-scraper').call({
appNames: ['Duolingo'], countries: ['us'], sort: 'most_critical', maxRating: 2, maxReviewsPerApp: 300,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const complaints = items.filter(r => r.type === 'review');
console.log(complaints.length, 'low-star reviews');
curl
curl -X POST "https://api.apify.com/v2/acts/kestrel~app-store-reviews-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>&timeout=300" \
-H "Content-Type: application/json" \
-d '{"appIds":["310633997"],"countries":["us"],"sinceDate":"7 days","sort":"most_recent","maxReviewsPerApp":200}'
The same three calls are runnable files in the examples repository: examples/app-store-reviews-scraper.py|.js|.sh.
Limits and honest notes
- Depth. Apple's endpoint pages ten reviews at a time by offset and keeps answering deep into an app's history (tens of thousands for the biggest apps). A run's
maxReviewsPerAppand its spending limit decide when to stop; 0 means "everything Apple pages out". - Ratings versus reviews. An app's
rating_countcounts star ratings, most of which have no text. Only written reviews are pageable;written_reviewson theapprow is how many the storefront holds. - Version. Apple's current endpoint does not always attach the app version to a review; the column is null when it is absent.
- Pace. Apple answers roughly one request a second per IP and replies 429 beyond that. The fetcher paces each session and rotates on 429, so a big run is slower with one IP than with several — use Apify Proxy for anything over a few hundred rows.
- Names.
appNamestakes the top search hit on each storefront; when a name is ambiguous, use the id.
FAQ
Does it need an Apple developer account or API key?
No. Everything is read from public endpoints. The App Store Connect API gives developers their own app's reviews only; this reads any app's.
Can I download App Store reviews as CSV or Excel?
Yes. Every run writes an Apify dataset; open it in the Console and export as CSV, Excel, JSON or XML, or fetch it from the API with ?format=csv. This is the download app store reviews and app store reviews csv path — an app store reviews export with no extra step.
How do I get only App Store negative reviews?
Set maxRating to 2 (or 1), sort to most_critical, and optionally requireText. Five-star reviews are dropped before billing.
Can I scrape reviews for many countries at once?
Yes — that is the point of countries. Each storefront gets its own app row and its own review rows, all in one dataset with a country column, which makes app store reviews by country a pivot rather than a project.
How many reviews can one app return?
As many as Apple pages out, which for large apps is tens of thousands per storefront. The old RSS feed stopped at 500; this does not use it.
Does it include the developer's reply?
Yes — developer_response and developer_response_date on the review row, which makes an app store developer response audit a filter.
Can I use it as a general app review API?
Yes: call run-sync-get-dataset-items and you have an app store review api that returns JSON for any app and storefront. Pair it with a Google Play reviews source to cover both stores.
Can I scrape App Store reviews without an API?
That is what this is: scrape app store reviews with a JSON input and get rows back, no Apple API, no browser.
What does bulk work cost?
Rows delivered times the per-row price, nothing for the rest. A filtered run pays for what it keeps. For competitor app reviews across a portfolio, cap per app and per storefront and let a schedule spread the work.
Which order should I schedule?
most_recent with a relative sinceDate. Every other order is for one-off pulls.
How to use Apple App Store Reviews Scraper
Related scrapers
Run Apple App Store Reviews Scraper
The actor lives on the Apify Store, where the pricing tab carries the live per-row price and the input form has every field above: apify.com/kestrel/app-store-reviews-scraper