# App Store Reviews Scraper (`scrapyx/appstore-reviews-scraper`) Actor

Apple App Store customer reviews across as many storefronts as you name. Apple caps every app at 500 reviews per storefront and sort, but each storefront holds a completely different set - so widening the country list is the only way past it, and the actor reports which pairs hit the ceiling.

- **URL**: https://apify.com/scrapyx/appstore-reviews-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 1,000 results

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

Customer reviews from Apple's **App Store** — rating, title, body, app version,
author and vote counts — across as many storefronts as you name. HTTP-only, no
API key, no login, no browser.

### The one thing to understand before you run it

**Apple serves a hard maximum of 500 reviews per (app × storefront × sort).**
Page 10 is the last page that exists; page 11 answers HTTP 400. No parameter
lifts it. An app with three million reviews will hand back five hundred.

**But every storefront holds a completely different set of reviews.** Measured
on WhatsApp, page 1, same minute:

| storefront | reviews | overlap with US |
| --- | ---: | ---: |
| us | 50 | — |
| gb | 50 | **0** |
| de | 50 | **0** |
| jp | 50 | **0** |
| id | 50 | **0** |
| br | 50 | **0** |

…each in that storefront's own language. So widening `countries` is the only
way past 500, and reach is roughly **500 × countries × sorts**. The summary
publishes `theoreticalMaxForThisGrid` so you can see what your grid can reach
before you run it, and `ceilingsHit` names every pair that ran all ten pages
full — meaning there are more reviews upstream that the endpoint will not serve
at any page number.

### Three more upstream quirks it corrects

#### 1. Only two sorts work, and the broken ones return HTTP 500

| `sortBy` | Result |
| --- | --- |
| `mostRecent` | 200, 50 reviews |
| `mostHelpful` | 200, 50 reviews |
| **`mostFavorable`** | **HTTP 500** |
| **`mostCritical`** | **HTTP 500** |
| `nosuchsort` | HTTP 500 |

`mostFavorable` and `mostCritical` look like supported values, and they fail
with the same 500 a genuine outage produces. A client that retried would burn
its budget on something that can never work; one that reported "upstream is
down" would be wrong. Both are refused locally, by name, with an explanation.

The two working sorts return **near-disjoint** sets — their first pages shared
nothing at all, though a full walk of one storefront turned up a single review
under both. Deduped by `(country, reviewId)` and counted, not assumed away.

#### 2. An unknown app id returns HTTP 200 with an empty feed

Exactly what a real app with no reviews in that storefront returns. There is no
way to tell them apart from the feed alone — so the actor resolves the app
through Apple's lookup endpoint first and reports `appResolved`. A run that
comes back empty with `appResolved: false` almost certainly has a wrong id.

#### 3. Every value is wrapped

`im:rating` is `{"label": "5"}` — a string inside an object. `author.name` is
`{"label": "..."}` while `author.label` is an empty string that looks like it
should hold the name. Nothing is read directly; every field is unwrapped and
coerced, and a changed wrapper costs one field rather than the run.

### Output

One `SEARCH_SUMMARY` per run, one `REVIEW` per review, one `ERROR` per
storefront/sort pair that failed.

`REVIEW`: `appId`, `appName`, `country`, `sortUsed`, `reviewId`, `title`,
`review`, `rating`, `appVersion`, `authorName`, `authorUrl`, `updatedAt`,
`voteSum`, `voteCount`, `pageFetched`, `resultRank`.

`SEARCH_SUMMARY` adds `appResolved`, `reviewsPerCountry`, `ceilingsHit`,
`emptyStorefronts`, `theoreticalMaxForThisGrid`, `duplicateReviewsDropped` and
a `ratingHistogram`.

### Limits

- **500 per app × storefront × sort**, structurally. Widen `countries` to reach
  more.
- Reviews are per-storefront and in that storefront's language — a multi-country
  run is deliberately multilingual.
- Apple ships a vote **sum** and a vote **count**, not a helpful/unhelpful
  split.
- No WAF on this host; a proxy is offered but was not needed for any of the
  reconnaissance.

# Actor input Schema

## `appId` (type: `string`):

A numeric id like 310633997, an `id310633997` form, or a full apps.apple.com URL. An id that does not exist returns an EMPTY feed rather than an error — identical to a real app with no reviews — so the actor resolves the app name first and reports appResolved.

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

Two-letter storefront codes. THIS IS THE IMPORTANT ONE: Apple caps every (app × storefront × sort) at 500 reviews, and each storefront holds a COMPLETELY different set — us, gb, de, jp, id and br shared not one review — so widening this list is the only way past the ceiling. Reviews come back in each storefront's own language.

## `sorts` (type: `array`):

Only mostRecent and mostHelpful work. mostFavorable and mostCritical LOOK supported and return HTTP 500 — indistinguishable from a real outage — so they are refused here rather than retried. Running both working sorts roughly doubles reach.

## `maxResults` (type: `integer`):

Set 0 for unlimited. The cap is divided across the storefront × sort grid so one storefront cannot consume all of it. Note the structural maximum is 500 per pair whatever you set here.

## `maxConcurrency` (type: `integer`):

Storefront/sort pairs in flight at once. Each pair is an independent walk.

## `minRequestInterval` (type: `integer`):

Politeness pacing shared across all workers. 0 uses the built-in default. Reconnaissance saw no interstitials at all.

## `proxyConfiguration` (type: `object`):

Apify Proxy on the shared datacenter pool. This is the default because it is included in your plan at no extra cost and this target works through it. If you start seeing blocks, challenges or empty results, switch the group here to Residential -- it uses real consumer IPs and gets through more, but Apify bills residential traffic per gigabyte, so leave it off unless you need it.

## Actor input object example

```json
{
  "appId": "310633997",
  "countries": [
    "us",
    "gb",
    "de",
    "jp"
  ],
  "sorts": [
    "mostRecent",
    "mostHelpful"
  ],
  "maxResults": 500,
  "maxConcurrency": 4,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "appId": "310633997",
    "countries": [
        "us"
    ],
    "sorts": [
        "mostRecent"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/appstore-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 = {
    "appId": "310633997",
    "countries": ["us"],
    "sorts": ["mostRecent"],
}

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/appstore-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 '{
  "appId": "310633997",
  "countries": [
    "us"
  ],
  "sorts": [
    "mostRecent"
  ]
}' |
apify call scrapyx/appstore-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/appstore-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/AeZ13tjwJeK1PAzfp/builds/fvimU05U8SdjED1QL/openapi.json
