# App Store Reviews Scraper (`renzomacar/app-store-reviews-scraper`) Actor

Scrape App Store customer reviews for any iOS app in any country. No API key, no proxy, no login. Star rating, title, body, app version, author and date.

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

## Pricing

from $0.40 / 1,000 review scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 - All Countries, No API Key

Pull customer reviews for any iOS app on the Apple App Store, from any country storefront, without an API key, without a developer account, without App Store Connect access and without a proxy. Give the actor an app ID, an `apps.apple.com` URL or an iOS bundle ID, pick your storefronts, and get back a clean table: star rating, review title, review body, author, date, whether the review was edited, and the developer's public response where one exists.

It reads the same endpoint Apple's own storefront web app uses, anonymously. Nothing to authenticate, nothing to sign. It runs on the Apify free plan and returns data on the free plan.

### Up to ~78,000 reviews per app, not 500

This is the headline, and it is a measured number rather than a claim.

The old public RSS review feed served **50 reviews per page and refused page 11**, a hard ceiling of 500 per app per country. This actor no longer uses that feed. Its depth is now bounded by the app's *actual review count*, not by a policy limit:

| App | Reviews reachable (measured) |
|---|---|
| Facebook | ~78,000 |
| Calm | ~42,000 |
| A 170k-rating app | ~14,000 |

Verified end to end: a single run pulled **1,500 unique reviews from one app in one storefront, zero duplicates**, spanning September 2017 to August 2026. Under the old feed that request would have stopped at 500.

Pagination was checked for integrity, not just for volume: 800 consecutive reviews across 40 pages returned **zero duplicate review IDs**.

### What you get per review

| Field | What it holds | Fill rate measured live |
|---|---|---|
| `reviewId` | Apple's review identifier, stable across runs | 300/300 |
| `appId` / `appName` | The app the review belongs to | 300/300 |
| `country` | Storefront | 300/300 |
| `source` | Which endpoint served this row - see below | 300/300 |
| `rating` | Star rating, 1 to 5, as a number | 300/300 |
| `title` | Review headline | 300/300 |
| `text` | Full review body, HTML entities decoded | 300/300 |
| `userName` | Reviewer's display name | 300/300 |
| `updatedAt` | ISO 8601 timestamp | 300/300 |
| `isEdited` | Whether the reviewer edited it after posting | 300/300 |
| `hasDeveloperResponse` | Boolean, so you can filter without null checks | 300/300 |
| `developerResponseText` / `developerResponseAt` | The publisher's public reply | **6/300 (~2%)** |
| `reviewUrl` | Link back to the app's reviews on Apple | 300/300 |
| `scrapedAt` | Collection timestamp | 300/300 |

#### Three fields are always empty, and you should know why

`appVersion`, `voteSum`, `voteCount` and `userUrl` are **emitted as `null`** on every row from the current source.

The retired RSS feed carried them. Apple's current reviews endpoint does not expose them, and no parameter recovers them - `extend=version`, `extend=appVersion`, `include=app` and an explicit `fields[user-reviews]` request were all tried and all returned the same six attributes.

They are kept in the schema rather than deleted so that rows from the legacy feed (see the fallback section) stay shape-compatible, and so nobody wonders where the column went. If your workflow depended on reading sentiment by app version, that specific analysis is not currently possible from Apple's public reviews data. Saying so is better than shipping you a column of nulls with no explanation.

### The source changed on 2026-08-21, and here is exactly what happened

The `itunes.apple.com/rss/customerreviews` feed went dark. Not down - **dark**. It still answers `HTTP 200`, still returns a complete envelope with `"title":"iTunes Store: Customer Reviews"`, an `author` block, a `rights` block and a same-day `updated` timestamp. It simply publishes **zero review entries**, for every app, every country, in both `/json` and `/xml`.

That was verified across 12 app-and-country combinations from three separate networks, including Apify's cloud, while `lookup`, `search` and the chart feeds kept working normally from the same machines.

An empty `200` is the most dangerous failure a scraper can meet, because the naive reading is "this app has no reviews" - a confidently wrong answer. So this actor does two things about it:

1. **It uses the endpoint Apple's own web storefront uses.** Apple's site bundle declares a same-origin proxy mapping `https://amp-api.apps.apple.com/` to `/api/apps/`, and requests through that proxy need **no bearer token at all**. There is no token to extract, rotate or hardcode, and nothing that expires.
2. **The legacy feed is still tried as an automatic fallback.** If the new endpoint cannot serve an app or storefront, the old RSS feed is attempted before anything is reported as failed - it costs nothing when it is dark, and it carries `appVersion` and vote counts if it ever returns.

Every row carries **`source`**: `amp-api` for the current endpoint, `rss` for the legacy one. You never have to guess which fields to trust on a given row.

### Ordering: there is no sort parameter, and no actor can give you one

Apple's reviews endpoint accepts no sort option. Nine variants were tested - `sort=-date`, `date`, `-rating`, `rating`, `helpful`, `-helpful`, `mostRecent`, `mostHelpful` and no sort at all - and **all nine returned an identical first result**. The parameter is silently ignored.

What you get is Apple's own fixed relevance ordering, which is *not* chronological. Sampling one app confirmed it: offset 0 held reviews from 2025 and 2024, offset 20 from 2021, offset 2,000 from 2017, offset 20,000 back to 2018. Recent reviews are present - the newest review in the first 120 was from July 2026 - but they are scattered rather than at the front.

This is why the old `sortBy` input has been **removed** instead of left in place as a knob that does nothing. In its place there is a filter that actually works:

**`publishedAfter`** - give it a date like `2026-01-01` and only reviews newer than that are delivered and billed. A live test returned 40 rows with the oldest at 2026-01-07. Because Apple does not sort, this filters what was fetched, so a narrow date range on a very large app may need a higher `maxItems` before matches turn up.

### Input

```json
{
  "appIds": [
    "310633997",
    "https://apps.apple.com/us/app/chatgpt/id6448311069",
    "com.burbn.instagram"
  ],
  "countries": ["us", "gb", "de", "jp"],
  "maxItems": 2000,
  "maxReviewsPerApp": 500,
  "minRating": 1,
  "maxRating": 2,
  "publishedAfter": "2026-01-01"
}
```

**App identifiers are flexible on purpose.** All three forms work and can be mixed in one run: numeric ID `310633997`, full URL `https://apps.apple.com/us/app/chatgpt/id6448311069`, or iOS bundle ID `com.burbn.instagram` (resolved through Apple's lookup endpoint automatically).

**Input aliases.** Migrating from another scraper, or driving this from an LLM that guessed the field names? `appIds` also accepts `ids`, `appId`, `apps`, `appUrls` and `urls`. `countries` also accepts `country`, `countryCodes` and `storefronts`. `maxItems` also accepts `maxResults`, `limit` and `maxReviews`. `publishedAfter` also accepts `since` and `reviewsAfter`. A comma separated string like `"us,gb,de"` works anywhere a list does.

#### Every input field

- **`appIds`** - the apps to scrape. Required.
- **`countries`** - two letter iTunes storefronts. Defaults to `["us"]`. Verified on `us`, `gb`, `de`, `jp`, `br`, `pe`, `au` and `in`.
- **`maxItems`** - hard ceiling on rows delivered and therefore on the bill. **Matters more than it used to**: depth now runs to tens of thousands, so this is the setting that keeps a run from being enormous.
- **`maxReviewsPerApp`** - optional per app-and-country cap so one huge app cannot eat the whole budget. `0` means no per-app cap.
- **`minRating`** / **`maxRating`** - star filter, applied before anything is written or billed.
- **`publishedAfter`** - ISO date. The way to target recent reviews given there is no sort.
- **`onlyWithDeveloperResponse`** - keep only reviews the publisher answered. Narrow by design; see the ~2% fill rate.
- **`proxyConfiguration`** - optional and off by default. Apple does not require a proxy here.

### Scraping 1-star and 2-star reviews only

Set `minRating: 1` and `maxRating: 2`. Filters run **before** the row is written, so you are not charged for the four and five star reviews you discarded. A live test returned 60 rows containing only ratings 1 and 2.

With tens of thousands of reviews now reachable per app, a complaint corpus is a genuinely different artefact than it was under a 500-review cap - you can pull years of one-star reviews for a whole competitive set and cluster them by theme.

Set `minRating: 5` for the opposite job: testimonial-grade quotes with the reviewer name attached.

### Multi-country review scraping for localisation

Reviews in `de` are written in German, in `jp` Japanese, in `br` Portuguese. Each storefront has its own review pool, so running one app across ten countries gives you a per-market sentiment read instead of a US-weighted average. Teams use this to decide which locale to localise next, and to separate a product problem from a translation problem.

### Developer response mining

Filter with `onlyWithDeveloperResponse: true` to get only the reviews a company chose to answer publicly, plus the reply text and its date. This is unusually high-signal competitive research: support teams disclose known issues, refund policy and roadmap in review replies far more freely than in any official channel.

Plan for volume in the tens, not the thousands - measured at roughly 2% of reviews. This field is new; the retired RSS feed never carried developer responses at all.

### Competitor review monitoring

Put five competitor app IDs in `appIds`, set a modest `maxItems`, add `publishedAfter` set to your last run date, and schedule it daily. Because `reviewId` is stable, deduplicating against yesterday's dataset is a single join. What you are building is an early warning system: the complaint in a competitor's reviews this week is the feature request in yours next month.

### Sentiment analysis input

The output is already shaped for a sentiment or topic model: one row per review, `text` decoded and clean, `rating` as a numeric label to train or validate against, `country` as a segment, `updatedAt` for time series. Export as JSON or CSV from the dataset, or read it from the Apify API.

### How this actor behaves when something goes wrong

1. **Errors never enter the dataset.** An app with no reviews in a storefront, an ID that does not resolve, an endpoint failure - each goes to a `FAILURES` record in the key-value store with the exact reason per input. None becomes a dataset row, because a row that says "error" bills you for receiving an error.
2. **A run that delivered nothing is marked FAILED, not SUCCEEDED.** Verified: a nonexistent app ID fails the run with a pointer to `FAILURES`.
3. **A bad `publishedAfter` fails immediately** with the offending value echoed back, rather than silently filtering everything away.
4. **The current endpoint is validated before parsing.** Apple's proxy rejects incomplete requests with `429 API capacity exceeded`, which looks like rate limiting but is really a malformed request - the actor always sends the required platform parameters, so that path is not hit.
5. **The free tier returns data.** Nothing throws because a plan feature is missing. Proxy is optional and defaults to off.

### Pricing

Pay per event. **$0.0003 per review delivered**, which is $0.30 per 1,000 reviews. No monthly subscription and no charge for starting a run.

You are billed only for rows that reach the dataset. Reviews removed by the rating, date or developer-response filters are not billed. Failed inputs are not billed. `maxItems` is a hard ceiling on both the row count and the bill, so a run cannot cost more than `maxItems x $0.0003`.

A 1,500-review deep pull costs **$0.45**. Pulling Facebook's full reachable ~78,000 US reviews would cost about $23 - possible now, which is exactly why `maxItems` exists.

### Output sample

```json
{
  "reviewId": "11405574963",
  "appId": "310633997",
  "appName": "WhatsApp Messenger",
  "country": "gb",
  "source": "amp-api",
  "userName": "Gruff The Sigma Rizzler",
  "userUrl": null,
  "rating": 5,
  "title": "Amazing",
  "text": "WhatsApp is really good however it's a bit hard to find some stuff...",
  "isEdited": false,
  "appVersion": null,
  "updatedAt": "2026-06-14T08:22:41.000Z",
  "voteSum": null,
  "voteCount": null,
  "developerResponseText": null,
  "developerResponseAt": null,
  "hasDeveloperResponse": false,
  "reviewUrl": "https://apps.apple.com/gb/app/id310633997?see-all=reviews",
  "scrapedAt": "2026-08-22T05:41:12.884Z"
}
```

### Dataset views

- **Reviews** - date, country, stars, title, review, author, source. The reading view.
- **Developer responses** - the review paired with the publisher's reply and its date.
- **Full records** - every field including IDs, edit flag and the legacy columns. The export view.

### Frequently asked questions

**Do I need an Apple developer account?** No. This reads what Apple's public storefront serves. It works on any app, including ones you do not own.

**Do I need a proxy or an API key?** Neither. No bearer token either - the endpoint used is a same-origin proxy that Apple's own site relies on, and it requires no authentication.

**How many reviews can I get for one app?** As many as the app actually has, up to a measured ~78,000 for a very large app. Use `maxItems` to control cost.

**Why are `appVersion` and the vote counts always empty?** Apple's current reviews endpoint does not expose them, and four different parameter approaches to recover them were tested and failed. See the field notes above.

**Can I sort by newest?** No - Apple's endpoint ignores every sort parameter, so no tool can honestly offer it. Use `publishedAfter` to target recent reviews instead.

**What is the `source` column?** Which endpoint served the row: `amp-api` for the current one, `rss` for the legacy feed used as fallback. Rows from `rss` carry `appVersion` and vote counts; rows from `amp-api` do not.

**Can I schedule it?** Yes. A small `maxItems` plus `publishedAfter` set to your last run date is the cheap daily-monitoring configuration.

### Related actors

- **App Store App Details Scraper** - ratings, in-app purchase names and prices, versions and release notes.
- **ASO Keyword Rank Tracker** - what position an app holds for a keyword in a given storefront.
- **Google Play Reviews Scraper** - the same job on Android, with developer replies and app version.
- **App Update Monitor** - new versions and release notes across both stores.

# Actor input Schema

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

Numeric app IDs, full apps.apple.com URLs, or iOS bundle IDs. Aliases accepted in JSON input: ids, appId, apps, appUrls.

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

Two letter iTunes storefronts. Each storefront holds its own separate pool of reviews, so adding countries multiplies your reach. Aliases: country, countryCodes.

## `maxItems` (type: `integer`):

Hard ceiling on reviews delivered and therefore on what you are charged. Apple's reviews endpoint goes as deep as the app's real review count - measured at ~78,000 for Facebook and ~42,000 for Calm - so this cap is what keeps a run from being enormous. Raise it deliberately.

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

Optional per-combination cap so one huge app cannot consume the whole maxItems budget. 0 means no per-app cap.

## `minRating` (type: `integer`):

Keep only reviews at or above this rating. Leave empty for all.

## `maxRating` (type: `integer`):

Keep only reviews at or below this rating. Set to 2 to mine complaints only.

## `publishedAfter` (type: `string`):

ISO date such as 2026-01-31. Apple's reviews endpoint has NO sort parameter - it returns a fixed relevance order, not newest first - so this filter is how you target recent reviews. Note it filters what was fetched, so a narrow date on a large app may need a high maxItems to find matches.

## `onlyWithDeveloperResponse` (type: `boolean`):

Developer responses are rare - measured at 3 out of 200 reviews (~1.5%) - so this filter is narrow by design.

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

Not required. Apple answers this actor without a proxy on the free plan. Only switch it on if your own network is rate limited.

## Actor input object example

```json
{
  "appIds": [
    "310633997",
    "https://apps.apple.com/us/app/chatgpt/id6448311069"
  ],
  "countries": [
    "us"
  ],
  "maxItems": 200,
  "maxReviewsPerApp": 0,
  "onlyWithDeveloperResponse": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One row per App Store review: rating, title, body, app version, author and date.

# 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",
        "https://apps.apple.com/us/app/chatgpt/id6448311069"
    ],
    "countries": [
        "us"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("renzomacar/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 = {
    "appIds": [
        "310633997",
        "https://apps.apple.com/us/app/chatgpt/id6448311069",
    ],
    "countries": ["us"],
}

# Run the Actor and wait for it to finish
run = client.actor("renzomacar/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 '{
  "appIds": [
    "310633997",
    "https://apps.apple.com/us/app/chatgpt/id6448311069"
  ],
  "countries": [
    "us"
  ]
}' |
apify call renzomacar/app-store-reviews-scraper --silent --output-dataset

```

## MCP server setup

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