# App Store & Google Play Reviews Scraper (`notabotpromise/app-review-monitor`) Actor

Scrape Apple App Store and Google Play reviews into one JSON schema. Only-new mode for scheduled monitoring, a clean stop at your budget, no browser.

- **URL**: https://apify.com/notabotpromise/app-review-monitor.md
- **Developed by:** [Beep Boop](https://apify.com/notabotpromise) (community)
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 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.

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

### 🔍 What does this App Store and Google Play reviews scraper do?

Collect **Apple App Store** reviews from [apps.apple.com](https://apps.apple.com) and **Google Play** reviews from [play.google.com](https://play.google.com/store/apps) in **one JSON schema**. Use only-new mode for scheduled monitoring, or treat this as an App Store and Google Play reviews API alternative: it reads public review data over HTTP, requires no store login, and uses no browser. It stops cleanly at your run budget and runs at 256 MB by default.

Price: **$0.0005 per review ($0.50 per 1,000)** plus **$0.00005 per run**.

### 💡 Use cases

- Track app ratings and store listing performance for ASO.
- Give support teams public reviewer names for reply workflows.
- Compare product feedback by `appVersion` after a release.
- Send review text into sentiment analysis or an LLM pipeline.
- Monitor public reviews for competitor apps you are authorized to track.
- Run on an Apify schedule, through the API, with webhooks, or from an integration.

### 🚀 How to scrape App Store and Google Play reviews

1. Open the Actor's **Input** tab. Find the App Store ID at the end of a link such as `https://apps.apple.com/us/app/name/id324684580`; find a Google Play package after `?id=`, such as `com.spotify.music`.
2. Enter the apps, choose storefront countries, and choose a Google Play language if needed.
3. Set `maxReviewsPerApp` and the run's maximum cost in **Run options**.
4. Click **Start**. Review records appear in the **Output** tab; use the **API** tab to call the Actor programmatically.
5. For monitoring, enable `onlyNewSinceLastRun` and create a schedule. Keep its first run to establish the baseline.

### ⚙️ Input

See the **Input** tab for every option. Each input is one JSON object:

- `appStoreApps`: App Store numeric IDs or supported App Store URLs.
- `googlePlayApps`: case-sensitive package names or Google Play URLs.
- `countries`: one or more two-letter storefront codes.
- `language`: Google Play language code such as `en` or `pt-BR`.
- `maxReviewsPerApp`: maximum newest reviews per app and country (and language on Google Play).
- `onlyNewSinceLastRun`: opt in to account-level review watermarks for monitoring.
- `includeUserImage`: opt in to Google Play avatar URLs, which are personal data.

```json
{
  "appStoreApps": ["324684580"],
  "googlePlayApps": ["com.spotify.music"],
  "countries": ["us"],
  "maxReviewsPerApp": 100
}
```

```json
{
  "appStoreApps": ["https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580"],
  "googlePlayApps": ["https://play.google.com/store/apps/details?id=com.spotify.music"],
  "countries": ["us", "gb"],
  "maxReviewsPerApp": 100
}
```

```json
{
  "appStoreApps": ["324684580", "389801252"],
  "googlePlayApps": ["com.spotify.music"],
  "countries": ["us", "gb", "de"],
  "maxReviewsPerApp": 200,
  "onlyNewSinceLastRun": true
}
```

```json
{
  "googlePlayApps": ["com.spotify.music"],
  "countries": ["br"],
  "language": "pt-BR",
  "maxReviewsPerApp": 50
}
```

The country inside an App Store URL is ignored; `countries` controls the storefront. Google Play package names are case-sensitive. `language` applies only to Google Play. Invalid input fails before any request, with a message that identifies what to fix, for example `Provide at least one app in appStoreApps or googlePlayApps`, `Invalid country "usa": use a two-letter storefront code such as "us"`, or `maxReviewsPerApp must be a whole number of at least 1`.

### 📦 Output

The default dataset contains one item per review. Download it as JSON, CSV, Excel, or HTML, or read it through the API. Fields a store does not provide are `null`, never empty strings. These two records preserve real key order and value types; personal text, names, IDs, and the Play review URL have been anonymized.

```json
[
  {
    "store": "apple",
    "appId": "324684580",
    "country": "us",
    "reviewId": "00000000001",
    "author": "Example Reviewer",
    "rating": 1,
    "title": "Example playlist review",
    "text": "Example review text about using the app.",
    "date": "2026-10-02T17:05:19Z",
    "appVersion": "9.1.88",
    "helpfulVotes": 0,
    "developerReply": null,
    "reviewUrl": null,
    "scrapedAt": "2026-10-03T19:17:29.695Z"
  },
  {
    "store": "google-play",
    "appId": "com.spotify.music",
    "country": "us",
    "reviewId": "00000000-0000-4000-8000-000000000002",
    "author": "Example Reviewer",
    "rating": 1,
    "title": null,
    "text": "Example review text about using the app.",
    "date": "2026-10-02T18:35:24.212Z",
    "appVersion": "9.0.82.1032",
    "helpfulVotes": 0,
    "developerReply": {
      "text": "Thanks for sharing your feedback.",
      "date": "2026-10-02T19:07:45.423Z"
    },
    "reviewUrl": "https://play.google.com/store/apps/details?id=com.spotify.music&reviewId=00000000-0000-4000-8000-000000000002",
    "scrapedAt": "2026-10-03T19:17:30.259Z"
  }
]
```

The Output tab also links `RUN_SUMMARY`, which contains five top-level keys: `itemsPushed` is the delivered review count; `budgetReached` says whether the run stopped at its spending limit; `completed` lists jobs and stop reasons; `failed` lists jobs with sanitized error details; and `skipped` lists jobs not started after a budget stop. This anonymized shape came from a real partial-failure run:

```json
{
  "itemsPushed": 5,
  "budgetReached": false,
  "completed": [
    {
      "label": "apple 324684580/us",
      "store": "apple",
      "appId": "324684580",
      "country": "us",
      "stopReason": "max-reviews",
      "itemsPushed": 5
    }
  ],
  "failed": [
    {
      "label": "apple 324684580/zz",
      "store": "apple",
      "appId": "324684580",
      "country": "zz",
      "itemsPushed": 0,
      "error": {
        "message": "apple 324684580/zz page 1: Unknown Apple storefront country \"zz\"",
        "status": 400,
        "attempts": 1
      }
    }
  ],
  "skipped": []
}
```

### 📊 What data does it extract?

| Field | Type | App Store | Google Play | Description |
|---|---|---|---|---|
| `store` | string | `apple` | `google-play` | Review source. |
| `appId` | string | Numeric app ID | Package name | App requested. |
| `country` | string | Storefront | Storefront | Two-letter storefront code. |
| `reviewId` | string | Review ID | Review ID | Store's identifier for the review. |
| `author` | string or null | Public name | Public name | Reviewer's public display name. |
| `rating` | integer | 1–5 stars | 1–5 stars | Star rating. |
| `title` | string or null | Available | `null` | App Store title; Google Play has no title field. |
| `text` | string or null | Review body | Review body | Public review text. |
| `date` | string | UTC ISO 8601 | UTC ISO 8601 | Posted or last-edited date. |
| `appVersion` | string or null | When supplied | When supplied | Version the reviewer used. |
| `helpfulVotes` | integer or null | Feed vote sum | Helpful count | Store-provided helpful votes. |
| `developerReply` | object or null | `null` | Reply or `null` | Public Google Play reply text and date. |
| `reviewUrl` | string or null | `null` | Review link | Per-review link when Google Play supplies one. |
| `userImage` | string or null | `null` | Opt-in avatar URL | Present only when `includeUserImage` is enabled. |
| `scrapedAt` | string | UTC ISO 8601 | UTC ISO 8601 | Fetch time; with `reviewUrl`, a provenance field. |

### 💰 How much does it cost to scrape app reviews?

This Actor uses pay-per-event pricing: **$0.0005 per review** through the default dataset-item event plus Apify's **$0.00005 start fee**, charged once per run. The Actor is capped at 1 GB and never runs above it, so there is no separate charge for the compute a run uses.

**Maximum run cost = $0.00005 + $0.0005 × reviews**, where reviews are at most `(App Store apps × countries × min(maxReviewsPerApp, 500)) + (Google Play apps × countries × maxReviewsPerApp)`.

| Example | Maximum reviews | Maximum event cost |
|---|---:|---:|
| Prefill: 1 App Store app + 1 Google Play app, `us`, 20 each | 40 | $0.02005 |
| 3 App Store apps + 2 Google Play apps, `us` and `gb`, 100 each | 1,000 | $0.50005 |
| 1 App Store app, 5 countries, `maxReviewsPerApp` 1000; Apple caps at 500 per country | 2,500 | $1.25005 |
| Daily only-new schedule: 5 apps × 2 countries × about 10 new reviews per day | About 3,000 per month | About $1.50 per month plus $0.0015 in start fees |

Set the maximum cost for each run in Run options (`maxTotalChargeUsd` through the API). The Actor stops before going over; the run still ends **SUCCEEDED** with `Stopped at the charge limit after N reviews.` The start fee counts toward the limit, so a $0.01 limit allows up to 19 reviews: `floor((0.01 − 0.00005) / 0.0005)`. In only-new mode, scheduled runs are charged for reviews no earlier run delivered. A budget stop or a failed app does not cause repeats, unless the state store itself fails: the next run skips the reviews already delivered. If the state store keeps failing, that app's job fails with a message that says how many reviews were delivered and billed. Progress is saved after every delivered page, so an aborted, timed-out or migrated run repeats at most the page it was writing; those reviews, and edited reviews that return as new, are charged again.

#### 💸 Only-new monitoring vs re-scraping: monthly cost

A scraper without only-new mode has to re-download a window of the newest reviews on every run and leave the de-duplication to you. With only-new mode you pay for reviews no earlier run delivered, plus any re-deliveries described in **How only-new monitoring works**.

Example: 5 apps × 2 countries checked daily, about 10 new reviews per app and country per day.

| Approach | Reviews charged per month | Review cost per month |
|---|---:|---:|
| This Actor, only-new mode | About 3,000 | About $1.50 |
| Re-scrape the newest 100 per app and country daily at $0.10 per 1,000 | About 30,000 | About $3.00, plus your own de-duplication |
| Re-scrape the newest 50 per app and country daily at $0.10 per 1,000 | About 15,000 | About $1.50, plus your own de-duplication, and busy days can overflow the window |

Only-new mode costs less once you would otherwise re-scrape more than about 50 reviews per app and country per run, and it removes the overlap between runs for you. Duplicates are rare; they can follow an aborted, timed-out or migrated run (at most the page it was writing), an edited review, or several countries that share one Google Play language (see **How only-new monitoring works** and **Limits and known gaps**). For one-off exports, leave the mode off.

### 🔔 How only-new monitoring works

The first run delivers up to `maxReviewsPerApp` newest reviews for each app and country as a baseline. Later runs deliver reviews that no earlier run of this Actor delivered.

- If a run stops at its charge limit or an app fails partway, the Actor remembers which of that app's reviews it already delivered. The next run skips them and continues with the reviews still owed, so a schedule that hits its budget every run keeps moving forward instead of paying for the same reviews again. The record is saved after every delivered page and again when the platform asks the Actor to save its state (a graceful abort or a migration), so a run that is aborted, times out or is migrated repeats at most the page it was writing when it stopped. After a budget stop or a failed app, no review that was already delivered is delivered again and none of the undelivered ones within the Actor's reach are dropped, unless the state store itself fails.
- If more new reviews arrive than `maxReviewsPerApp`, the newest are delivered and the rest are skipped for good. The App Store's 500-per-country feed limit has the same effect, so schedule often enough.
- Edited reviews can return as new because newness is based on review date.
- State lives in `app-review-monitor-state`, in your account, with one record per app and country (and language for Google Play), plus a `.pending` record while an app's reviews are only partly delivered. Records store only dates and review IDs. Every task and schedule for this Actor in your account shares that state; parallel runs are last-write-wins.
- To start over, delete an app's records (its `.pending` record too, if there is one) or the whole store in Console Storage. A damaged record fails that app and names the record to delete.
- Leave the mode off for one-time exports.

### ⚠️ Limits and known gaps

- The App Store feed returns only the **500 newest reviews per country** (pages 1–10 of 50). It cannot backfill older history.
- App Store records have no per-review link, developer reply, or avatar. Google Play records have no title.
- Google Play serves reviews by language. In a measured Spotify sample, US and GB with `en` returned **20 of 20 identical reviews**. Each country copy is delivered and charged; use one country per language when you want to avoid duplicates.
- An unknown app ID or an app with no reviews may return no items with a `returned no reviews` warning instead of an error. `helpfulVotes` on the App Store is the vote sum reported by its feed.
- Google Play reviews come through an unofficial endpoint of the Play website. A change there can interrupt runs until an update ships. A rare platform migration restarts a run: without `onlyNewSinceLastRun`, reviews delivered before the migration are delivered and charged again; with it, at most the page in flight repeats.

### 🛠️ Troubleshooting

When one app fails, the run can still end **SUCCEEDED**. The status message identifies the failed app, `RUN_SUMMARY` on the Output tab lists its sanitized error, and reviews already delivered remain delivered and charged. Failure details are never added to the dataset because every dataset item is billed. The run ends **FAILED** when every app job fails, or when input is invalid before a request.

A live partial-failure run returned five US reviews and reported:

> Finished: 5 reviews from 2 app/country jobs. Failed 1 of 2: apple 324684580/zz (HTTP 400). See RUN_SUMMARY for details.

HTTP 429, HTTP 5xx, network errors, and timeouts are retried for at most **4 attempts total** with exponential backoff waits of **1, 2, and 4 seconds**. Each request has a **20-second timeout**, and requests are spaced at least **500 ms** apart. HTTP 400 unknown storefronts, other permanent 4xx errors, invalid IDs, and malformed responses fail without a retry. Errors include the store, app, country, page, and HTTP status when available.

- `returned no reviews`: check the app ID or package and storefront; there may be no reviews there. Rerun if the source returned a transient empty first page.
- `Stopped at the charge limit`: raise the run's maximum cost.
- An only-new run with no items may have nothing new to deliver; this is expected.
- For a damaged only-new state record, delete the named record in Console Storage.
- Use the **Issues** tab to report a problem.

### ❓ FAQ

#### Can I scrape App Store and Google Play reviews in one run?

Yes. Put App Store IDs or URLs in `appStoreApps` and Google Play packages or URLs in `googlePlayApps`. Both stores come back in the same JSON schema, so one dataset covers both.

#### How do I get only new app reviews since the last run?

Enable `onlyNewSinceLastRun` and run the Actor on a schedule. The first run sets the baseline; later runs deliver reviews no earlier run delivered. After a budget stop or a failed app, the next run continues where it stopped. An aborted, timed-out or migrated run can repeat at most the page it was writing, and edited reviews can return as new; see **How only-new monitoring works**.

#### Do I need a login, API key, or proxy?

No. The Actor reads public review data without a store login or developer API key, and it needed no proxy in tests on the Apify platform.

#### How many reviews can I get per app?

Up to `maxReviewsPerApp` newest reviews per app and country (and language on Google Play). The App Store feed stops at the 500 newest reviews per country.

#### Can I filter reviews by star rating?

Not in the input yet. Every record has a `rating` field from 1 to 5, so filter the dataset in your export, spreadsheet, or integration.

#### How do I cap what a run can cost?

Set the maximum cost per run in Run options, or `maxTotalChargeUsd` through the API. The Actor stops before the limit and the run still ends SUCCEEDED.

#### Can I call it from Python, Node.js, or an AI agent?

Yes. Use the **API** tab for ready-made calls, the Apify client libraries for Python and JavaScript, or Apify's MCP server to let an AI agent run it.

#### Can I export app reviews to CSV or Excel?

Yes. Download the dataset as JSON, CSV, Excel, or HTML from the Output tab, or read it through the API.

### 🔒 Data use and GDPR

This Actor reads public App Store and Google Play reviews and does not extract private data such as email addresses, gender, or location. Results can still contain personal data. Do not collect or process personal data unless you have a legitimate reason; consult your lawyer if you are unsure. Usernames are included because they support review-reply workflows. Avatar URLs are collected only when `includeUserImage` is enabled. App Store reviewer profile links are never collected. Only-new state stores dates and review IDs. Dataset retention follows your Apify plan; the named state store remains in your account until you delete it.

### 📝 Changelog

#### 0.1 update (2026-10-03)

- Only-new mode remembers the reviews a run delivered before it stopped at its charge limit or lost an app to an error, so the next run continues instead of charging them again.
- Invalid input ends the run FAILED with the input error as its status message.

#### 0.1 (2026-10-03)

- Added App Store and Google Play reviews in one schema.
- Added only-new monitoring and a clean stop at the run budget.
- Added per-app failure isolation with `RUN_SUMMARY` and bounded retries with request timeouts.

# Actor input Schema

## `appStoreApps` (type: `array`):

Numeric App Store IDs or App Store links. The country inside a link is ignored; Countries decides the storefronts. Leave empty to read only Google Play.

## `googlePlayApps` (type: `array`):

Google Play package names or Play Store links. Package names are case-sensitive. Leave empty to read only the App Store.

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

Two-letter storefront codes. Each app is read once per country. The App Store returns at most the 500 newest reviews per country.

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

Google Play only. App Store reviews are returned in the language reviewers wrote.

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

Newest reviews to deliver per app and country (and language on Google Play). Each review costs $0.0005. The App Store caps this at 500 per country.

## `onlyNewSinceLastRun` (type: `boolean`):

Deliver only reviews that no earlier run of this Actor in your account delivered for the same app and country. Intended for schedules. After a budget stop or a failed app, the next run continues where it stopped; an aborted, timed-out or migrated run can repeat at most the page it was writing. If more new reviews arrive than the maximum per app, only the newest are delivered. Dates and review IDs are kept in app-review-monitor-state.

## `includeUserImage` (type: `boolean`):

Adds the reviewer's avatar URL for Google Play. This is personal data; leave it off unless you need it.

## Actor input object example

```json
{
  "appStoreApps": [
    "324684580",
    "https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580"
  ],
  "googlePlayApps": [
    "com.spotify.music",
    "https://play.google.com/store/apps/details?id=com.spotify.music"
  ],
  "countries": [
    "us",
    "gb",
    "de"
  ],
  "language": "pt-BR",
  "maxReviewsPerApp": 200,
  "onlyNewSinceLastRun": true,
  "includeUserImage": false
}
```

# Actor output Schema

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

Dataset with one unified record per App Store or Google Play review.

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

Completed and failed apps, review counts, and sanitized errors.

# 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 = {
    "appStoreApps": [
        "324684580"
    ],
    "googlePlayApps": [
        "com.spotify.music"
    ],
    "countries": [
        "us"
    ],
    "maxReviewsPerApp": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("notabotpromise/app-review-monitor").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 = {
    "appStoreApps": ["324684580"],
    "googlePlayApps": ["com.spotify.music"],
    "countries": ["us"],
    "maxReviewsPerApp": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("notabotpromise/app-review-monitor").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 '{
  "appStoreApps": [
    "324684580"
  ],
  "googlePlayApps": [
    "com.spotify.music"
  ],
  "countries": [
    "us"
  ],
  "maxReviewsPerApp": 20
}' |
apify call notabotpromise/app-review-monitor --silent --output-dataset

```

## MCP server setup

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

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/6SbqfGAYh9ppwKltS/builds/3Sq6q8vSRMhb6Fr7l/openapi.json
