kestrel actors docs

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

EventWhat it isPriceCharged for
reviewReview row$0.004One 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.

FieldTypeDefaultWhat it does
appIdsarray[]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.
startUrlsarray[]App pages, e.g. https://apps.apple.com/us/app/whatsapp-messenger/id310633997 — the id is read from the URL for you.
appNamesarray[]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.
countriesarray["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.
maxReviewsPerAppinteger1000 = 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.
sortstring (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.
minRatinginteger00 = keep every review. 1-5 = keep only reviews at or above this many stars. Filtered reviews are never charged. Range 0-5.
maxRatinginteger00 = 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.
sinceDatestring""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.
requireTextbooleanfalseDrop reviews that are a star rating with no words, so you deliver and pay for only the ones that say something.
includeAppRowbooleantrueAlso emit one free row per app and storefront: name, developer, genre, price, version, average rating, rating count and the 1-5 star histogram.
sessionsinteger4How many proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately. Range 1-16.
perIpnumber1Pace for each session. Apple answers about 1/s per IP; faster earns 429 replies and retries, not speed. Range 0.1-3.
proxyConfigurationobject{"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.

FieldTypeWhat it holds
typestringRow type: review, app or status.
app_idstringApple's numeric app id.
app_namestringApp name on that storefront.
countrystringStorefront the row came from (two-letter code).
review_idstringApple's id for the review.
ratingintegerStars the reviewer gave, 1-5.
titlestringReview headline.
textstringThe review body.
authorstringReviewer's public nickname.
versionstringApp version the review was written against, when Apple reports it.
is_editedbooleanTrue when the reviewer edited the review after posting.
review_datestringWhen the review was posted (ISO 8601, UTC).
review_daystringThe same as a plain YYYY-MM-DD date, for filtering and charts.
developer_responsestringThe developer's public reply, when there is one.
developer_response_datestringWhen the developer replied.
urlstringThe app's reviews page on that storefront.
developerstringApp rows: developer (artist) name.
sellerstringApp rows: seller of record.
bundle_idstringApp rows: bundle identifier.
genrestringApp rows: primary genre.
pricenumberApp rows: price on that storefront (0 = free).
currencystringApp rows: currency of the price.
age_ratingstringApp rows: content advisory rating, e.g. 12+.
rating_avgnumberApp rows: average star rating on that storefront.
rating_countintegerApp rows: how many ratings the app has there.
rating_avg_currentnumberApp rows: average rating of the current version.
rating_count_currentintegerApp rows: rating count of the current version.
hist_1integerApp rows: number of 1-star ratings.
hist_2integerApp rows: number of 2-star ratings.
hist_3integerApp rows: number of 3-star ratings.
hist_4integerApp rows: number of 4-star ratings.
hist_5integerApp rows: number of 5-star ratings.
written_reviewsintegerApp rows: how many written reviews the storefront holds (ratings without text are not reviews).
releasedstringApp rows: first release date.
updatedstringApp rows: current version's release date.
iconstringApp rows: icon image URL.
app_urlstringApp rows: the App Store page.
reviews_fetchedintegerApp rows: how many reviews were read for the app on that storefront before filtering.
statusstringStatus rows: ok, no_reviews, not_found or error.
targetstringStatus rows: the id, URL or name this job was given.
reviewsintegerStatus rows: review rows delivered.
filteredintegerStatus rows: reviews dropped by minRating, maxRating, sinceDate or requireText, never charged.
pagesintegerStatus rows: pages of reviews read.
duplicatesintegerStatus rows: 1 when another input already covered this app on this storefront.
errorstringStatus rows: why the job failed, null when it did not.
fetched_atstringUTC 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 maxReviewsPerApp and its spending limit decide when to stop; 0 means "everything Apple pages out".
  • Ratings versus reviews. An app's rating_count counts star ratings, most of which have no text. Only written reviews are pageable; written_reviews on the app row 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. appNames takes 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

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