# App Store Reviews Scraper — Every Country, Not Just US (`cleanfeed/app-store-reviews-scraper`) Actor

Scrape Apple App Store reviews for any app, in any country store. Get rating, title, body, author, app version and date as JSON or CSV. Apple's feed is per-country and most tools return only US reviews — this one reports every market you ask for, including the ones with none.

- **URL**: https://apify.com/cleanfeed/app-store-reviews-scraper.md
- **Developed by:** [Yaniv van der Stigchel](https://apify.com/cleanfeed) (community)
- **Categories:** E-commerce, Automation, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.05 / 1,000 review returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## App Store Reviews Scraper — verify it fetched the right app

Every row names the app **as Apple returns it for the id you passed** — name,
developer, bundle id and store URL. Ask for Spotify, see Spotify in row one. See
something else and you know the run is wrong immediately, rather than after
loading five thousand rows into a spreadsheet.

That sounds unremarkable until you read this category's reviews. *"Keeps
scraping the example app (candycrush) even if I update the app ID"* (1.2 stars,
527 users). *"kept spewing out the same incorrect reviews"*. *"defaults to
retrieving snapchat and facebook reviews"*. Five Actors across four publishers
return the wrong app's reviews, and it goes unnoticed because review text never
says which app it belongs to.

### What it does

- Scrape App Store reviews for any app, in any country store
- Prove which app the reviews belong to, from Apple, on every row
- Group complaints by release: `appVersion`, `latestVersion` and `isLatestVersion`
- Filter stale feedback with `reviewAgeDays` — no date arithmetic
- Compare markets, and see which country stores had no reviews at all
- Export to JSON or CSV, or run it on a schedule

### Three things this category gets wrong

**Wrong app, confidently.** Fixed by resolving identity from Apple's lookup and
stamping it on every row, success or failure. If `appName` is not your app, the
run is wrong, and you can see it in row one.

**Silent truncation.** Apple's public feed serves at most **500 reviews per app
per country store**, newest first. That number is printed in the log before any
work starts, a warning names every app and country that reaches it, and
`SUMMARY` carries `pairsTruncatedByAppleCap` with the pairs listed. When nothing
was truncated the summary says so too, so a complete result is confirmed rather
than assumed.

**Missing markets.** There is no country allow-list and no curated dropdown.
Any two-letter ISO code goes straight to Apple — `cn`, `hk`, `tw`, `kz`, `is`,
whatever you need. No store can be absent because we did not think of it.

### No duplicate reviews

Apple's pages shift while a run is in progress, which is how duplicates get into
other tools. Every review is deduplicated on Apple's own `reviewId`, which is
stable across runs — so it also works as a primary key for merging today's run
into yesterday's without double-counting.

### Why per-country matters

Ask for `us`, `gb` and `de` for the same app and you routinely get three
different pictures: different star averages, different top complaints, and
sometimes an empty store. A single-country tool cannot show you that, and worse,
it cannot tell you it did not look.

Every row carries its `country`, so the split is checkable rather than trusted.

### You only pay for reviews

| `errorCode` | Meaning | Charged |
|---|---|---|
| — (`success: true`) | A review row | Yes |
| `no-reviews-in-country` | App exists, nobody reviewed it in that store | No |
| `app-not-found` | No such app id, or Apple refused | No |
| `unparseable-app-id` | Input was not an App Store id or URL | No |
| `unparseable-country` | Country was not a two-letter code | No |

An empty country store is a real answer and one of the more useful ones. It is
reported as a row and never billed.

### Input

| Field | Required | Description |
|---|---|---|
| `apps` | yes | Numeric ids (`324684580`) or any `apps.apple.com` URL |
| `countries` | no | Two-letter codes. Default `["us"]` |
| `maxReviewsPerApp` | no | Per app *and* country. Default 500, which is Apple's cap |

```json
{
  "apps": ["324684580", "https://apps.apple.com/us/app/duolingo/id570060128"],
  "countries": ["us", "gb", "de", "jp"],
  "maxReviewsPerApp": 200
}
```

### Output

Every row has the same fields whether it succeeded or failed, so you can select
columns without branching.

| Field | Type | Description |
|---|---|---|
| `success` | boolean | True when the row carries a review |
| `appId` | string | App Store numeric id, as you asked for it |
| `appName` | string | The app's name **as Apple has it** — your check that the run returned the right app |
| `developer` | string | Publisher, from the store |
| `storeRating` | number | Overall rating in this country store |
| `storeRatingCount` | integer | How many ratings that average covers |
| `bundleId` | string | Reverse-DNS id (`com.spotify.client`) — the join key to Google Play and analytics |
| `appUrl` | string | The store page, to confirm the app by eye in one click |
| `country` | string | Country store this review came from |
| `reviewId` | string | Apple's stable id — deduplicates within and across runs |
| `rating` | integer | 1 to 5 |
| `title` | string | Review headline |
| `body` | string | Full review text |
| `authorName` | string | App Store nickname; Apple publishes no real name |
| `appVersion` | string | The build the review was written against |
| `latestVersion` | string | The build shipping now, from Apple |
| `isLatestVersion` | boolean | Whether the review is about the current release. Null when either version is unknown |
| `updatedAt` | string | ISO 8601 timestamp |
| `reviewAgeDays` | integer | Whole days old at run time |
| `voteSum` | integer | Net helpfulness votes |
| `voteCount` | integer | Total helpfulness votes |
| `errorCode` | string | Null on success |
| `errorMessage` | string | Human-readable explanation |

```json
{
  "success": true,
  "appId": "324684580",
  "appName": "Spotify: Music and Podcasts",
  "developer": "Spotify",
  "storeRating": 4.77467,
  "storeRatingCount": 41883904,
  "bundleId": "com.spotify.client",
  "appUrl": "https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580?uo=4",
  "country": "us",
  "reviewId": "14504656715",
  "rating": 1,
  "title": "Ads are getting insane",
  "body": "It got to the point where after 2 songs, 2 one-minute ads would play.",
  "authorName": "Amber10122012333",
  "appVersion": "9.1.78",
  "latestVersion": "9.1.78",
  "isLatestVersion": true,
  "updatedAt": "2026-09-03T00:01:34-07:00",
  "reviewAgeDays": 1,
  "voteSum": 0,
  "voteCount": 0,
  "errorCode": null,
  "errorMessage": null
}
```

### The run summary

`SUMMARY` in the key-value store answers "what did this run achieve", per app
**and** per country store — not just in total:

```json
{
  "requestedApps": 1,
  "requestedCountries": 2,
  "pairs": 2,
  "delivered": 500,
  "emptyCountries": 1,
  "notFound": 0,
  "maxReviewsPerAppPerCountry": 500,
  "appleFeedCapPerAppPerCountry": 500,
  "pairsTruncatedByAppleCap": 1,
  "truncationNote": "1 app/country pair(s) reached Apple's cap of 500 reviews per country store, newest first: 324684580/us. Older reviews exist but the public feed does not serve them.",
  "byAppAndCountry": [
    {
      "appId": "324684580",
      "appName": "Spotify: Music and Podcasts",
      "developer": "Spotify",
      "country": "us",
      "reviewsDelivered": 500,
      "appleCapHit": true,
      "storeRating": 4.77467,
      "sampleAverageRating": 2.31,
      "ratingDrift": -2.46,
      "ratingBreakdown": { "1": 231, "2": 47, "3": 38, "4": 41, "5": 143 },
      "newestReview": "2026-09-04T05:12:03-07:00",
      "oldestReview": "2026-08-27T11:40:19-07:00",
      "versionsCovered": ["9.1.76", "9.1.78"],
      "latestVersion": "9.1.78",
      "errorCode": null
    }
  ]
}
```

`ratingDrift` is the sampled average minus the lifetime store rating. Negative
means the newest reviews are worse than the app's history — the reading a star
rating on its own cannot give you.

### Use it from an AI agent (MCP)

This Actor is callable as a tool through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), so Claude, ChatGPT, Cursor and VS Code can run it directly.

Add the server to your MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com",
      "headers": {
        "Authorization": "Bearer <YOUR_APIFY_TOKEN>"
      }
    }
  }
}
```

Then ask for what you want in plain language — for example *“get the App Store reviews for this app in the UK and German stores”* — and the agent calls `cleanfeed/app-store-reviews-scraper` with the right input. Every output field is described in the dataset schema, so the agent knows what it is getting back before it runs anything.

### Call it from code

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("cleanfeed/app-store-reviews-scraper").call(run_input={
    "apps": ["324684580"],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["success"]:
        print(item)
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });

const run = await client.actor('cleanfeed/app-store-reviews-scraper').call({
    apps: ["324684580"],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.success));
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/cleanfeed~app-store-reviews-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"apps": ["324684580"]}'
```

### Related actors

| If you need | Use |
|---|---|
| Comments on a product's YouTube videos | [YouTube Comments Scraper](https://apify.com/cleanfeed/youtube-comments-downloader) |
| Contact details for the company behind an app | [Verified Website Contacts](https://apify.com/cleanfeed/verified-website-contacts) |

### Limitations

- **Apple caps the public feed at 500 reviews per app per country**, newest first — ten pages of fifty, with no way to page beyond it. The cap is printed in the run log before any work starts, warned about per app and country when it is reached, and reported in `SUMMARY` as `pairsTruncatedByAppleCap` with the pairs named. When it is *not* reached, `SUMMARY` says so too, which means a full result is confirmed rather than assumed. Anything promising an app's complete review history is not using this source.
- **Reviews are per-country, not per-language.** A German-language review left from a US account appears in the `us` store. An app with no reviews in a country returns `errorCode: no-reviews-in-country`, which is a real answer and never charged.
- **Newest first is the only order.** Apple's public feed offers no most-helpful or most-critical sort, so `maxReviewsPerApp` always trims the oldest reviews, never a random sample.
- **No developer responses.** Apple's public feed carries the review only.
- **Author names are App Store nicknames**, not real names, and should not be treated as identifying a person.
- **A star rating with no text is a real review** and returns with a null `title` and `body`.
- **`isLatestVersion` is null when either version is unknown.** An unknown comparison is reported as unknown rather than guessed as false.

### FAQ

#### How do I know it fetched the right app?

Because every row carries `appName`, `developer`, `bundleId` and `appUrl` **as Apple returns them for the id you passed**, never echoed from your input. Ask for Spotify and see Spotify in row one. See something else and the run is wrong, visibly, in the first row. This is the category's most common failure — competing Actors have reviews reading *"Keeps scraping the example app (candycrush) even if I update the app ID"* — and it is invisible in a plain review dump because review text never names the app it belongs to.

#### Will I silently get fewer reviews than I asked for?

No. Apple's feed stops at 500 per app per country store. That number is printed in the log before the run starts, a warning names every app and country that reached it, and `SUMMARY` carries `pairsTruncatedByAppleCap` plus a plain-English `truncationNote`. If nothing was truncated the note says that as well, so you know the result is complete rather than hoping it is.

#### Are there duplicate reviews?

No. Apple's pages shift while a run is in progress, which is how duplicates get into other tools. Every review is deduplicated on Apple's own `reviewId`, which is stable across runs — so you can also use it as a primary key to merge today's run into yesterday's without double-counting.

#### Which country codes can I use?

Any two-letter ISO code Apple runs a store in — `us`, `gb`, `de`, `fr`, `jp`, `br`, `in`, `cn`, `hk`, `tw`. There is no allow-list in the code and no dropdown of curated markets, so no store can be missing because we forgot it. A code Apple does not run returns `errorCode: unparseable-country` or `no-reviews-in-country` rather than being silently dropped.

#### How do I group complaints by release?

Each row carries `appVersion` (the build the review was written against), `latestVersion` (what is shipping now, from Apple) and `isLatestVersion`. Filter to `isLatestVersion: true` for feedback on what you just released, or group by `appVersion` to see which build a complaint arrived with. `SUMMARY` lists `versionsCovered` per app and country.

#### What does the run summary tell me?

Per app **and** country: reviews delivered, the app Apple resolved, the star breakdown, the newest and oldest review in the sample, the versions covered, whether Apple's cap was hit, and `ratingDrift` — the sampled average minus the lifetime store rating. A negative drift means the newest reviews are worse than the app's history, which a star rating on its own cannot tell you.

#### How do I find an app's id?

It is the number after `/id` in any App Store URL. You can paste the whole URL instead and the id is extracted for you.

#### Why did a country return no reviews?

Because nobody has reviewed the app in that store. This is common for apps with one strong home market, and it is exactly the finding per-country reporting exists to surface. You are not charged for it.

#### Why not just query the US store?

Because ratings and complaints genuinely differ by market. Ask for `us`, `gb` and `de` for the same app and you routinely get three different pictures. A single-country tool cannot show you that, and cannot tell you it did not look.

#### Can I get Google Play reviews too?

Not from this Actor. Play is a separate source with a different shape, and merging them behind one schema would hide which store a row came from.

#### Do I need an Apple developer account?

No. This reads Apple's public customer-reviews feed, which needs no key and no login.

### Notes

Only Apple's public customer-reviews feed is read — the same data any visitor
sees on an app's App Store page. No developer account, no key, no login, and
nothing that is not already published.

# Actor input Schema

## `apps` (type: `array`):

App Store apps: numeric ids such as 324684580, or any apps.apple.com URL. The id is the number after /id in the URL.

## `countries` (type: `array`):

Two-letter country codes (us, gb, de, jp, cn, hk). Apple's review feed is per-country: a review written in Germany appears only in de. There is no allow-list here, so any store Apple runs is reachable. A country with no reviews is reported, not hidden, and not charged.

## `maxReviewsPerApp` (type: `integer`):

Newest first. You are charged per review returned, so this is your cost ceiling. Apple's public feed is hard-capped at 500 per app per country store; when a run reaches it the log warns and SUMMARY reports which pairs were truncated.

## Actor input object example

```json
{
  "apps": [
    "324684580"
  ],
  "countries": [
    "us",
    "gb",
    "de"
  ],
  "maxReviewsPerApp": 500
}
```

# Actor output Schema

## `reviews` (type: `string`):

One row per review: rating, title, body, author, app version, review age, country — plus the app name, developer and bundle id Apple returns for the id you asked for.

## `summary` (type: `string`):

Per app and country: reviews delivered, star breakdown, versions covered, newest and oldest review, rating drift against the store rating, and whether Apple's 500-review cap was reached.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "apps": [
        "324684580"
    ],
    "countries": [
        "us",
        "gb",
        "de"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanfeed/app-store-reviews-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "apps": ["324684580"],
    "countries": [
        "us",
        "gb",
        "de",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("cleanfeed/app-store-reviews-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "apps": [
    "324684580"
  ],
  "countries": [
    "us",
    "gb",
    "de"
  ]
}' |
apify call cleanfeed/app-store-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cleanfeed/app-store-reviews-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/PeDqiZwaln1M6zihT/builds/WdhVJJi1nMe5H50yg/openapi.json
