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

App Store and Google Play reviews in one flat schema: rating, title, text, date, app version, developer reply, thumbs-up. Any country or language, star and date filters, new-review alerts, free app details. Failed apps are free.

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

## Pricing

from $0.25 / 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 - One Schema

Get public reviews from the Apple App Store and Google Play in one run, in one flat format. Every review row has the same fields for both stores, so you can sort, filter and export them together as JSON, CSV or Excel.

You give it a list of apps. You get back:

- One row per review: store, app, country, language, rating, title, text, date, app version, reviewer display name, developer reply and reply date, thumbs-up count.
- One free details row per app: name, developer, category, rating, ratings count, star histogram (Google Play), installs (Google Play), price, version, release notes and dates.
- One free status row per app and storefront, so you can see what worked and what did not.

No login. No API key. No captcha solving.

### Who uses it

- **Product and support teams** that read what users say after each release, with the app version next to each review.
- **ASO and marketing teams** that compare their app with competitors on both stores and in several countries.
- **Gulf and Middle East teams** that need Arabic reviews from Saudi Arabia, the UAE, Egypt and other stores. Arabic text comes through exactly as written.
- **AI and sentiment pipelines** that need clean text, a star label and a language code in one shape for iOS and Android.
- **Anyone who wants alerts**: schedule it with "only new reviews" and get just the reviews written since the last run.

### What you can put in

One app per line. Mix both stores freely.

| You type | What it means |
|---|---|
| `https://apps.apple.com/sa/app/careem/id592978487` | App Store app |
| `592978487` or `id592978487` | Same app, shorter |
| `https://play.google.com/store/apps/details?id=com.careem.acma` | Google Play app |
| `com.careem.acma` | Same app, shorter |

Other options:

- **Countries.** 2-letter codes such as `us`, `gb`, `sa`, `ae`, `eg`. App Store reviews belong to one country's store, so each App Store app is read once per country.
- **Languages (Google Play).** Codes such as `en`, `ar`, `fr`, `pt-br`. Google Play groups reviews by language, so each Google Play app is read once per language.
- **Max reviews per app and storefront.** "Storefront" means one App Store country, or one Google Play language. You pay only for reviews delivered.
- **Order.** Newest first, or the store's "most relevant" order.
- **Star ratings.** For example only 1 and 2 stars.
- **Reviews published on or after.** A date like `2026-01-01`, or `30 days`, or `6 months`.
- **App details row.** On by default, and free.
- **Only new reviews since the last run.** For scheduled alerts. Give each watch list its own state name.

### Output

All examples below come from a real run on 2026-10-26 (6 apps, both stores, Saudi Arabia, UAE and Egypt, Arabic and English). Long text is shortened.

#### Review row, App Store

```json
{
  "type": "review",
  "store": "appStore",
  "appId": "592978487",
  "appName": "Careem: rides, food, grocery",
  "country": "sa",
  "language": "ar",
  "languageSource": "detected",
  "reviewId": "14596533255",
  "rating": 1,
  "title": "تزويد مبالغ غير مستحقه",
  "text": "تزويد مبالغ غير مستحقه ونصب",
  "date": "2026-09-26T17:58:04.000Z",
  "appVersion": "26.37.1",
  "reviewerName": "Rrooroo",
  "developerReplyText": "يؤسفنا جداً سماع ذلك! ولكن بالتأكيد نحن هنا دائماً لمساعدتك...",
  "developerReplyDate": "2025-05-05T17:41:53.000Z",
  "thumbsUpCount": 1,
  "isEdited": true,
  "reviewUrl": null,
  "appUrl": "https://apps.apple.com/sa/app/careem-rides-food-grocery/id592978487",
  "sort": "newest",
  "source": "apps.apple.com",
  "input": "https://apps.apple.com/sa/app/careem/id592978487",
  "scrapedAt": "2026-10-26T12:07:39.302Z"
}
```

#### Review row, Google Play

Same fields. Google Play reviews have no title, so `title` is `null`.

```json
{
  "type": "review",
  "store": "googlePlay",
  "appId": "com.careem.acma",
  "appName": "كريم: احجز مشوارك في السعودية",
  "country": "sa",
  "language": "ar",
  "languageSource": "store",
  "reviewId": "652ff2e2-3b93-41ce-8b63-3fc9a3807cf9",
  "rating": 1,
  "title": null,
  "text": "التطبيق كان سابقا ممتاز في الأقصر ولكن الكباتن الان في قمة عدم الاهتمام بالعميل مثل السابق",
  "date": "2026-10-21T18:12:35.000Z",
  "appVersion": "26.41",
  "reviewerName": "Yasser Elwrdany",
  "developerReplyText": "أهلاً وسهلاً بك ياسر، نشكرك على تقييمك الرائع وهذه الكلمات الطيبة 💚",
  "developerReplyDate": "2024-08-18T07:16:34.000Z",
  "thumbsUpCount": 0,
  "isEdited": null,
  "reviewUrl": "https://play.google.com/store/apps/details?id=com.careem.acma&reviewId=652ff2e2-3b93-41ce-8b63-3fc9a3807cf9",
  "appUrl": "https://play.google.com/store/apps/details?id=com.careem.acma",
  "sort": "newest",
  "source": "play.google.com",
  "input": "com.careem.acma",
  "scrapedAt": "2026-10-26T12:07:42.640Z"
}
```

#### Field notes

- `date` is the review date in ISO format, UTC.
- `language`: for Google Play this is the language you asked for, and Google returns only reviews in that language (`languageSource: "store"`). The App Store does not split reviews by language, so the actor checks the text with a simple built-in rule: alphabet first, then common words (`languageSource: "detected"`). It is `null` when the text is too short to tell, for example a single emoji.
- `country`: the App Store country the review was written in. For Google Play it is the first country in your input, used for app details and prices.
- `appVersion`: Google Play gives it for most reviews. For the App Store it is filled for the newest 500 reviews per country (newest-first runs).
- `thumbsUpCount`: Google Play thumbs-up count. For the App Store it is Apple's "helpful" vote count, filled for the newest 500 reviews per country (newest-first runs).
- `developerReplyDate` can be older than `date`. That happens when a user edits a review after the developer replied.
- `isEdited`: App Store only.
- `reviewUrl`: Google Play only. Apple has no public link to a single review.
- Personal data: the actor keeps only the reviewer's display name. It drops profile photos and user IDs.

#### App details row (free)

```json
{
  "type": "app",
  "store": "googlePlay",
  "appId": "com.talabat",
  "appName": "طلبات: الطعام والبقالة وغيره",
  "country": "sa",
  "language": "ar",
  "developer": "Talabat",
  "genre": "طعام ومشروب",
  "rating": 3.937,
  "ratingsCount": 546165,
  "reviewsCount": 16307,
  "histogram": { "1": 121492, "2": 11285, "3": 14207, "4": 32312, "5": 366831 },
  "installs": "+50,000,000",
  "installsMin": 50000000,
  "price": 0,
  "currency": "SAR",
  "version": "14.1.0",
  "released": "2012-01-19T18:48:21.000Z",
  "updated": "2026-10-23T17:36:00.000Z",
  "minOsVersion": "Android 8.0",
  "appUrl": "https://play.google.com/store/apps/details?id=com.talabat"
}
```

App Store details rows have the same fields. They add `bundleId`, `ratingCurrentVersion`, `sizeBytes`, `languages` and `releaseNotes`. They have no histogram and no install count, because Apple does not publish them. App Store details are per country. Google Play details come once per app, in the first language and country of your input.

#### Status row (free)

```json
{"type": "status", "store": "appStore", "appId": "592978487", "appName": "Careem: rides, food, grocery", "country": "ae", "status": "ok", "reviews": 100, "requests": 8, "newestReviewDate": "2026-10-24T09:33:49.000Z", "oldestReviewDate": "2026-09-29T17:28:45.000Z", "source": "apps.apple.com reviews", "note": null, "error": null, "seconds": 8.5}
```

Status values:

- `ok`: reviews delivered.
- `partial`: some reviews delivered, then the store stopped answering. The `error` field says why.
- `no_reviews`: the app exists but has no reviews in this storefront.
- `no_matching_reviews`: reviews exist, but none matched your star or date filters.
- `no_new_reviews`: "only new" mode found nothing since the last run.
- `not_found`: the app id is wrong, the app was removed, or it is not sold in that country.
- `blocked`: the store refused every retry on every proxy tier.
- `bad_input`: the line is not an app URL, App Store id or package name.
- `skipped_cost_limit` or `cut_by_cost_limit`: your maximum cost for the run was reached.

The run always ends as SUCCEEDED, so a scheduled run never breaks your pipeline. Look at the status rows to see what happened to each app. The `RUN_SUMMARY` record in the key-value store has every status row, reviews per store, requests, bytes per 1,000 reviews and the proxy tier used.

### How it works

- **App Store reviews.** The actor reads the same JSON that the apps.apple.com web page loads when you open "Ratings and Reviews". It gives 20 reviews per request, newest first or most relevant, with developer replies, and goes deep: in a test it paged past review 5,000 for one country. No token or login is needed.
- **App Store version and helpful votes.** Apple's public review feed (RSS) carries these two fields for the newest 500 reviews per country. The actor reads that feed alongside and adds the two fields to matching reviews. The review ids are the same in both sources.
- **App Store fallback.** If the main review source fails, the actor reads the public feed instead (up to 500 reviews per country, no developer replies). The status row says when this happened.
- **App Store details.** Apple's public iTunes Lookup API.
- **Google Play reviews.** The actor sends the same request the Google Play web page sends when you scroll the reviews. It reads up to 200 reviews per request and follows Google's page tokens. A single star rating is filtered by Google itself.
- **Google Play details.** The public app page. Each field is checked for the shape it must have. If Google changes the page, the run log and `RUN_SUMMARY` say which fields failed.
- **Pacing and proxies.** Requests to each store are spaced out. Short "too many requests" answers are retried with growing waits. The actor starts without a proxy. If a store keeps refusing, it moves to Apify datacenter proxy, then residential. You can turn residential off, or set your own proxy.

### Scheduling alerts

1. Turn on **Only new reviews since the last run**. Set a **State name**, for example `my-apps`.
2. Keep **Order** on newest first. Set **Max reviews per app and storefront** to the most you want per run, for example 200.
3. In Apify Console, open **Schedules**, create a schedule (for example every day at 08:00), and pick this actor with this input.
4. The first run delivers the newest reviews as a baseline. Each later run delivers only reviews written since then. Runs with nothing new cost only the start fee.
5. To get the reviews by email or Slack, add an integration to the actor or the schedule.

Use one state name per watch list. Two schedules with the same state name share one memory.

### Pricing

$0.30 per 1,000 reviews ($0.0003 per review). Each run also has a start fee of $0.00005. Platform usage is included in the price. App details rows and status rows are free. Apps that are not found or blocked cost nothing. Set a maximum cost on the run and the actor stops cleanly when it is reached.

Measured on 2026-10-26 (local runs, 6 apps, 30 storefronts, 2,897 reviews):

- App Store: about 60 to 85 requests and 0.4 to 0.6 MB of compressed traffic per 1,000 reviews. About 40 seconds per 1,000 reviews, because requests to Apple are spaced out.
- Google Play: about 6 to 15 requests and 0.3 to 1.2 MB per 1,000 reviews. The app page (about 200 KB) is read once per app. About 4 seconds per 1,000 reviews.

### Limits

- **App Store version and helpful votes** are filled only for the newest 500 reviews per country, and only in newest-first runs. Apple publishes these two fields only in its feed, and the feed stops at 500.
- **App Store reviews are per country.** An app with reviews in 10 countries needs 10 countries in the input. Small storefronts can have few written reviews even when the app has many star ratings.
- **App Store details text** comes in Apple's default language for that country.
- **Google Play reviews do not depend on country.** The same reviews come back for every country, so the actor reads Google Play once per language.
- **Google Play shows a review after a delay.** In our tests the newest reviews were about one day old.
- **"Most relevant" order is not sorted by date.** With a date filter, use newest first. The actor stops reading after 1,000 reviews in a row that match none of your filters, and the status row says so.
- **Several star ratings at once** are filtered by the actor after reading, so a rare combination can need many requests. One star rating is filtered by Google itself on Google Play.

### Tips

- To compare competitors, put all of them in one run. Filter the dataset by `appName` or `store`.
- For a Gulf view, use countries `sa`, `ae`, `kw`, `qa`, `bh`, `om` and languages `ar`, `en`.
- To feed an AI pipeline, keep `text`, `title`, `rating`, `language` and `appVersion`. They are filled the same way for both stores.

# Actor input Schema

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

One per line, both stores mixed. App Store: a URL like https://apps.apple.com/us/app/whatsapp-messenger/id310633997, or the id 310633997. Google Play: a URL like https://play.google.com/store/apps/details?id=com.whatsapp, or the package name com.whatsapp.

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

2-letter country codes, for example us, gb, sa, ae, eg. App Store reviews belong to one country's store, so each App Store app is read once per country. Google Play reviews do not depend on the country; the first country is used for prices and app details.

## `languages` (type: `array`):

Language codes, for example en, ar, fr, pt-br. Google Play shows reviews by language, so each Google Play app is read once per language. App Store reviews are not split by language; each App Store review gets a detected language code instead.

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

Per App Store app and country, and per Google Play app and language. You pay only for reviews delivered.

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

Newest first, or the store's 'most relevant' order. Alerts (only new reviews) always use newest first.

## `stars` (type: `array`):

Only reviews with these ratings. Leave empty for all.

## `dateFrom` (type: `string`):

YYYY-MM-DD, or relative like '30 days' or '6 months'. With newest-first order the actor stops reading at this date. Leave empty for no date limit.

## `includeAppDetails` (type: `boolean`):

One free row per app (per country for the App Store): name, developer, category, rating, ratings count, star histogram (Google Play), installs (Google Play), price, version, release notes, dates, size and description.

## `onlyNew` (type: `boolean`):

For scheduled alerts. Each run remembers the reviews it delivered for each app and storefront, under the state name below. The first run is the baseline and delivers up to the max.

## `stateName` (type: `string`):

Separate names keep separate memories, for example 'my-apps' and 'competitors'.

## `proxyConfiguration` (type: `object`):

Leave off to use the automatic fallback below. If you set a proxy here, it is used for every request and the fallback is off.

## `autoProxyFallback` (type: `boolean`):

Starts without a proxy. If a store keeps refusing, switches to Apify datacenter proxy, then residential.

## `allowResidentialFallback` (type: `boolean`):

Turn off to never use residential proxy.

## `maxConcurrency` (type: `integer`):

Requests to each store are still spaced out, so more is not always faster.

## `maxRetries` (type: `integer`):

Retries with growing waits, fresh sessions and the next proxy tier before a storefront is marked blocked.

## Actor input object example

```json
{
  "apps": [
    "https://apps.apple.com/sa/app/careem/id592978487",
    "com.careem.acma"
  ],
  "countries": [
    "sa"
  ],
  "languages": [
    "ar"
  ],
  "maxReviewsPerApp": 20,
  "sort": "newest",
  "stars": [],
  "dateFrom": "",
  "includeAppDetails": true,
  "onlyNew": false,
  "stateName": "default",
  "autoProxyFallback": true,
  "allowResidentialFallback": true,
  "maxConcurrency": 3,
  "maxRetries": 5
}
```

# Actor output Schema

## `results` (type: `string`):

All rows. Filter by the type field.

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

Status per storefront, reviews per store, requests, bytes per 1,000 reviews, retries and proxy tier used.

# 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/sa/app/careem/id592978487",
        "com.careem.acma"
    ],
    "countries": [
        "sa"
    ],
    "languages": [
        "ar"
    ],
    "maxReviewsPerApp": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dottti/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/sa/app/careem/id592978487",
        "com.careem.acma",
    ],
    "countries": ["sa"],
    "languages": ["ar"],
    "maxReviewsPerApp": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dottti/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/sa/app/careem/id592978487",
    "com.careem.acma"
  ],
  "countries": [
    "sa"
  ],
  "languages": [
    "ar"
  ],
  "maxReviewsPerApp": 20
}' |
apify call dottti/app-reviews --silent --output-dataset

```

## MCP server setup

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