# App Store and Google Play Reviews Scraper (`omraydev/app-store-reviews`) Actor

Scrape app reviews from Google Play and the Apple App Store into one fixed table. Pick the country and language, sort by newest, relevant or rating, filter by stars or date, and get thousands of reviews per app.

- **URL**: https://apify.com/omraydev/app-store-reviews.md
- **Developed by:** [Om Ray](https://apify.com/omraydev) (community)
- **Categories:** Agents, Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.10 / 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 and Google Play Reviews Scraper

This actor collects app reviews from Google Play and the Apple App Store and puts them in one table. Every row has the same fields in the same order, whichever store it came from and whatever you set in the input. You can mix both stores in one run.

It runs without a login or a proxy. The actor only reads reviews that anyone can see on the store pages.

### Input

Put one app per line in `apps`. The actor works out the store from what you paste:

- a Google Play package ID, like `com.spotify.music`
- a Google Play URL, like `https://play.google.com/store/apps/details?id=com.spotify.music`
- an App Store numeric ID, like `324684580` or `id324684580`
- an App Store URL, like `https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580`

The other settings apply to every app in the run.

| Field | What it does |
| --- | --- |
| `country` | Two-letter country code. Default `us`. |
| `language` | Language code such as `en`, `fr` or `pt-BR`. Default `en`. |
| `sort` | `newest`, `relevant` or `rating` (highest first). Default `newest`. |
| `maxReviews` | Most reviews to save for each app, from 1 to 100,000. Default 100. |
| `stars` | Keep only these star ratings, for example `["1", "2"]`. Empty keeps all. |
| `since` | Keep only reviews from this date onward, for example `2026-10-01`. |
| `includeReviewerName` | Add the reviewer's public display name. Off by default. |
| `maxRunMinutes` | Stop collecting after this many minutes. Default 30. |

This is the input from `examples/input.json`:

```json
{
  "apps": ["com.spotify.music", "324684580"],
  "country": "us",
  "language": "en",
  "sort": "newest",
  "maxReviews": 50
}
```

### Output

Each review is one dataset row. These two rows come from a real run of the input above.

```json
[
  {
    "type": "review",
    "store": "google-play",
    "appId": "com.spotify.music",
    "appTitle": "Spotify: Music and Podcasts",
    "country": "us",
    "language": "en",
    "reviewId": "49084f4d-4f1d-4301-8559-80f8d1ccdf30",
    "rating": 1,
    "title": null,
    "text": "fake offer. $1 first 30 days is not $13.90 for 2 months. Thats legit scamming! Waiting in full refund!",
    "date": "2026-10-31T14:52:54.000Z",
    "appVersion": "9.1.44.2120",
    "helpfulCount": 0,
    "developerReply": "Hi. Thanks for bringing this up. We're open to feedback, and we'd love to get some more info about this. If you have the time, give our Customer Service a shout. You can find them by heading to support.spotify.com and searching \"Contact us\".",
    "developerReplyDate": "2026-10-31T15:18:05.000Z",
    "reviewUrl": "https://play.google.com/store/apps/details?id=com.spotify.music&reviewId=49084f4d-4f1d-4301-8559-80f8d1ccdf30&hl=en&gl=us",
    "reviewerName": null,
    "input": "com.spotify.music",
    "statusMessage": null
  },
  {
    "type": "review",
    "store": "app-store",
    "appId": "324684580",
    "appTitle": "Spotify: Music and Podcasts",
    "country": "us",
    "language": "en",
    "reviewId": "14732288924",
    "rating": 4,
    "title": "Great app but the non music and podcast related adds",
    "text": "On Spotify it is stated in the name “music and podcasts” but I’m always getting adds that are not even related to music or podcasts half the time it just feels asinine to me that I’m getting adds about delivery services or clothes when the app is literally focused around music and podcasts. I don’t mind the music and podcasts adds but it’s the nonsense of these random business appearing on a music app",
    "date": "2026-10-31T04:40:00.000Z",
    "appVersion": "9.1.94",
    "helpfulCount": 0,
    "developerReply": "Hi. We'd love to get some more info about this. If you have the time, give our Customer Service a shout. You can find them by heading to support.spotify.com and searching \"Contact us\".",
    "developerReplyDate": "2025-09-05T12:30:46.000Z",
    "reviewUrl": null,
    "reviewerName": null,
    "input": "324684580",
    "statusMessage": null
  }
]
```

#### Fields

| Field | Meaning |
| --- | --- |
| `type` | `review` for a review. `status` for a free row that explains why an app gave nothing. |
| `store` | `google-play` or `app-store`. |
| `appId` | Package ID on Google Play, numeric ID on the App Store. |
| `appTitle` | App name in the country and language you picked. |
| `country` | The country code used for the request. |
| `language` | The language code used for the request. |
| `reviewId` | The store's ID for the review. |
| `rating` | Stars, 1 to 5. |
| `title` | Review title. Always `null` on Google Play, which has no titles. |
| `text` | Review text. |
| `date` | When the review was posted or last edited, in UTC. |
| `appVersion` | App version the reviewer had. On the App Store, only the newest 500 or so reviews have it. |
| `helpfulCount` | Thumbs-up count on Google Play. Helpful votes on the App Store, for the newest 500 or so reviews only. |
| `developerReply` | The developer's reply, if there is one. |
| `developerReplyDate` | When the reply was posted or last edited. Apple keeps an old reply on a review that was later edited, so this date can be older than `date`. |
| `reviewUrl` | Link to the review on Google Play. `null` on the App Store, which has no link per review. |
| `reviewerName` | Public display name, only when `includeReviewerName` is on. |
| `input` | The line from `apps` that produced this row. |
| `statusMessage` | On status rows, the reason nothing was collected. `null` on reviews. |

The actor never collects profile pictures or user IDs.

#### Run summary

The `OUTPUT` record in the key-value store lists every app with how many reviews were saved and why collection stopped:

- `reached-max`: it saved `maxReviews`.
- `no-more-reviews`: the store has no more reviews for this app, or none that match your filters.
- `reached-since-date`: with `sort` set to `newest`, it reached reviews older than `since`.
- `store-limit`: the store would not give more pages.
- `time-budget`: `maxRunMinutes` ran out.
- `charge-limit`: your spending limit for the run was reached.
- `invalid-app-id` or `app-not-found`: the app line was not an app, or the store has no such app in that country.
- `error`: the store kept failing after retries. The message has the details.

When an app gives no reviews, you also get one `status` row in the dataset with the reason, so you never end up with an empty table and no explanation.

### Country and language

On the App Store, reviews belong to a country's store. Setting `country` to `ng` gets reviews from the Nigerian App Store only, and the app title comes from that store too. To cover more countries, run the actor once per country.

Google Play works differently and groups reviews by language. The actor sends your country to Google Play and records it on every row, but Google Play returns the same reviews for `en` whether the country is `us`, `ng` or `gb`. To get reviews in another language, change `language`.

### How far it goes

On Google Play, the actor pages with the store's continuation token until it reaches `maxReviews` or the last review. In a local test it saved 3,000 Snapchat reviews in about 35 seconds.

On the App Store, it reads the same reviews API the App Store website uses, 20 reviews a page, and follows each page to the next. In a local test it saved 3,000 Snapchat reviews from the Nigerian store in about a minute. Apple slows down fast requests, so the actor waits and retries when that happens. Very large App Store runs take a while. The App Store does check `language`, and a code it doesn't know ends that app with an `error` status row.

If Apple's reviews API is not reachable, the actor falls back to Apple's public reviews RSS feed. That feed stops at 10 pages, which is about 500 reviews per country, and the summary says `store-limit` when that happens. Running more countries gets you more reviews in that case.

Filters work like this. A single star rating on Google Play is filtered by Google, so it is fast. Other star filters, and all star filters on the App Store, are applied as reviews come in, so the actor may read many reviews to find enough matches. The `since` filter stops early only with `sort` set to `newest`. With other sorts it has to read the whole list to find recent reviews, so on a big app it usually runs until `maxRunMinutes`. Use `newest` with `since`.

The `rating` sort on the App Store means most favorable first. If the RSS fallback is in use, `rating` falls back to newest first.

### Price

You pay $0.0001 for each review saved, which is 10 cents per 1,000 reviews. Status rows are free. A run stops cleanly when it reaches the spending limit you set on Apify.

### Run it locally

```sh
npm install
npm run check
npx apify run --input-file examples/input.json
```

The tests in `test/` use fake store responses to check store detection, paging, filters, the fallback, status rows and the spending limit.

If something breaks or looks wrong, open an issue on the Issues tab and it gets fixed fast, and if this actor helped you, an honest review on the Store helps other people find it.

# Actor input Schema

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

One app per line. Use a Google Play package ID (com.spotify.music), a Google Play URL, an App Store numeric ID (324684580) or an App Store URL. The store is detected for you. Up to 100 apps.

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

Two-letter country code such as us, gb, ng or in. Reviews come from this country's store, and every row records it.

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

Language code such as en, fr or pt-BR. Google Play returns reviews written in this language. The App Store returns all reviews from the country's store and uses this only for the request locale. Every row records it.

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

Newest first, most relevant (most helpful on the App Store) or highest rating first.

## `maxReviews` (type: `integer`):

The actor pages until it has this many reviews for each app or the store runs out.

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

Only keep reviews with these star ratings. Leave empty for all ratings.

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

Only keep reviews posted on or after this date. With the Newest sort the actor stops as soon as it reaches older reviews.

## `includeReviewerName` (type: `boolean`):

Off by default. When on, each row includes the public display name shown next to the review. Profile images and user IDs are never collected.

## `maxRunMinutes` (type: `integer`):

The run stops collecting after this many minutes and saves what it has.

## Actor input object example

```json
{
  "apps": [
    "com.spotify.music",
    "324684580"
  ],
  "country": "us",
  "language": "en",
  "sort": "newest",
  "maxReviews": 50,
  "stars": [],
  "includeReviewerName": false,
  "maxRunMinutes": 30
}
```

# Actor output Schema

## `dataset` (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": [
        "com.spotify.music",
        "324684580"
    ],
    "country": "us",
    "language": "en",
    "maxReviews": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("omraydev/app-store-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": [
        "com.spotify.music",
        "324684580",
    ],
    "country": "us",
    "language": "en",
    "maxReviews": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("omraydev/app-store-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": [
    "com.spotify.music",
    "324684580"
  ],
  "country": "us",
  "language": "en",
  "maxReviews": 50
}' |
apify call omraydev/app-store-reviews --silent --output-dataset

```

## MCP server setup

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