# App Store Reviews Scraper & App Search API (`omargnagy/app-store-reviews-scraper`) Actor

Apple App Store reviews and app search with a live Standby HTTP API, so an AI agent can pull reviews mid-task with no cold start. Clean, LLM-ready JSON: rating, title, text, app version, helpful votes, date. Any storefront, no tokens to rotate. No start fee, pay only for results.

- **URL**: https://apify.com/omargnagy/app-store-reviews-scraper.md
- **Developed by:** [Omar Nagy](https://apify.com/omargnagy) (community)
- **Categories:** E-commerce, Developer tools, AI
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.10 / 1,000 reviews

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Reviews Scraper & App Search API

Pull **Apple App Store reviews** and **search the App Store** at speed, with clean, LLM-ready JSON. Built for AI agents, review-monitoring pipelines, and product teams who need real user feedback in a structured shape.

Two things make this one different: it runs in **Standby mode**, so an agent can call a live HTTP endpoint mid-task with no cold start, and it reads Apple's own public feeds, so there is no token to rotate, nothing to unblock, and no proxy bill.

### What you get

- **Reviews by app**: star rating, review title, review text, app version, helpful votes, timestamp, and a permalink.
- **App search**: find an app's numeric App Store ID, plus developer, average rating, rating count, price, genre, and icon.
- **Any storefront**: `country` selects the store, and reviews differ per store, so `us`, `gb`, `de` and `eg` each return their own.
- **Standby HTTP API**: a persistent endpoint your agent or app can hit directly.
- **MCP ready**: exposed as a typed tool through Apify's MCP server via the input schema.
- **No dead columns**: every field in the output is a field Apple actually serves. Measured on 5 September 2026: 100 of 100 reviews carried a title.

### Use cases

- Watch what users say after each iOS release, and catch a crash wave before the support queue fills up.
- Compare the same app across storefronts, which is the fastest read on a localisation problem.
- Feed competitor reviews into an LLM for feature-gap and pain-point analysis.
- Give an AI agent a live tool that answers "what are people complaining about in this app right now".
- Collect training or RAG data from real, public product feedback.

### Modes

| Mode | You give it | You get back |
|------|-------------|--------------|
| `search` | `query` (app name or keyword) | matching apps with their numeric App Store IDs |
| `reviews` | `appIds` (numeric IDs) | reviews, paginated up to `maxItems` |

Start with `search` to get an ID, then run `reviews` on it. `reviews` and `search` are the only two accepted modes, lowercase and exact.

### Input

Find an app:

```json
{
  "mode": "search",
  "query": "notion",
  "country": "us",
  "maxItems": 5
}
```

Pull its reviews:

```json
{
  "mode": "reviews",
  "appIds": ["310633997"],
  "country": "us",
  "maxItems": 100,
  "sortBy": "mostRecent"
}
```

Compare three apps in one run. `maxItems` is the budget for the whole run and is
split evenly, so this returns 30 reviews of each, not 90 of the first one:

```json
{
  "mode": "reviews",
  "appIds": ["324684580", "544007664", "310633997"],
  "country": "us",
  "maxItems": 90
}
```

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `mode` | `reviews` or `search` | `reviews` | required. Any other value is rejected before anything is fetched. |
| `appIds` | array of strings | | numeric App Store IDs, for example `310633997`. **Required when `mode` is `reviews`.** Ignored in `search`. |
| `query` | string | | app name or keyword. **Required when `mode` is `search`.** Ignored in `reviews`. |
| `country` | 2-letter code | `us` | App Store storefront |
| `maxItems` | integer 1 to 5000 | `100` | budget for the whole run, **split evenly across `appIds`**. 100 across 3 apps is 34 / 33 / 33. |
| `maxItemsPerApp` | integer 1 to 5000 | | optional. When set, every app gets exactly this many and the even split is ignored, so 3 apps at 50 return up to 150 rows. |
| `sortBy` | `mostRecent`, `mostHelpful` | `mostRecent` | review order, `reviews` mode only |

A run that asks for `reviews` without `appIds`, or `search` without `query`, fails immediately with a clear input error and returns nothing, so a misconfigured run costs you nothing.

### Output

Real output from runs on 5 September 2026. Larger samples of both are in the `examples/` folder of the source.

App search:

```json
{
  "type": "app",
  "appId": 1232780281,
  "bundleId": "notion.id",
  "name": "Notion: Notes, Tasks, AI",
  "developer": "Notion Labs, Incorporated",
  "rating": 4.77668,
  "ratingCount": 89975,
  "price": "Free",
  "genre": "Productivity",
  "url": "https://apps.apple.com/us/app/notion-notes-tasks-ai/id1232780281?uo=4",
  "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/2e/aa/40/2eaa406d-99e0-e722-530a-ce20259f3834/AppIconProd-0-0-1x_U007epad-0-0-0-1-0-0-P3-85-220.png/100x100bb.jpg"
}
```

A review:

```json
{
  "type": "review",
  "appId": "310633997",
  "country": "us",
  "reviewId": "14505487796",
  "author": "Barirah Ibrahim",
  "rating": 1,
  "title": "Terrible",
  "text": "Keeps of restricting my account for no reason, usually I get my account back after a few hours but this time it's been 5 days. I still can't login!!!",
  "version": "26.34.72",
  "votesHelpful": 0,
  "votesTotal": 0,
  "updatedAt": "2026-09-03T05:29:13-07:00",
  "url": "https://itunes.apple.com/us/reviews/id1964520100"
}
```

### Example run

Input: the two blocks above. Output on 5 September 2026: **100 reviews for `310633997` in about 1 second**, and 5 apps for the `notion` search. Every one of the 100 reviews had a non-empty `title` and `text`.

### Use it from an agent (Standby)

Standby keeps the Actor warm and answers over plain HTTP, so there is no per-call container start.

```
GET {standbyUrl}/search?query=notion&country=us&maxItems=5
GET {standbyUrl}/reviews?appId=310633997&country=us&maxItems=50&sortBy=mostRecent
```

Every successful response is:

```json
{
  "ok": true,
  "count": 90,
  "perApp": [
    { "appId": "324684580", "requested": 30, "fetched": 30, "charged": 30, "error": null },
    { "appId": "544007664", "requested": 30, "fetched": 30, "charged": 30, "error": null },
    { "appId": "310633997", "requested": 30, "fetched": 30, "charged": 30, "error": null }
  ],
  "items": [ ... ],
  "tookMs": 2410
}
```

The numbers above show the shape of a three-app call at `maxItems: 90`, not a
recording of one particular request.

`perApp` is the run's own accounting: what each app was allotted, what it returned,
and what it was billed. An app id that cannot be read gets an `error` string and
`charged: 0` while the rest of the call keeps its full budget, so one bad id does
not fail the request.

`GET {standbyUrl}/` returns a small service document with the modes and the running build number.

`appId` (singular) is accepted as a convenience alongside a comma-separated `appIds`.

#### Errors are typed, so an agent knows whether to retry

| Status | When | Body |
|--------|------|------|
| `404` | the path is not `/reviews` or `/search`, for example `/foobar`, `/REVIEWS`, `/v1/reviews` | `{ "ok": false, "error": "...", "statusCode": 404, "modes": ["reviews","search"] }` |
| `400` | the path is valid but the parameters are not, for example `/reviews` with no `appId` | same shape, `statusCode: 400` |
| `500` | an upstream failure | same shape |

`404` and `400` mean "fix the call", not "try again". Neither fetches any data, so neither returns anything billable.

### Good to know

- **App IDs are numeric**, for example `310633997` for WhatsApp Messenger. Get them from `mode: "search"`, or read them off the end of an App Store URL.
- **Reviews are per storefront.** The same app returns a different set in `us` and in `de`. There is no global feed.
- **Apple's paging has holes, and this Actor reads through them.** The feed serves up to 10 pages of about 50 reviews, but an individual page can come back empty while later pages are full, and which pages are empty moves over minutes. One 10-page sweep of `389801252` on 6 September 2026 read `[0, 50, 50, 50, 50, 50, 50, 0, 0, 50]`, and re-reading an empty page URL five times a second apart returned 0 every time, so an in-run retry buys nothing. This Actor therefore reads every one of the 10 pages until your budget is full: a run of empty pages, however long, never ends the pull early. It can still return fewer rows than the app really has, if Apple serves those pages empty for the whole run. Results are de-duplicated by review ID.
- **The practical ceiling is roughly 500 reviews per app and storefront**, which is what Apple exposes. Ask for more across several storefronts if you need more.
- **`maxItems` is split evenly across `appIds`.** Three apps at `maxItems: 90` return 30 each, and an app that runs out early leaves its remainder unspent rather than handing it to the next app, so no app can spend another app's budget. Set `maxItemsPerApp` when you want a fixed number each regardless of how many apps you list. (Until 6 September 2026 the first app in the list took the whole budget and the rest returned nothing. That is fixed.)
- **Per-app accounting is in the run.** Every batch run writes a summary to the key-value store as `OUTPUT`, including a `mode: search` run where the per-app list is empty, and logs one `[app] <id> requested=N fetched=N charged=N` line per app. Standby answers over HTTP and does not write to the key-value store: it returns the same breakdown as `perApp` in the response body instead.
- Reviews are public data published by users on the App Store. Respect Apple's terms and any applicable privacy law in how you store and use it.

### Parity with the Google Play Actor

There is a sibling Actor, **Google Play Reviews Scraper & App Search API**, with the same
two modes, the same Standby routes, the same error shapes and the same pay-per-event
model. Input is the same except for two fields, and the review record maps field for
field with five differences. Every difference below is a difference in what the two
stores publish, not a difference in style.

Input:

| Field | App Store | Google Play |
|---|---|---|
| `mode`, `appIds`, `appId`, `query`, `country`, `maxItems`, `maxItemsPerApp` | same | same |
| review order | `sortBy`: `mostRecent`, `mostHelpful` | `sort`: `newest`, `rating`, `helpfulness` |
| review language | not accepted, Apple serves per storefront only | `lang` |

Review output:

| What it means | App Store | Google Play |
|---|---|---|
| record kind | `type` | `type` |
| app identifier | `appId` (numeric, `310633997`) | `appId` (package name, `com.whatsapp`) |
| storefront | `country` | `country` |
| review id | `reviewId` | `reviewId` |
| author display name | `author` | `author` |
| stars, 1 to 5 | `rating` | `rating` |
| review body | `text` | `text` |
| app version | `version` | `version` |
| permalink | `url` | `url` |
| when it was posted | `updatedAt` | `date` |
| review language | not served | `lang` |
| helpfulness | `votesHelpful` and `votesTotal` | `thumbsUp` |
| headline | `title` | not served |
| developer reply | not served | `replyText`, `replyDate` |

The five differences, each measured rather than assumed:

1. **Timestamp name.** Apple's customer-reviews feed names its only time field `updated`, so this Actor exposes `updatedAt`. Google Play carries a review date, exposed there as `date`.
2. **Language.** A live Apple feed entry carries exactly these keys: `author`, `content`, `id`, `im:contentType`, `im:rating`, `im:version`, `im:voteCount`, `im:voteSum`, `link`, `title`, `updated` (read 6 September 2026, storefront `us`, app `324684580`, 50 entries). There is no language key, so there is nothing to expose.
3. **Helpfulness.** Apple gives two numbers, a sum and a count, exposed as `votesHelpful` and `votesTotal`. Google Play gives one, `thumbsUp`.
4. **Headline.** Apple serves a review `title`. Google Play no longer does: zero of 1,200 reviews across 8 apps carried one on 5 September 2026, so that field is absent there.
5. **Developer reply.** Apple's feed has no reply key at all (see the key list in point 2). Google Play serves one and it is common there (111 of 150 reviews on Adobe Scan, measured 5 September 2026).

One behavioural difference worth knowing when you compare depth: Apple serves up to 10
fixed pages and an individual page can come back empty while later pages are full, which
this Actor skips rather than treating as the end. Google Play pages with a continuation
token until it runs out.

### Pricing

You pay for results, not for time. There is **no start fee**, and nothing is charged for a run that returns nothing.

- **review**: one App Store review row returned, in batch mode or through Standby.
- **app**: one app record returned by a search.

A 1,000-review pull is charged as 1,000 review events. Finding an app costs a handful of app events. Current rates are on the Pricing tab of this page.

### FAQ

**How do I find an app's ID?**
Run `mode: "search"` with the app name. The `appId` in the output is the numeric ID.

**Can I get every review an app ever received?**
No. Apple exposes a recent window per storefront, around 500 reviews. Pull the same app from several storefronts for a wider picture, or run it on a schedule and accumulate.

**Does it need a proxy?**
No. It reads Apple's public feeds directly.

**Can I run it on a schedule?**
Yes. Use Apify's scheduler, or call the Standby endpoint from your own job.

**Do you support Google Play too?**
Yes, in a separate Actor in the same series.

***

Built by Omar Nagy. Part of an agent-native data-tool series on Apify.

# Actor input Schema

## `mode` (type: `string`):

reviews = fetch reviews for App Store IDs (requires App IDs below). search = find apps by keyword (requires Search query below). These are the only two accepted values, lowercase and exact.

## `appIds` (type: `array`):

Apple App Store numeric app IDs, e.g. 310633997 for WhatsApp Messenger. REQUIRED when mode = reviews, and a run with mode = reviews and no app ID fails immediately with a 400-style input error. Ignored when mode = search. Get IDs from mode = search.

## `sortBy` (type: `string`):

Order for review results. Used only when mode = reviews.

## `query` (type: `string`):

App name or keyword. REQUIRED when mode = search, and a run with mode = search and no query fails immediately with a 400-style input error. Ignored when mode = reviews.

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

Two-letter App Store country code (storefront). Reviews are per storefront. Applies to both modes.

## `maxItems` (type: `integer`):

Total number of results for the whole run. With several app IDs it is SPLIT EVENLY between them (100 across 3 apps is 34 / 33 / 33), so no single app can spend another app's budget. Apple serves up to about 500 reviews per app and storefront.

## `maxItemsPerApp` (type: `integer`):

Optional. When set, EVERY app ID gets exactly this many reviews and the even split of Max items is ignored, so 3 apps at 50 return up to 150 rows. Leave it empty to let Max items be the total budget. Used only when mode = reviews.

## Actor input object example

```json
{
  "mode": "reviews",
  "appIds": [
    "310633997"
  ],
  "sortBy": "mostRecent",
  "query": "whatsapp",
  "country": "us",
  "maxItems": 100
}
```

# Actor output Schema

## `results` (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 = {
    "mode": "reviews",
    "appIds": [
        "310633997",
        "324684580"
    ],
    "sortBy": "mostRecent",
    "query": "notion",
    "country": "us",
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("omargnagy/app-store-reviews-scraper").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 = {
    "mode": "reviews",
    "appIds": [
        "310633997",
        "324684580",
    ],
    "sortBy": "mostRecent",
    "query": "notion",
    "country": "us",
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("omargnagy/app-store-reviews-scraper").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 '{
  "mode": "reviews",
  "appIds": [
    "310633997",
    "324684580"
  ],
  "sortBy": "mostRecent",
  "query": "notion",
  "country": "us",
  "maxItems": 100
}' |
apify call omargnagy/app-store-reviews-scraper --silent --output-dataset

```

## MCP server setup

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

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/syQ566huikgkK4dzC/builds/NpEynYCbKUsuuisQd/openapi.json
