# App Store Review History Scraper - All Storefronts (`linus009400/app-store-review-history`) Actor

Apple caps App Store reviews at 500 per app per country. This Actor sweeps up to 175 storefronts, de-duplicates by review ID, normalizes timestamps to UTC and reaches far older reviews.

- **URL**: https://apify.com/linus009400/app-store-review-history.md
- **Developed by:** [HEESEOK HONG](https://apify.com/linus009400) (community)
- **Categories:** SEO tools, E-commerce, Developer tools
- **Stats:** 3 total users, 2 monthly users, 61.1% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.06 / 1,000 reviews

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 Review History Scraper — All Storefronts

Apple stops at **500 reviews per app per country**. Ten pages of fifty. Page eleven does not exist, no matter which scraper you use or how much you pay for it.

For a popular app that means the US storefront holds only the last few days.

This Actor gets past that by sweeping **up to 175 storefronts**, de-duplicating by review ID, and giving you back one clean, chronological dataset.

### The measurement that explains the whole product

WhatsApp Messenger (`310633997`), collected 2026-08-11:

| Storefront | Reviews available | Distinct app versions in them |
|---|---|---|
| United States | 500 (Apple's cap) | **3** |
| Mongolia | 46 (all of them) | **37** |

Large storefronts are a firehose with a short memory. Small storefronts are a trickle that remembers everything.

And the sweep compounds. One run across 44 storefronts of the same app returned **16,427 reviews covering 508 distinct app versions**. The US storefront, on its own, offers three.

If you want reviews from the version you shipped last spring, they are not in the US feed. They are scattered across the small storefronts, and the only way to reach them is to sweep.

### What you get per review

| Field | Notes |
|---|---|
| `reviewId` | unique across the whole run; duplicates are dropped |
| `rating`, `title`, `body` | rating is a number, not Apple's string |
| `appVersion` | the version the reviewer was actually running |
| `isCurrentVersion` | joined against live app metadata; `null` when unknown |
| `updatedAt` | **normalized to UTC** — 175 storefronts otherwise means 175 offsets |
| `updatedAtLocal` | Apple's original string, kept so you can check |
| `country`, `countryName`, `storefrontId` | `countryName` in English |
| `authorName`, `authorId`, `authorUrl` | read from the right field (see below) |
| `voteSum`, `voteCount`, `contentType`, `reviewUrl` | |
| `raw` | Apple's untouched record, so you can verify every field above |

An `appProfile` record per app carries name, seller, bundle ID, genres, current version and its release date, price, content rating, and the storefront-level rating average and count. It lands in the run's `app-profiles` dataset, separate from the reviews, so neither table has empty columns in it.

### Coverage is measured, not promised

Every run produces a `RUN_SUMMARY`, and it is **free** — you are not charged for it. You will find it in the run's `run-summary` dataset and in the key-value store, and its headline numbers are printed in the log. It is kept out of the main dataset so your review table stays a clean table of reviews:

```json
{
  "itemType": "RUN_SUMMARY",
  "reviewsCollected": 16427,
  "countriesRequested": 175,
  "countriesWithReviews": 44,
  "skippedCountries": [{ "country": "bt", "reason": "noReviews" }],
  "partialCountries": [],
  "reviewsByCountry": { "us": 500, "gb": 500, "mn": 46 },
  "distinctAppVersions": 508,
  "duplicatesRemoved": 0,
  "apiCalls": 340,
  "rateLimitSignals": 0,
  "stoppedBy": null
}
```

No Actor can promise 175 responding storefronts, because it depends on where the app is sold, and because Apple throttles hard sweeps. So this one reports exactly which storefronts answered, which returned nothing, and which were cut short — on every run.

`stoppedBy` is `null` unless one of your own limits ended the run early. There is no second, hidden cap — a short result set always has a stated reason.

### Input

| Field | Default | |
|---|---|---|
| `appIds` | — | Numeric IDs (`310633997`) or App Store URLs. The ID is extracted for you. |
| `countries` | `ALL` | Storefront codes, or `ALL` for the full sweep. |
| `sortBy` | `mostrecent` | `mostrecent` or `mosthelpful`. |
| `dateFrom` | — | Drop older reviews. With `mostrecent` the Actor also **stops paging a storefront as soon as a whole page predates your cutoff**, which lowers your bill. |
| `maxReviews`, `maxReviewsPerCountry`, `maxApiCalls`, `concurrency` | | Every one is reported back in `RUN_SUMMARY`. |

### Pricing

Pay per result. **$0.12 per 1,000 reviews.** App profiles are $0.002 each. **There is no start fee**, so a small test run costs you cents.

### Notes on the source

Apple's public RSS feed needs no API key and no account — yours or ours. It is also inconsistent in ways that quietly corrupt naive scrapers, and each one is handled here and covered by a test:

- an unknown app ID returns HTTP 200 with a feed whose `entry` key is **absent**, not empty
- the `next` link over-reports and walks past the end of the data forever; only `last` is honest
- every pagination link points at an XML endpoint, so following one returns XML, not JSON
- a storefront with exactly one review returns an object where every other storefront returns an array
- `author.label` is always an empty string; the real name is one level down

Apple throttles with **HTTP 403**, not only 429 — measured at roughly 14 requests per second, after 340 requests. So requests are paced across all workers, the pacing widens automatically whenever Apple pushes back and narrows again once responses are clean, and a throttled storefront is retried on a second, slower pass. If it still will not answer, it is reported in `skippedCountries` and the rest of the sweep continues.

Reviews are not shared between storefronts, so measured cross-storefront duplication is zero. De-duplication is a guarantee here, not a feature — it means a `reviewId` never appears twice in your dataset even when you re-run or overlap storefronts.

***

Questions or a field you need added: open an issue on the Actor's Issues tab.

# Actor input Schema

## `appIds` (type: `array`):

One entry per app. Accepts a numeric App Store id (310633997) or a full App Store URL (https://apps.apple.com/us/app/id310633997) - the id is extracted for you. Left empty, the Actor demonstrates itself on WhatsApp Messenger.

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

Two-letter storefront codes such as us, gb, jp, mn. Use ALL to sweep every one of the 175 catalogued storefronts - that is what reaches older reviews, because Apple stops each single storefront at 500. A sweep of every storefront takes a few minutes; a single storefront takes seconds. Storefronts that return nothing are listed in RUN\_SUMMARY.skippedCountries.

## `sortBy` (type: `string`):

mostrecent returns newest reviews first and is required for the Reviews since cutoff to stop paging early. mosthelpful returns Apple's helpfulness ranking instead.

## `dateFrom` (type: `string`):

Optional cutoff, for example 2025-01-01. Reviews older than this are dropped. With Most recent sorting the Actor also stops paging a storefront as soon as a whole page predates the cutoff, which lowers your cost.

## `includeAppProfile` (type: `boolean`):

Emit one app metadata record per app (name, seller, current version, release dates, average rating). Turn this off if you only want review rows.

## `maxReviews` (type: `integer`):

Hard stop on the total number of reviews collected per app. The run ends cleanly and RUN\_SUMMARY.stoppedBy records that this limit was the reason. There is no second hidden cap.

## `maxReviewsPerCountry` (type: `integer`):

Apple itself stops at 500 per app per storefront (10 pages of 50), so 500 is the real ceiling. Lower it to sample many storefronts cheaply.

## `maxApiCalls` (type: `integer`):

Hard stop on requests sent to Apple, counting both review pages and metadata lookups. Protects against a runaway sweep.

## `concurrency` (type: `integer`):

How many storefronts are fetched at the same time. Apple throttles with HTTP 403, not only 429: a measured sweep at roughly 14 requests per second was throttled after 340 requests. 2 is the tested default. Raising it makes throttling more likely, and the Actor then slows itself down anyway.

## `minRequestIntervalMs` (type: `integer`):

Pacing shared across all concurrent workers. The Actor widens this automatically whenever Apple pushes back and narrows it again once responses are clean. Set 0 to disable pacing, which is only sensible for a one-storefront run.

## Actor input object example

```json
{
  "appIds": [
    "310633997"
  ],
  "countries": [
    "us"
  ],
  "sortBy": "mostrecent",
  "includeAppProfile": true,
  "maxReviews": 100,
  "maxReviewsPerCountry": 500,
  "maxApiCalls": 6000,
  "concurrency": 2,
  "minRequestIntervalMs": 220
}
```

# 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 = {
    "appIds": [
        "310633997"
    ],
    "countries": [
        "us"
    ],
    "maxReviews": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("linus009400/app-store-review-history").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 = {
    "appIds": ["310633997"],
    "countries": ["us"],
    "maxReviews": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("linus009400/app-store-review-history").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 '{
  "appIds": [
    "310633997"
  ],
  "countries": [
    "us"
  ],
  "maxReviews": 100
}' |
apify call linus009400/app-store-review-history --silent --output-dataset

```

## MCP server setup

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

```

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/1OE4hEznh8NXrjFna/builds/X4BlDzwUxscwNT2ci/openapi.json
