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

User reviews of iOS and Android apps in one Actor, with developer replies where the store publishes them.

- **URL**: https://apify.com/lanternlab318/app-reviews.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Categories:** Developer tools, E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

## App Store & Google Play Reviews Scraper

A Lantern Lab Actor. Give it iOS and Android apps and get their user reviews: one row per review with the rating, title, text, app version, publish time, reviewer name and - on Google Play - the developer's public reply. Both stores in one run, any storefront, including the Chinese App Store. No login, no cookies, no account of your own.

Every app and country gets a summary row saying whether its reviews were collected in full. An app that was cut short is marked `partial: true` with the reason, and one that could not be read is `failed` - neither is ever reported as "0 reviews".

### What it does

- Reads App Store reviews from Apple's public customer-review feed, page by page, and Google Play reviews from the store page's own review endpoint, following its pagination.
- Takes both stores in one `apps` list: App Store links or IDs, Google Play links or package names, mixed freely.
- Reads every app once per country in `countries`, so one line covers as many storefronts as you need.
- Carries the developer's reply on Google Play rows: who answered, what they wrote and when.
- Says per app and country how complete the result is, and ends with a `RUN_SUMMARY` holding the run totals, the request count and the reason the run stopped.

### What it does not do

- No app details, charts, or store search. Reviews only.
- No sentiment scores and no AI summaries. The text is the store's, unchanged.
- **No developer replies on App Store rows.** Apple's public review feed does not carry them, so `developerReply` is `null` on every App Store row and the summary row says `developerRepliesAvailable: false`. Google Play rows carry the real reply. The field is never filled with a guess.
- No review titles on Google Play rows: that store has no title field, so `title` is `null`.
- No guessing when a store returns nothing for an app in a storefront: that pair is reported `not_found` with the reason, never as an app without reviews. For the App Store the empty feed is checked against Apple's public lookup first, so "no such app" and "no reviews yet" stay apart.
- No scheduling or monitoring built in; use Apify schedules.

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `apps` | - | One app per line: an App Store link (`https://apps.apple.com/us/app/id284882215`), a bare App Store ID (`284882215` or `id284882215`), a Google Play link (`https://play.google.com/store/apps/details?id=com.spotify.music`), or a bare package name (`com.spotify.music`). Required. |
| `countries` | `["us"]` | Two-letter storefront codes. Each app is read once per country. |
| `maxReviewsPerApp` | `200` | Reviews per app **and country** (1-5000). |
| `sort` | `newest` | `newest`, `mostHelpful`, or `rating`. |
| `maxUpstreamRequests` | derived | Hard ceiling on store requests for the whole run, counted in HTTP attempts. |

Example:

```json
{
  "apps": [
    "https://apps.apple.com/us/app/id284882215",
    "com.spotify.music"
  ],
  "countries": ["us", "gb"],
  "maxReviewsPerApp": 200,
  "sort": "newest"
}
```

The same app entered twice is read once. The country in an App Store link is ignored: `countries` decides which storefronts are read.

Two store limits are reported rather than worked around:

- The public App Store feed holds at most **500 reviews per app and country**. Ask for more and that pair ends `partial` with `partialReasonCode: "store_page_limit"`.
- That feed has no rating sort. With `sort: "rating"` an App Store app is read newest-first and its summary row shows `sortRequested: "rating"`, `sortApplied: "newest"`. Google Play supports all three orders.

`maxUpstreamRequests` gives you a known upper bound before the run starts. Left empty, the Actor derives one from your input - `(sum over app/country pairs of ceil(reviews wanted / 50) + 1) x 2 + 2`, which is 12 for one app, one country and 200 reviews - so a run is never uncapped. When the ceiling is reached, the pair being read is marked `partial` with `partialReasonCode: "request_cap"`, every pair after it is written as `not_collected`, and the run ends normally. That is your limit, not a problem at the store.

### Output

One dataset; `recordType` tells the rows apart. Two table views are predefined: Reviews and App summaries. A `RUN_SUMMARY` record in the key-value store holds the run totals and the status of every app and country.

Review rows (`recordType: "review"`):

| Field | Meaning |
| --- | --- |
| `store` | `appStore` or `googlePlay`. |
| `appId` | Numeric App Store ID, or Google Play package name. |
| `country` | The storefront this row was read from. |
| `reviewId` | The store's own ID for the review. |
| `parentCommentId` | Always `null`: neither store threads reviews. |
| `rating` | Stars, 1-5. |
| `title` | Review title. `null` on Google Play, which has no title field. |
| `text` | The review as the user wrote it. |
| `version` | App version the review was written against, when the store publishes it. |
| `publishedAt` | When the store says it was posted (UTC). Apple updates this timestamp when a review is edited. |
| `userName` | The public display name shown next to the review. |
| `helpfulVotes` | Google Play: thumbs-up count. App Store: Apple's net helpful-vote sum. |
| `developerReply` | `{ developerName, text, repliedAt }`, or `null`. Always `null` on App Store rows. |
| `appUrl`, `reviewUrl` | Public links. `reviewUrl` is `null` on App Store rows: Apple has no per-review link. |
| `collectedAt` | When we read the row (UTC). |

App summary rows (`recordType: "app_summary"`), one per app and country:

| Field | Meaning |
| --- | --- |
| `collectionStatus` | `complete`, `partial`, `empty`, `not_found`, `failed`, or `not_collected`. |
| `partial` | `true` when this pair was not collected in full. |
| `partialReason` / `partialReasonCode` | Why collection was short, in words and as a stable code: `request_cap`, `charge_limit`, `store_page_limit`, `store_blocked`, `store_error`. The code column never carries anything else, so it is safe to branch on. |
| `statusReason` | The status in words, including statuses that have no code - for example an app that is not in this storefront. |
| `reviewsCollected` / `reviewsRequested` | What we got, and what the run asked for. |
| `duplicatesSkipped` | Reviews the store returned twice while paging; written once. |
| `pagesRead` | Store pages read for this pair. |
| `developerReplies` | How many of the collected reviews had a developer reply. |
| `developerRepliesAvailable` | Whether this store publishes replies at all. |
| `sortRequested` / `sortApplied` | What you asked for, and what the store could serve. |

Statuses in one line: `complete` - everything you asked for, or everything the store has; `partial` - cut short, reason given; `empty` - the app exists here and has no reviews; `not_found` - no such app in this storefront; `failed` - the store could not be read; `not_collected` - the run stopped before this pair was reached.

### Collection boundaries

Only what the stores show publicly, signed out. No login, no cookies, no account. Reviewer fields are limited to the display name the store prints next to the review; no avatars, profile links or other personal data are collected.

# Actor input Schema

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

One line per app. An App Store link (https://apps.apple.com/us/app/id284882215), a bare App Store ID (284882215 or id284882215), a Google Play link (https://play.google.com/store/apps/details?id=com.spotify.music) or a bare package name (com.spotify.music). Mix both stores freely. The country in an App Store link is ignored: "countries" decides which storefronts are read, so one line can cover several of them.

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

Two-letter country codes of the storefronts to read, for example us, gb, de, jp, cn. Each app is read once per country, and each app/country pair gets its own summary row. The App Store serves cn; Google Play does not, and says so in that pair's summary row instead of returning nothing.

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

Stop each app/country pair after this many reviews. The public App Store review feed holds at most 500 per app and country; ask for more and that pair is reported partial with reason "store_page_limit" rather than as if the app had no more reviews. Google Play has no such limit.

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

Order the store returns reviews in. The public App Store feed offers newest and most helpful only: with "rating" an App Store app is read newest-first and its summary row says so in sortApplied. Google Play supports all three.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the requests this run may send to the stores. Counted in HTTP attempts, so an internal retry uses one up. When the ceiling is reached, collection stops there, the app/country pair being read is written with partial: true and partialReasonCode "request_cap", and every pair after it is written as "not_collected" - results are never silently short. Leave empty and the Actor works the ceiling out from the rest of your input: (sum over app/country pairs of ceil(reviews wanted / 50) + 1) x 2 + 2, which is 12 for one app, one country and 200 reviews. RUN_SUMMARY reports the number used in requestCap and where it came from in requestCapSource.

## Actor input object example

```json
{
  "apps": [
    "https://apps.apple.com/us/app/id284882215",
    "com.spotify.music"
  ],
  "countries": [
    "us"
  ],
  "maxReviewsPerApp": 200,
  "sort": "newest"
}
```

# Actor output Schema

## `results` (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/id284882215",
        "com.spotify.music"
    ],
    "countries": [
        "us"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/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": [
        "https://apps.apple.com/us/app/id284882215",
        "com.spotify.music",
    ],
    "countries": ["us"],
}

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/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": [
    "https://apps.apple.com/us/app/id284882215",
    "com.spotify.music"
  ],
  "countries": [
    "us"
  ]
}' |
apify call lanternlab318/app-reviews --silent --output-dataset

```

## MCP server setup

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