# App Store & Google Play Reviews Scraper (`djbrooks111/app-reviews`) Actor

Scrape Apple App Store and Google Play reviews into one schema, with incremental `since` sync and optional lexicon sentiment.

- **URL**: https://apify.com/djbrooks111/app-reviews.md
- **Developed by:** [David Brooks](https://apify.com/djbrooks111) (community)
- **Categories:** Marketing, Developer tools, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.15 / 1,000 delivered reviews

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## App Store & Google Play Reviews Scraper

Both mobile stores, one review schema, one price — with the Apple 500-per-storefront cap explained instead of hidden.

This App Store Reviews Scraper and Google Play Reviews Scraper pulls app reviews from Apple and Google into a single flat schema, so an iOS and an Android app can be compared side by side without reconciling two exports. Every row carries the rating, title, body, app version, developer reply, helpful votes, storefront, language and posting date, plus a stable review id for deduplication. Pass numeric App Store ids, Android package names, or store URLs and the store is detected for you. Use `since` for incremental runs that stop as soon as they reach reviews you already have, and turn on `sentiment` for a positive/neutral/negative label with topic keywords.

### What you get

- Both stores in one run: `apps: ["284882215", "com.spotify.music"]` returns one dataset with identical keys
- Every row is proven to belong to the app you asked for — Apple rows are checked against the feed's own id and each review's permalink, and any row pointing elsewhere is dropped rather than delivered
- Developer replies and reply dates from Google Play, quality sub-ratings (battery, performance, ads) where Google collects them
- `since` incremental sync: a scheduled run stops at the first review older than your cutoff, so you pay for new reviews only
- `countries: ["all"]` walks all 115 App Store storefronts in one run, deduplicating by review id — the supported way past Apple's 500-per-storefront ceiling
- Optional lexicon sentiment label plus up to three topic keys per review — deterministic, no LLM, no API key
- Reviewer names off by default; `schemaVersion` on every row so keys never change under you

Sample row:

```json
{
  "schemaVersion": 1,
  "sourceId": "google-play:us:04d72efa-9805-4abf-bb76-5958628b868f",
  "sourceUrl": "https://play.google.com/store/apps/details?id=com.spotify.music&reviewId=04d72efa-9805-4abf-bb76-5958628b868f",
  "source": "google-play",
  "subjectId": "com.spotify.music",
  "subjectName": "Spotify: Music and Podcasts",
  "country": "US",
  "language": "en",
  "rating": 3,
  "title": null,
  "body": "Good catalogue but the ads between every song are too much.",
  "author": null,
  "version": "9.1.80.2221",
  "publishedAt": "2026-09-13T15:41:22.000Z",
  "developerReply": "Hey! We hear you and we'll pass your feedback along to the Ads team.",
  "developerReplyAt": "2026-09-13T19:27:18.000Z",
  "helpfulVotes": 0,
  "totalVotes": null,
  "subRatings": "{\"ads_frequency\":2,\"visual_appeal\":1}",
  "sentiment": "negative",
  "sentimentTopics": [
    "ads",
    "price"
  ]
}
```

### Input

| Field | Description |
|---|---|
| `apps` | Apple numeric ids (284882215), Android packages (com.spotify.music), or store URLs. Mixed lists are fine. |
| `store` | auto (default), apple or google — force detection when an id is ambiguous |
| `countries` | Two-letter storefront/country codes, default \["us"], or \["all"] to walk all 115 App Store storefronts |
| `language` | Google Play `hl` code for review text and the app title (default en) |
| `sort` | recent (default), helpful, or rating |
| `since` | ISO date; stop paging at reviews older than this. Makes scheduled runs incremental. |
| `includeAuthor` | Include the reviewer's display name. Off by default — personal data. |
| `sentiment` | Add a lexicon sentiment label and topic keys. Charged per scored review. |
| `maxItems` | Hard cap on delivered reviews (default 100) |
| `maxCostUsd` | Hard stop on spend, enforced before each request |

### Pricing

Pay per event. You are charged only for delivered, validated results — never for empty pages, blocked requests, or duplicates. `maxCostUsd` is a hard stop enforced before each request.

| Event | Price | Unit |
|---|---|---|
| `run-start` | $0.001 | per run — charged only once the run delivers its first row |
| `review` | $0.00015 | per delivered review — $0.15 per 1,000 |
| `sentiment` | $0.0008 | per scored review — only when sentiment is enabled |

- 10,000 reviews across both stores ≈ $1.50
- A daily 200-review incremental monitor ≈ $0.93/month
- 1,000 reviews with sentiment ≈ $0.95

### Why this actor

- Delivering a review from the wrong app is structurally impossible on the Apple side: the feed's own id and each review's permalink are both checked against the app you requested, and a whole response that belongs to another app fails the run instead of filling your dataset
- Apple's feed intermittently answers HTTP 200 with no reviews at all; a scraper that treats that as the end of the data silently returns a fraction of what exists. Empty pages are retried, and one empty page never ends a fan-out that still has pages behind it
- The 500-review ceiling is a platform limit, not a bug, and it is documented here with the storefront fan-out that gets you past it — rather than surfacing as an unexplained cap of 10 or 99 results
- One schema for both stores, versioned with `schemaVersion`, so output keys do not change between runs
- Caps are enforced before the request that would breach them, and you are charged only for validated rows that reach your dataset
- A run that is blocked or finds nothing costs nothing at all, run-start included

### Known limits

- Apple serves at most 10 pages of 50 reviews for each storefront and sort, so 500 reviews per storefront per sort is a hard ceiling (page 11 answers HTTP 400). To go further, list more `countries` or pass `["all"]` for all 115 storefronts: each has its own 500. The actor also fans out across both of Apple's sorts and deduplicates by review id.
- `countries: ["all"]` fans out App Store apps only. Google Play does not partition reviews by storefront the way Apple does, and each Play country costs a large details fetch, so Play uses the country codes you list explicitly, or `us`.
- Google Play pagination stops at 200 pages per app and storefront (20,000 reviews), and stops early if Google repeats a continuation token or two pages in a row bring nothing new.
- Apple's public feed carries no developer replies, so `developerReply` and `developerReplyAt` are always null for App Store rows. Google Play rows do carry them.
- Apple has no rating-sorted feed; `sort: "rating"` falls back to newest first for App Store apps and is applied normally on Google Play.
- Setting `since` forces newest-first ordering on both stores, because that is the only ordering in which stopping early is correct.
- Sentiment is a keyword lexicon with negation handling, not a language model, and the `sentiment` event is priced as a convenience add-on rather than as model inference passed through at cost. It falls back to the star rating when a review contains no lexicon term. It is accurate on plain review prose and will miss sarcasm. Leave `sentiment` off and pay nothing for it if you would rather score the text yourself.
- On the default flat output, Google Play sub-ratings are one JSON-string column named `subRatings` (null when a review has none), so every row has the same columns; the sample row above shows that shape. With `outputFormat: nested` it is an object.
- Google Play rows are attributed to the package id you requested: Google's review payload and continuation token do not repeat the package id, so unlike the App Store (where the feed id and each entry's app link are checked) there is nothing in the response to cross-check it against.
- Google Play review rows carry no title (the store has no title field) and Apple rows carry no per-review permalink, so `sourceUrl` is the app page for App Store rows.
- Neither store needs a proxy for small runs. Google Play throttles per IP after a few hundred pages — switch on the datacenter Apify Proxy for large Play pulls.
- When Apple's feed is having an outage, meaning a storefront answers every page with HTTP 200 and no usable reviews (no entries at all, or entries that arrive without an app id in their link), that storefront is listed in `meta.appleFeedEmpty` with a warning. The run ends with a PARSE ERROR, and nothing is charged, only when no review was delivered from any storefront or store; otherwise it succeeds with what it found. A storefront whose lookup reports 0 ratings is not an outage: that app simply has no reviews, and the run succeeds empty and free.

### Use from an AI agent (MCP)

Add the actor as an MCP tool in Claude Desktop / Cursor via the Apify MCP server:

```json
{ "mcpServers": { "app-reviews": { "url": "https://mcp.apify.com?actors=<your-username>/app-reviews", "headers": { "Authorization": "Bearer <APIFY_TOKEN>" } } } }
```

The tool accepts exactly the input fields above and returns the same rows, so an agent can ask for
"the last 200 one-star reviews of com.spotify.music in the US" in one call.

### Use from n8n / Make

Use the Apify node → "Run actor" with actor `<your-username>/app-reviews` and a JSON input such as
`{"apps":["284882215","com.facebook.katana"],"countries":["us","gb"],"since":"2026-09-01","sentiment":true,"maxItems":1000}`
on a daily schedule, then "Get dataset items" and route negative rows to Slack.

### FAQ

**Why do I only get 500 reviews per app from the App Store?**

Apple's public review feed stops at 10 pages of 50 per storefront and sort; page 11 returns HTTP 400 for every app. Add more entries to `countries`, or pass `["all"]` to walk all 115 storefronts, and the actor deduplicates by review id across every one of them.

**What happens if a run is blocked or the app has no reviews?**

You are charged nothing, including the run-start fee, which is only applied once the run delivers its first row. A blocked run fails with a typed error rather than reporting success with an empty dataset.

**Why is `developerReply` empty for my iOS app?**

Apple does not expose developer replies anywhere in the public feed. Google Play does, and those rows are populated.

**How do I run this incrementally?**

Set `since` to the timestamp of your last run. Paging stops at the first review older than that, so a daily run charges only for new reviews.

**Does the sentiment option call an LLM?**

No. It is a deterministic keyword lexicon with negation handling, so the same text always gets the same label and there is no API key to supply.

**Do I need a proxy?**

Not for small runs. Google Play starts throttling per IP after a few hundred pages, so enable the datacenter Apify Proxy for large Play pulls. Residential is never required.

### Legal and data posture

This actor reads publicly published app reviews from Apple's open RSS feed and Google Play's public store pages. It never logs in, never uses accounts or cookies from accounts, and never solves a CAPTCHA. Reviewer display names are personal data under GDPR and CCPA, so `includeAuthor` is false by default and no reviewer avatar, profile URL or numeric reviewer id is ever emitted unless you switch it on and have a lawful basis for doing so. Review text is the reviewer's copyrighted expression: it is delivered to you for your own analysis, and we neither rehost it nor build or resell a corpus. Request rates are kept modest, 429 responses are honoured with backoff, and a blocked run fails with a typed error rather than quietly returning nothing.

### Support

Open an issue on the Issues tab; first response within 6 hours on business days. Every run writes a `RUN_SUMMARY` record to the key-value store with items delivered, events charged, and the stop reason.

# Actor input Schema

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

Apple numeric app ids (284882215), Android package names (com.spotify.music), or App Store / Google Play URLs. The store is detected from each value.

## `store` (type: `string`):

auto (default) detects the store from each value. Force apple or google when an id is ambiguous.

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

Two-letter codes, e.g. \["us","gb","de"]. Apple serves at most 500 reviews per storefront per sort, so adding storefronts is how you get more than 500. Pass \["all"] to walk all 115 App Store storefronts; Google Play is not fanned out that way (it does not partition reviews by storefront and each country costs a large page fetch), so Play uses the codes you list, or us.

## `language` (type: `string`):

Google Play `hl` language code for review text and the app title. Apple's feed has no language parameter.

## `sort` (type: `string`):

recent (default), helpful, or rating. Apple has no rating-sorted feed and falls back to newest first.

## `since` (type: `string`):

ISO date, e.g. 2026-09-01. Paging stops once reviews are older than this, which makes a scheduled run incremental. Forces newest-first ordering.

## `includeAuthor` (type: `boolean`):

Off by default. Reviewer display names are personal data; leave this off unless you have a lawful basis for collecting them.

## `sentiment` (type: `boolean`):

Add a lexicon-based positive/neutral/negative label and up to three topic keys per review. Charged as a separate `sentiment` event. No LLM is called.

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

Hard cap on delivered reviews for this run. Default 100 keeps free trials cheap; raise it for production.

## `maxCostUsd` (type: `number`):

Hard stop: the run never charges more than this. Enforced before each request, not after.

## `stateNamespace` (type: `string`):

Optional suffix for this actor's cross-run state store, so separate schedules or test runs keep separate monitor baselines and watermarks (e.g. `prod`, `staging`).

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

Neither store needs a proxy for small runs, so none is used by default. Turn on the datacenter Apify Proxy for large Google Play pulls: Play throttles per IP after a few hundred pages. Residential is never required.

## `outputFormat` (type: `string`):

flat (default): one level of keys. nested: keep objects.

## Actor input object example

```json
{
  "apps": [
    "284882215",
    "com.spotify.music"
  ],
  "store": "auto",
  "countries": [
    "us"
  ],
  "language": "en",
  "sort": "recent",
  "includeAuthor": false,
  "sentiment": false,
  "maxItems": 100,
  "maxCostUsd": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "outputFormat": "flat"
}
```

# Actor output Schema

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

Every delivered row, validated against the actor's entity schema (flat keys, ISO dates, sourceUrl, scrapedAt, sourceId). Only delivered rows are billed.

## `runSummary` (type: `string`):

RUN_SUMMARY: items delivered, events charged, USD charged, blocks met, requests made, the typed error if the run failed, and actor-specific meta.

# 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": [
        "284882215",
        "com.spotify.music"
    ],
    "countries": [
        "us"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("djbrooks111/app-reviews").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": [
        "284882215",
        "com.spotify.music",
    ],
    "countries": ["us"],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("djbrooks111/app-reviews").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": [
    "284882215",
    "com.spotify.music"
  ],
  "countries": [
    "us"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call djbrooks111/app-reviews --silent --output-dataset

```

## MCP server setup

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

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/Hdkhx2isdwaxnTHgU/builds/OdIsv74gkR99j41ra/openapi.json
