# App Store and Google Play Reviews to Complaint Themes (`fractionalhqforyou/app-review-themes`) Actor

Collects recent App Store and Google Play reviews for one or more apps and turns them into the top complaint and praise themes, each with a count and verbatim quotes tied to review ids. Bring your own OpenAI key for the themes step; without it you still get the reviews.

- **URL**: https://apify.com/fractionalhqforyou/app-review-themes.md
- **Developed by:** [Jessy Mariau](https://apify.com/fractionalhqforyou) (community)
- **Categories:** AI, E-commerce
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 app analyseds

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?

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 and Google Play Reviews to Complaint Themes

A thousand one-star reviews reads as a wall of text. Somewhere in there are the handful of things that are actually breaking, and the way most teams find them is by having someone read for an afternoon and come back with a feeling.

There are plenty of Actors that hand you app reviews as rows. This one does that too, and then does the part that takes the afternoon: it groups the reviews into the themes that repeat, counts how many reviews each theme covers, and attaches up to three real quotes per theme so you can check the label against what someone actually wrote.

### What it does

You give it apps. An App Store id like `id334235181`, a Google Play package name like `com.deliveroo.orderapp`, or a URL copied straight out of either store. Mix the two stores in one run if you want the same app from both.

It collects the recent reviews, applies whatever filters you set, and writes one row per review. On each row: the star rating, the title and body, the app version it was left against, the date, the storefront, the developer's public reply where there is one, and a link back.

Then, if you gave it an OpenAI key, it makes one model call per app and writes a second row: `complaint_themes` and `praise_themes`, each with a label, a count and up to three quotes, plus a `readout` that is one paragraph a product owner can read in twenty seconds.

Every quote is checked before it is written. The model returns a quote and the id of the review it says it came from, and the quote is only kept if it appears character for character in that review, ignoring case and whitespace. Anything that does not match is thrown away and counted in `quotes_failed_verification`. A paraphrase cannot survive that check, so a quote in the output is text a real person typed.

### What you need

Nothing for the reviews. Both stores publish them and neither needs a key, a proxy or a browser.

Your own OpenAI key for the themes. Paste it into `openaiApiKey`; Apify stores it as a secret and it never reaches the log or the dataset. The themes step is one call per app on `gpt-5-mini` by default, which is fractions of a penny for a hundred reviews, and it is billed by OpenAI to you, not by me.

Run it with no key and you still get every review, with the themes row marked to say the step was skipped. Run it with no input at all and you get a demo dataset, nothing charged and nothing fetched, so you can see the shape before you connect anything.

### How to run it

An App Store app, complaints only:

```json
{
  "apps": "id334235181",
  "country": "gb",
  "starsFilter": ["1", "2"],
  "maxReviewsPerApp": 100,
  "openaiApiKey": "your-openai-key"
}
```

A Google Play package, everything from this year, both storefronts tried in order:

```json
{
  "apps": "com.hermescourier.app",
  "country": "gb, us",
  "newerThan": "2026-01-01",
  "maxReviewsPerApp": 200,
  "openaiApiKey": "your-openai-key"
}
```

Store URLs work as well as ids, so you can paste from the address bar:

```json
{
  "apps": "https://apps.apple.com/gb/app/id334235181\nhttps://play.google.com/store/apps/details?id=com.deliveroo.orderapp"
}
```

The `OUTPUT` record in the key-value store holds the same themes rows plus a one-line summary of the run.

### Output fields

Review rows, `type: "review"`:

| Field | What it holds |
|---|---|
| `app`, `appId`, `store` | the app name, its id or package, and which store the row came from |
| `reviewId` | the store's own id for the review |
| `rating`, `title`, `text` | stars 1 to 5, the review title where the store has one, the body |
| `version` | the app version the review was left against |
| `publishedDate` | when it was posted |
| `country` | the storefront the review was read from |
| `author` | the display name the reviewer chose |
| `reply`, `replyDate` | the developer's public reply, on Google Play |
| `url` | the review on Google Play, or the app's reviews page on the App Store |

Themes rows, `type: "themes"`, one per app:

| Field | What it holds |
|---|---|
| `complaint_themes` | up to 8 problems, most frequent first: `label`, `count_estimate`, `quotes` |
| `praise_themes` | up to 5 things people like, same shape |
| `quotes` | each one `review_id`, `quote`, `rating`, `url` |
| `readout` | one paragraph of plain English about what the reviews say |
| `reviews_analysed`, `reviews_collected` | how many went into the themes, and how many came back before filtering |
| `rating_breakdown` | 1 to 5 star counts across the reviews analysed |
| `store_rating`, `store_rating_count`, `store_histogram` | the store's own numbers; the histogram is Google Play only |
| `model`, `tokens` | which model ran and what it used |
| `quotes_failed_verification` | quotes the model returned that were not verbatim, and were dropped |
| `collection_note` | what the store did or did not give back on this run |
| `note` | why a step was skipped, when one was |

### Pricing

Pay per event: one `app-analysed` charge per app that came back with something. Reviews are not charged for separately, so a hundred reviews and five hundred reviews cost the same. An app that returned nothing at all, or a line that was not a valid id, costs nothing. The demo run costs nothing.

### Limits

Read these before you plan a run on them.

**Apple's public feed is capped and it is moody.** The customer-reviews RSS feed serves at most 10 pages of 50, so 500 is the hard ceiling. In practice almost nobody reaches it: most of those 10 pages come back empty at any given moment, and which pages answer is a property of Apple's cache rather than of the app. Measured on 5 September 2026 in the GB storefront: Trainline answered on 2 of 10 pages for 100 reviews, Monzo on 4 pages for 200, Deliveroo and Facebook on 1 page each for 50, and Evri Courier Community answered on 10 of 10 pages for 113 reviews in the morning and on none at all two hours later. Retrying the same empty page does not help, and neither does switching storefront when the whole app has gone quiet. So the Actor asks for all 10 pages every time rather than stopping at the first empty one, and `collection_note` tells you exactly which pages answered. If an app comes back thin, run it again later.

**Google Play is the reliable half.** 150 reviews a page, real pagination, and the 500 cap is the Actor's, not the store's. Play has no server-side star filter, so `starsFilter` is applied after collection and the Actor over-fetches to compensate.

**Both stores show you recent reviews, not the full history.** Neither of these is an archive. You are reading the top of the pile, newest first, which is what you want for "what is breaking now" and is not what you want for a five-year trend.

**Some fields exist on one store only.** Google Play publishes developer replies, a per-star histogram and a permalink for every review. Apple's feed publishes none of those three. On App Store rows that means a null `reply`, a null `store_histogram`, and a review `url` pointing at the app's reviews page rather than at the review. Neither store attaches a reviewer's country to a review, so `country` is the storefront it was read from.

**The themes are a model reading the reviews.** The quotes are verified verbatim, and the count of any that failed is on the row. The labels, the counts and the readout are the model's own wording and its own arithmetic over the reviews you collected. Treat them as a fast, checkable summary of that sample, not as a measurement of your whole user base.

### Pair it with a workflow

If you run n8n, my free templates on https://n8n.io/creators/fractional-hq take output like this and turn it into a weekly digest, a ticket per theme or a Slack post, without another tool in the chain.

How this copes with Apple's moody feed, and what it will not tell you, is written up at https://fractionalhq.uk/use-cases/app-review-themes

### Built by Fractional HQ

I run Fractional HQ, a one-man studio for design and automation. I do not write code by hand: I architect, review and hold the standard while AI agents do the typing. That method runs my own estate of seven brands, on a Postgres operations system with more than 100 scheduled jobs. I publish n8n templates. I am Zapier Silver certified.

Want the themes wired into a weekly report, or the complaints turned into a fix list for your support team? Get in touch: https://fractionalhq.uk/brief

# Actor input Schema

## `apps` (type: `string`):

One app per line. An App Store id (id123456789), a Google Play package name (com.example.app), or a store URL from either. Leave empty to get a demo dataset that charges nothing.

## `country` (type: `string`):

Two-letter country code, or several separated by commas. The first one that returns reviews is used, and the row records which. Default gb.

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

Newest first. Hard cap 500, which only Google Play reaches in practice: Apple's public feed usually returns far fewer and says so on the row.

## `starsFilter` (type: `array`):

Leave empty for all ratings. Pick 1 and 2 to feed the themes step complaints only.

## `newerThan` (type: `string`):

For example 2026-01-01. Leave empty for whatever the store returns.

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

Used for Google Play, for example en, fr, de. Apple's feed is served per storefront and takes no language parameter.

## `openaiApiKey` (type: `string`):

Your own key from platform.openai.com. It pays for the themes step and is stored as a secret. Without it the Actor still collects the reviews and the themes row says the step was skipped.

## `themeModel` (type: `string`):

Any OpenAI model that supports structured JSON output.

## Actor input object example

```json
{
  "apps": "id1568298035\ncom.hermescourier.app",
  "country": "gb",
  "maxReviewsPerApp": 100,
  "language": "en",
  "themeModel": "gpt-5-mini"
}
```

# Actor output Schema

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

One row per review (type review) and one summary row per app (type themes) with the complaint and praise themes, counts, verbatim quotes and a plain-English readout.

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

The OUTPUT record: one entry per app with its themes row, plus a one-line text summary.

# 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": `id1568298035
com.hermescourier.app`
};

// Run the Actor and wait for it to finish
const run = await client.actor("fractionalhqforyou/app-review-themes").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": """id1568298035
com.hermescourier.app""" }

# Run the Actor and wait for it to finish
run = client.actor("fractionalhqforyou/app-review-themes").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": "id1568298035\\ncom.hermescourier.app"
}' |
apify call fractionalhqforyou/app-review-themes --silent --output-dataset

```

## MCP server setup

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

```

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/2ENxRIp8bbY5ofKPA/builds/6T1zqzZPgC2zOI8e7/openapi.json
