# App Store Review Monitor - New & Edited Reviews (`natstandridge/app-store-review-monitor`) Actor

Monitor App Store reviews across countries, detect new and edited reviews from your snapshot, filter ratings and keywords, and get version-country sample rollups. Pay only for delivered reviews.

- **URL**: https://apify.com/natstandridge/app-store-review-monitor.md
- **Developed by:** [Nathaniel Standridge](https://apify.com/natstandridge) (community)
- **Categories:** Marketing, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.50 / 1,000 delivered app store 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

## App Store Review Monitor — New & Edited Reviews by Version/Country

Turn recent Apple App Store reviews into a daily product feedback feed. Monitor selected apps and countries, filter for ratings or keywords, and identify reviews that are new or edited compared with your own previous snapshot. Export clean review rows to CSV, JSON, a database, or your existing automation.

**Price: $0.50 per 1,000 delivered reviews ($0.0005 each). No startup event.** Failed feeds, invalid reviews, duplicates, filtered reviews and undelivered reviews are uncharged. Coverage diagnostics, the next snapshot and rating rollups of the delivered sample are included at no additional Actor event charge.

### Who it helps

App product teams need a fresh review triage list after releases so they can find regressions and decide which fixes to prioritize. Agencies managing several apps need a repeatable view across markets. Instead of comparing CSV exports manually, supply the previous snapshot and receive only new or changed review content, with stable identifiers and the source feed URL.

The Actor adds country selection, bounded pagination, rating/version/keyword filters, change detection, integration identifiers, billing limits and sample rollups to Apple's public review feeds. It uses deterministic parsing; it does not generate sentiment or summaries with an AI model.

### Try a small run

```json
{
  "apps": ["618783545"],
  "countries": ["us"],
  "maxPages": 1,
  "maxResults": 50,
  "maxRunSeconds": 120
}
```

This inspects one page of recent US reviews for the example app. Up to 50 delivered rows cost at most **$0.025 in Actor events**. The default run charge cap is $0.05. Apple may return fewer reviews. You can also supply an official URL such as `https://apps.apple.com/us/app/slack/id618783545`; the URL supplies the app ID and `countries` controls the requested storefronts.

Open the **Reviews and changes** dataset view for usable rows. Open **OUTPUT** in key-value storage for coverage, the snapshot and per-version/country sample rollups. An empty dataset with `OUTPUT.status = "succeeded"` means the requested window was inspected and no review matched; check its filters and coverage before interpreting the result.

### A recurring review workflow

1. Run once for the apps and countries you track. Review `OUTPUT.complete` and each entry in `OUTPUT.coverage`.
2. Save `OUTPUT.snapshot.previousReviews` in your own workflow storage.
3. On the next run, send that array as `previousReviews` and set `changesOnly` to `true`. Keep the same scope for a comparable window.
4. Send delivered `new` and `updated` rows into your existing triage queue. Use `reviewKey` to update an existing review record; use `observationId` to deduplicate a particular content version.
5. Replace the workflow snapshot with the returned snapshot after reviewing coverage. A partial run preserves undelivered baseline hashes and advances only delivered records.

No schedule, shared named store, webhook or external notification is created by this Actor. Configure frequency and destination in your own Apify task, API client, Make, n8n or other workflow.

Example for a second run:

```json
{
  "apps": ["618783545"],
  "countries": ["us"],
  "maxPages": 2,
  "maxResults": 100,
  "maxRating": 2,
  "keywords": ["crash", "login"],
  "changesOnly": true,
  "previousReviews": [
    {
      "appId": "618783545",
      "country": "us",
      "reviewId": "14749821280",
      "contentHash": "d5ce7c008641885ed94ef936eaf215f57b99ddab71280b69df8fc954c35835c1"
    }
  ]
}
```

Use the complete snapshot from your previous run; the single-record array illustrates its shape. Changing filters does not retroactively add filtered reviews to the baseline. A review not previously delivered can therefore be marked `new` when it first matches.

### What a review row contains

This excerpt comes from the example app's public US feed, inspected on November 5, 2026. Review text can change later. Reviewer names and author IDs are omitted.

```json
{
  "reviewKey": "618783545:us:14749821280",
  "appId": "618783545",
  "country": "us",
  "reviewId": "14749821280",
  "changeType": "observed",
  "rating": 5,
  "title": "“Catch Up” feature is game changing",
  "text": "Plz Salesforce Don’t Ruin It",
  "appVersion": "26.10.40",
  "updatedAt": "2026-11-04T12:52:29Z",
  "matchedKeywords": [],
  "sourceUrl": "https://itunes.apple.com/rss/customerreviews/page=1/id=618783545/sortby=mostrecent/json?cc=us"
}
```

Each complete row also includes `schemaVersion`, `observationId`, `contentHash`, `previousContentHash` and `observedAt`. `matchedKeywords` contains the supplied terms found in the title or text, using case-insensitive substring matching. An empty array means no keyword filter was supplied.

| Field | Meaning |
| --- | --- |
| `reviewKey` | Stable app/country/review identity, suitable for an upsert key. |
| `observationId` | SHA-256 identifier for that review identity and content version. |
| `changeType` | `observed` without a supplied baseline; otherwise `new`, `updated` or `unchanged`. `changesOnly` excludes unchanged rows. |
| `contentHash` | Fingerprint of normalized rating, title, text and app version. A timestamp-only change does not count as an edit. |
| `previousContentHash` | Prior fingerprint for the same review, or null. The minimal snapshot does not retain previous text or rating values. |
| `updatedAt` | Apple's source timestamp, normalized to UTC. |
| `observedAt` | UTC time this run began collecting its frozen result plan. |
| `appVersion` | Version reported with the review, or null when absent. |

`new` means absent from your supplied snapshot. It does not prove the review was first written during this run. Passing `previousReviews: []` intentionally starts an empty baseline. Omitting it keeps the status `observed`; `changesOnly: true` requires an explicit baseline.

### Inputs and bounds

| Input | Default | Accepted scope |
| --- | --- | --- |
| `apps` | Required | 1–10 app IDs or official HTTPS Apple App Store URLs. Duplicate IDs are removed. |
| `countries` | `["us"]` | 1–5 supported country codes; maximum 20 app-country pairs. Duplicates are removed. |
| `maxPages` | 1 | 1–10 pages per app-country, up to 50 reviews per page. |
| `maxResults` | 100 | 1–1,000 delivered reviews across the run. |
| `minRating`, `maxRating` | 1, 5 | Inclusive integer rating range. |
| `keywords` | `[]` | Up to 20 terms, 100 characters each; any case-insensitive title/text substring matches. |
| `versions` | `[]` | Up to 20 exact app version strings; empty accepts all versions. |
| `since` | Omitted | Inclusive ISO-8601 timestamp with timezone, for example `2026-11-01T00:00:00Z`. |
| `changesOnly` | false | Deliver only new/updated rows compared with explicit `previousReviews`. |
| `previousReviews` | Omitted | Up to 20,000 minimal snapshot records. |
| `maxRunSeconds` | 180 | 60–240 seconds, including a 25-second collection reserve for delivery. Use a platform timeout of at least 300 seconds. |
| `requestTimeoutSeconds` | 20 | 3–30 seconds per bounded source request including safe retries and robots checks. |

Supported codes: `ae ar at au bd be br ca ch cl cn co cz de dk eg es fi fr gb gr hk hu id ie il in it jp ke kr lk mx my ng nl no nz pe ph pk pl pt ro sa se sg th tr tw ua us vn za`.

Scopes are processed in app order, then country order, with pages from most recent onward. A small overall result or charge cap can stop before later countries or apps. Inspect `coverage` rather than assuming every supplied scope was visited. `since` and filters apply within the fetched recent window; they do not extend it or request historical backfill.

### Coverage, snapshots and rollups

`OUTPUT.complete` means all requested **bounded** windows were inspected without source errors and every planned matching row was delivered. It never means the app's full review history was collected. Each app-country reports pages fetched, source/invalid/duplicate entry counts, planned/delivered reviews, `requestedWindowComplete`, `recentFeedExhausted` and a stop reason. `configured_page_limit` is the requested boundary, while `feed_exhausted` means a short page was observed at that time.

The snapshot carries forward earlier records and updates only rows actually delivered. It does not advance filtered, skipped or undelivered reviews. At 20,000 records, additional new review identities are skipped without charge, `snapshot_limit` is reported, and existing review updates remain eligible. The Actor never silently evicts baseline hashes; rotate or partition your baseline intentionally when that bound is reached. Changing a baseline's scope can make previously unknown reviews appear new.

`versionCountryRollups` groups the **delivered sample** by app, country and version. It includes the sample size, average rating, counts for each star rating and change counts. These values are affected by your filters, result caps and current feed window. They are not the app's overall store rating or a representative survey of all customers.

Source failures and malformed entries are available in `OUTPUT.errors` without a review event charge. A fully failed source collection fails the run; a valid empty match succeeds. Partial results are retained and marked partial. The Actor does not infer deleted reviews from missing entries, since Apple's recent window moves and can omit older or edited reviews.

### Billing and recovery

Each delivered default-dataset row triggers the single `apify-default-dataset-item` event. There is no second custom charge. The run's remaining charge allowance limits both collection and delivery; a zero allowance fetches no feeds. Examples: 3 delivered rows cost $0.0015, 50 cost $0.025, and 200 cost $0.10 in Actor events. Platform charges outside the Actor's event price, if applicable to your account, are shown by Apify.

Results are frozen before delivery. Within the same run's resurrection, the Actor compares saved output with that frozen plan and native event counts, then resumes without reposting confirmed rows. A lost write response leaves a pending checkpoint; an unresolved append fails safely instead of risking a repeated billable write. A **new run** with `changesOnly: false` intentionally returns and charges its selected reviews again. Use the snapshot workflow to avoid repeatedly purchasing unchanged reviews.

### Source and limitations

- Only public Apple customer-review RSS JSON feeds are requested. There is no App Store Connect login or private review access.
- Source URLs use the fixed `itunes.apple.com` host and explicit `cc` country selection. Supplied App Store URLs are parsed for IDs, never fetched directly. Links embedded in source data are never followed.
- Robots policy is respected. DNS must resolve only to public destinations; HTTPS, redirects, response sizes, request counts and execution time are bounded. No deployment credential is sent to Apple.
- Apple exposes a recent window of at most 10 pages and 50 reviews per page for this endpoint. It may rate-limit, change formats, return fewer reviews or omit older edits. HTTP 429/503 stops further same-origin collection in that run.
- Live pagination can change while a run is in progress. Duplicate review identities are not delivered twice; contradictory content or repeated pages mark coverage incomplete.
- Reviews are preserved as user-written text and are untrusted content. No reviewer names or author IDs are included. The Actor does not verify claims made in reviews.
- A 20 MiB encoded plan bound, a 20,000-entry snapshot bound and per-field size limits keep processing finite. A limit or malformed field is reported rather than silently billed as a complete review.

### Local verification

The project includes deterministic fixtures for country provenance, parser shape changes, malformed/empty responses, filters, pagination, edits, snapshot limits, deadlines and retry safety. Runtime failure injection exercises the pinned Apify client's actual dataset write path before commit, after commit with a lost response, and on restart with pending rows visible or unavailable.

```bash
uv run python -B -m unittest discover -s tests -v
uv run python -B tools/local_smoke.py --cases /path/to/private/cloud-cases.json --case full
uv run python -B tools/validate_cloud.py /path/to/private/run /path/to/private/cloud-cases.json full /path/to/private/source-evidence
```

Raw live feed evidence remains in ignored private storage. The source validator independently compares rating, title, text, version and UTC timestamp against Apple's original entries; dataset schema validation alone is not treated as output correctness.

# Actor input Schema

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

1-10 numeric IDs or https://apps.apple.com/.../id... links. URLs are parsed for their ID; countries below select the storefronts. At most 20 app-country pairs.

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

1-5 country codes, such as us, gb, de, fr or ca. Each country has its own public review feed. See README for supported codes.

## `maxPages` (type: `integer`):

Apple returns up to 50 reviews per page, up to 10 recent pages. This is a recent window, not full history.

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

Overall result cap across all apps and countries. Unfetched or undelivered reviews never advance the snapshot.

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

Include reviews rated at least this many stars, inclusive. Must not exceed maximum rating.

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

Include reviews rated at most this many stars, inclusive. Must not be below minimum rating.

## `keywords` (type: `array`):

Optional case-insensitive substring matches in title or text; any keyword matches. Up to 20 terms, each 100 characters. Empty means all.

## `versions` (type: `array`):

Optional exact version strings, such as 5.2.1. Empty means all. Reviews with an unavailable version do not match an explicit version filter.

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

Optional inclusive ISO-8601 timestamp with timezone, for example 2026-11-01T00:00:00Z. The bounded recent feed is still scanned; older history is not backfilled.

## `changesOnly` (type: `boolean`):

Requires an explicit previousReviews array. An empty array is an intentional empty baseline. Without a baseline, reviews are marked observed.

## `previousReviews` (type: `array`):

Paste OUTPUT.snapshot.previousReviews from the previous run. Each row contains only appId, country, reviewId and contentHash. Maximum 20000 records. This input remains yours; no shared cross-run state is used.

## `maxRunSeconds` (type: `integer`):

Includes collection and delivery; collection reserves 25 seconds for persisted results. Platform timeout should be at least 300 seconds.

## `requestTimeoutSeconds` (type: `integer`):

Bounded safe retries, redirects, DNS and robots checks share this hard limit.

## Actor input object example

```json
{
  "apps": [
    "618783545"
  ],
  "countries": [
    "us"
  ],
  "maxPages": 1,
  "maxResults": 100,
  "minRating": 1,
  "maxRating": 5,
  "keywords": [],
  "versions": [],
  "changesOnly": false,
  "maxRunSeconds": 180,
  "requestTimeoutSeconds": 20
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

# 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": [
        "618783545"
    ],
    "countries": [
        "us"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("natstandridge/app-store-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 = {
    "apps": ["618783545"],
    "countries": ["us"],
}

# Run the Actor and wait for it to finish
run = client.actor("natstandridge/app-store-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 '{
  "apps": [
    "618783545"
  ],
  "countries": [
    "us"
  ]
}' |
apify call natstandridge/app-store-review-monitor --silent --output-dataset

```

## MCP server setup

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