# App Store Review Insights – Topics & Version Regressions (`skuang/app-store-review-insights`) Actor

Pull recent iOS App Store reviews for any app and country, tagged by topic, plus a per-app report: rating by version with regression flags, complaint topics, top negative phrases and quotes. Deterministic rules, no AI.

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

## Pricing

from $0.20 / 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?

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 Insights

Pull the most recent iOS App Store reviews for any app, in any App Store country, and get a per-app report of what
users complain about: rating by app version (with regression flags), complaint topics, the most common phrases in
1–2★ reviews, and short quotes. Everything is computed with fixed keyword rules and counts, so the same reviews always
give the same report. No AI model is involved.

### What it does

1. For every app and country you list, it reads Apple's public customer-review feed (newest first, up to 500 reviews
   per app and country) and looks up the app's name.
2. It tags each review with topics such as `crash_bug`, `ads` or `login_account` using keyword rules.
3. For each app it builds one **insights report** across all countries, with a per-country breakdown.
4. It saves the reviews and/or reports to the dataset and writes a run summary (`OUTPUT`) to the key-value store.

### Who it is for

- **App product managers** checking how a release landed: did the average rating drop on the new version, and what
  do 1–2★ reviewers mention?
- **ASO and app-marketing agencies** producing recurring review reports for clients across countries.
- **Competitor research**: compare complaint topics and ratings of several apps side by side.

### Input

| Field                  | Type             | Default                | Description                                                                                                     |
| ---------------------- | ---------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------- |
| `apps`                 | array of strings | required               | Up to 50 App Store URLs (`https://apps.apple.com/us/app/instagram/id389801252`) or numeric IDs (`389801252`).   |
| `countries`            | array of strings | `["us"]`               | Up to 30 two-letter App Store country codes. `uk` is accepted as `gb`.                                          |
| `maxReviewsPerCountry` | integer          | `500`                  | 1–500. Apple's feed returns at most 500 reviews per app and country.                                            |
| `sinceDate`            | string           | empty                  | Optional `YYYY-MM-DD`. Keeps only reviews from that date (UTC) on and stops paging once older reviews appear.   |
| `outputMode`           | string           | `reviews_and_insights` | `reviews_and_insights`, `insights_only`, or `reviews_only`.                                                     |
| `includeAuthorNames`   | boolean          | `false`                | Add the reviewer's App Store nickname to each review. Off by default to keep personal data out of your dataset. |

Example:

```json
{
    "apps": ["https://apps.apple.com/us/app/instagram/id389801252", "310633997"],
    "countries": ["us", "gb"],
    "maxReviewsPerCountry": 500,
    "outputMode": "reviews_and_insights"
}
```

An entry that is not an App Store URL or ID, or a country code that is not two letters or that Apple does not
recognize, is recorded as failed in `OUTPUT`; the rest of the run continues.

### Output

The dataset holds two kinds of items, told apart by `type`. The **Reviews** and **Insights** views in the Output tab
show one kind each (rows of the other kind appear with empty columns; filter by `type` when exporting).

#### Review item (`type: "review"`)

```json
{
    "type": "review",
    "appId": "389801252",
    "appName": "Instagram",
    "country": "gb",
    "reviewId": "14649988546",
    "rating": 1,
    "sentiment": "negative",
    "title": "Rubbish",
    "text": "The latest update is terrible. If I want to pause a reel I have to hold the screen. ...",
    "version": "450.1.0",
    "date": "2026-10-10T14:26:36.000Z",
    "topics": ["crash_bug"],
    "appUrl": "https://apps.apple.com/us/app/instagram/id389801252"
}
```

- `sentiment` comes from the star rating only: 1–2★ `negative`, 3★ `neutral`, 4–5★ `positive`. The review text is
  not analyzed for sentiment.
- `date` is the review's timestamp from the feed, in UTC.
- `authorName` is present only when `includeAuthorNames` is on.

#### Insights item (`type: "insights"`, one per app)

Trimmed example from a run on Instagram, `us` + `gb`, 100 reviews each (2026-10-12):

```json
{
    "type": "insights",
    "appId": "389801252",
    "appName": "Instagram",
    "appUrl": "https://apps.apple.com/us/app/instagram/id389801252",
    "countries": ["us", "gb"],
    "reviewsAnalyzed": 200,
    "summary": "Instagram: 200 reviews analyzed (US, GB) from 2026-10-01 to 2026-10-10. Average rating 2.46/5; 63% are 1–2★ and 33% are 4–5★. Most frequent topics in 1–2★ reviews: UI & usability (18, 14%), Crashes & bugs (16, 13%), Login & account (8, 6%). Rating drops of 0.5★ or more between versions: 450.1.0 averages 2.16★ over 160 reviews vs 3.57★ on 449.0.0. Most common phrase in 1–2★ reviews: \"full screen\" (11 reviews).",
    "dateRange": { "from": "2026-10-01T15:06:53.000Z", "to": "2026-10-10T16:26:18.000Z" },
    "averageRating": 2.46,
    "ratingDistribution": { "1": 109, "2": 16, "3": 10, "4": 5, "5": 60 },
    "sentimentCounts": { "negative": 125, "neutral": 10, "positive": 65 },
    "averageRatingByVersion": [
        {
            "version": "449.0.0",
            "reviews": 21,
            "averageRating": 3.57,
            "regression": false,
            "previousVersion": "448.0.0",
            "changeFromPrevious": -0.23
        },
        { "version": "450.0.0", "reviews": 4, "averageRating": 3.25, "regression": false, "lowSample": true },
        {
            "version": "450.1.0",
            "reviews": 160,
            "averageRating": 2.16,
            "regression": true,
            "previousVersion": "449.0.0",
            "changeFromPrevious": -1.41
        }
    ],
    "topicCounts": { "crash_bug": "...", "...": "reviews per topic, all ratings" },
    "negativeTopicCounts": {
        "crash_bug": 16,
        "login_account": 8,
        "subscription_pricing": 1,
        "ads": 7,
        "performance_battery": 1,
        "ui_ux": 18,
        "feature_request": 3,
        "customer_support": 3,
        "privacy_data": 0,
        "sync_data_loss": 1,
        "notifications": 1
    },
    "topNegativePhrases": [
        { "phrase": "full screen", "words": 2, "count": 11, "exampleReviewId": "14649966281" },
        { "phrase": "new update", "words": 2, "count": 11, "exampleReviewId": "14650404979" },
        { "phrase": "latest update", "words": 2, "count": 10, "exampleReviewId": "14649988546" }
    ],
    "sampleQuotes": [
        {
            "topic": "ui_ux",
            "label": "UI & usability",
            "negativeReviews": 18,
            "quotes": [
                {
                    "reviewId": "14650333233",
                    "country": "us",
                    "rating": 1,
                    "version": "450.1.0",
                    "quote": "The new ‘Shop the Look’ button is ruining Reels."
                }
            ]
        }
    ],
    "perCountry": [
        {
            "country": "us",
            "reviewsAnalyzed": 100,
            "averageRating": 2.07,
            "...": "same rating and topic fields per country"
        },
        { "country": "gb", "reviewsAnalyzed": 100, "averageRating": 2.84, "...": "..." }
    ]
}
```

The report is computed from every review fetched, also in `insights_only` mode.

#### Run summary (`OUTPUT` in the key-value store)

For every app and country: `status` (`ok`, `empty`, `failed`, or `skipped`), reviews collected and saved, pages
fetched, why paging stopped, and any error. Also totals, and whether the run stopped at your spending limit.

- `ok`: at least one review collected (`partial: true` if a later page failed after retries).
- `empty`: the app exists but has no reviews in that country (or none since `sinceDate`).
- `failed`: invalid input, unknown country, app not available in that country, or the feed could not be read.
- `skipped`: not processed because the run reached its spending limit or was aborted.

The run fails only when every app and country failed.

### How the insights are computed

- **Topics.** Each review's title and text are lowercased and matched against regular-expression rules for 11 topics:
  `crash_bug`, `login_account`, `subscription_pricing`, `ads`, `performance_battery`, `ui_ux`, `feature_request`,
  `customer_support`, `privacy_data`, `sync_data_loss`, `notifications`. A review can have several topics or none.
  The rules are English keyword rules kept in one file (`src/topic-rules.js`); they are tested against a hand-written
  set of example reviews, which is not a measure of accuracy on real reviews. Expect some misses and some wrong tags,
  especially for sarcasm, misspellings, and non-English text.
- **Topic counts.** `topicCounts` counts reviews per topic across all ratings; `negativeTopicCounts` only within 1–2★
  reviews.
- **Rating by version.** Reviews are grouped by the app version the reviewer was on, sorted by version number. A
  version is flagged `regression: true` when it has at least 5 reviews and its average is at least 0.5★ below the
  closest earlier version that also has at least 5 reviews. Versions with fewer than 5 reviews are listed with
  `lowSample: true` and are neither flagged nor used for comparison.
- **Top negative phrases.** From 1–2★ reviews: text is split at punctuation, common English stopwords, numbers and the
  app's own name into runs of adjacent words, and the 2- and 3-word sequences inside each run are counted. Words that
  were not next to each other in the review are never joined ("banned for no reason" gives "no reason", not "banned
  no"). `count` is the number of
  reviews that contain the phrase (each review counts once); phrases need at least 2 reviews. A shorter phrase is
  dropped when a longer listed phrase containing it has the same count. Negations such as "can't" and "not" are kept.
- **Sample quotes.** For the 3 most frequent topics in 1–2★ reviews, up to 3 of the most recent matching reviews,
  quoting the first sentence that matches the topic, cut to at most 200 characters.
- **Summary.** A sentence template filled with the numbers above.

### Pricing

Pay per event:

| Event             | When it is charged                                                                                       | Intended price                      |
| ----------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `review-scraped`  | Each review item saved to the dataset                                                                    | $0.0002 (= $0.20 per 1,000 reviews) |
| `insights-report` | Each insights item saved to the dataset, once per country it covers (countries with at least one review) | $0.02 per app per country           |

The Pricing tab shows the current prices. Items are saved and charged together: the Actor only saves as many items
as your spending limit allows and charges exactly for those. When the limit is reached it stops fetching, saves the
`OUTPUT` summary, and says so in the run status. An insights report is saved only if all of its per-country events
fit. Insights are saved before the reviews of the same app.

Example: 2 apps × 2 countries × 500 reviews with `reviews_and_insights` = 2,000 reviews + 2 reports covering 2
countries each = $0.40 + 4 × $0.02 = $0.48. A country where the app has no reviews is not charged.

### Limitations

- Apple's feed returns at most the **500 most recent reviews per app and country** (10 pages of 50). For busy apps
  that may cover only a few days; check `dateRange` in the report. Older reviews are not available from this source.
- **Ratings without a written review are not in the feed**, so averages here are averages of written reviews and
  differ from the star rating shown on the App Store.
- Recent reviews are dominated by the latest version, so older versions often have few reviews in the sample. Use
  `lowSample` and the review counts when reading version comparisons.
- Topic rules and stopwords are English. Reviews in other languages are still collected and counted, but will mostly
  get no topics and less useful phrases.
- Reviews are fetched politely (at most 3 app and country pairs at a time, requests at least 0.5 s apart, up to 3
  retries on rate limits, server errors and timeouts), so large runs take a while: roughly 6 s per app and country
  at 500 reviews.
- This Actor reads a public Apple feed that Apple can change or limit at any time.

### FAQ

**Can I get more than 500 reviews per country?** Not from this feed. Run the Actor on a schedule and keep the
datasets to build a longer history.

**Why is a country marked failed?** Either the code is not a valid App Store country (Apple answers HTTP 400), or the
app is not available in that country. The error in `OUTPUT` says which.

**Is the sentiment from AI?** No. It is derived from the star rating only.

**Can I add or change topics?** Yes, if you run your own copy: edit `src/topic-rules.js` (one entry per topic with a
list of regular expressions) and add example reviews to `test/tagger.test.js`.

**Does it store reviewer names?** Only when `includeAuthorNames` is on.

**What happens if the run is moved to another server?** The Actor records each delivered insights report and each
country's reviews in a `PROGRESS` record as soon as they are saved and charged. When Apify restarts the run elsewhere,
it skips what was already delivered, so you are not charged twice. `PROGRESS` is deleted when the run finishes.

# Actor input Schema

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

Up to 50 App Store URLs (for example https://apps.apple.com/us/app/instagram/id389801252) or numeric app IDs (389801252). Entries that are neither are reported as failed in the OUTPUT record; the rest of the run continues.

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

Up to 30 two-letter App Store country codes (us, gb, de, jp, ...). Each app is fetched separately in each country. "uk" is accepted as an alias for "gb". Codes Apple does not recognize are reported as failed for that app and country; the rest of the run continues.

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

Stop after this many reviews per app and country. Apple's feed returns at most 500 of the most recent reviews (10 pages of 50).

## `sinceDate` (type: `string`):

Optional. Keep only reviews posted on or after this date (YYYY-MM-DD, midnight UTC) and stop paging once older reviews appear. Leave empty to take the most recent reviews up to the limit above.

## `outputMode` (type: `string`):

What to save to the dataset. The insights report is always computed from every review fetched, even when individual reviews are not saved.

## `includeAuthorNames` (type: `boolean`):

Add the reviewer's App Store nickname to each review item. Off by default to keep personal data out of your dataset.

## Actor input object example

```json
{
  "apps": [
    "https://apps.apple.com/us/app/instagram/id389801252",
    "310633997"
  ],
  "countries": [
    "us",
    "gb"
  ],
  "maxReviewsPerCountry": 500,
  "outputMode": "reviews_and_insights",
  "includeAuthorNames": false
}
```

# Actor output Schema

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

No description

## `insights` (type: `string`):

No description

## `runSummary` (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": [
        "https://apps.apple.com/us/app/instagram/id389801252",
        "310633997"
    ],
    "countries": [
        "us",
        "gb"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("skuang/app-store-review-insights").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": [
        "https://apps.apple.com/us/app/instagram/id389801252",
        "310633997",
    ],
    "countries": [
        "us",
        "gb",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("skuang/app-store-review-insights").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": [
    "https://apps.apple.com/us/app/instagram/id389801252",
    "310633997"
  ],
  "countries": [
    "us",
    "gb"
  ]
}' |
apify call skuang/app-store-review-insights --silent --output-dataset

```

## MCP server setup

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

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/B74MXNyxMxZeeSKs0/builds/jJkxg4RI2JjybVU4f/openapi.json
