# App Store Scraper — App Details & Reviews API (`plainfetch/app-store-scraper`) Actor

Search or look up any app on the iOS/macOS App Store and get its full details (price, rating, version, description, screenshots) plus its most recent customer reviews, straight from Apple's own public APIs. You only pay per app and per review delivered.

- **URL**: https://apify.com/plainfetch/app-store-scraper.md
- **Developed by:** [Plainfetch Studio](https://apify.com/plainfetch) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 apps

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

Search or look up any app on the [iOS/macOS App Store](https://www.apple.com/app-store/) and get back its
full details — price, rating, version history, description, screenshots — plus its most recent customer
reviews, without touching a browser. **App Store Scraper** is an **App Store API alternative** built
directly on Apple's own official, public APIs: the iTunes Search API, the Lookup API, and the
customer-reviews feed. Give it search terms or app ids and get one dataset item per app, plus one item
per review, ready to filter, sort, or feed into another tool.

### What can you do with it?

- **ASO and competitor research.** Pull a competitor's rating, price, category, description and screenshot
  set, or run a search term the way a shopper would and see exactly which apps rank for it.
- **Monitor app ratings and releases.** Track `rating`, `ratingCount` and `version`/`updatedAt` for a list
  of apps over time — run it on a schedule and diff the results to catch a new release or a rating drop.
- **Review mining for product feedback.** Fetch an app's most recent reviews to spot recurring complaints,
  feature requests, or praise — `rating`, `title` and `text` for every review, in the language reviewers
  actually wrote it in.
- **Feed AI agents and downstream tools.** Call this Actor from the Apify API, an integration, or an
  MCP-connected AI agent so it can look up an app's details or read its reviews as one step in a larger
  workflow.

### What data do you get?

Each run produces two kinds of dataset item, distinguished by `type`.

**One `app` item per app:**

| Field                                      | Type                            | Description                                                                                                      |
| ------------------------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `name` / `title`                           | string                          | The app's name (`title` is a duplicate of `name`, kept for consistency with `review` items).                     |
| `developer`                                | string | null                  | The developer/seller name, e.g. `"Duolingo"`.                                                                    |
| `price` / `currency`                       | number | null                  | `0` for a free app; `isFree` is `true`/`false` derived from it.                                                  |
| `rating` / `ratingCount`                   | number | null                  | The app's overall App Store rating and how many ratings it has.                                                  |
| `version` / `updatedAt`                    | string | null                  | The current version and when it was released.                                                                    |
| `description`                              | string | null                  | The full App Store description.                                                                                  |
| `genres` / `primaryGenre`                  | string\[] / string | null       | The App Store category (or categories) the app is listed under.                                                  |
| `screenshots`                              | string\[]                        | Screenshot image URLs, straight from Apple.                                                                      |
| `url`                                      | string | null                  | The app's App Store page.                                                                                        |
| `reviewsComplete`                          | boolean | null                 | Whether this app's reviews (if requested) were fetched in full — `null` if reviews weren't requested. See below. |
| `reviewsIncompleteReason` / `reviewsError` | string | null / object | null | Why `reviewsComplete` is `false` — see below.                                                                    |
| `error`                                    | object | null                  | Set when a requested search term or app id could not be delivered — see Limitations.                             |

**Every requested search term or app id leaves exactly one row — never silently dropped.** A term or
id skipped entirely, before it was ever attempted, still gets its own `app` row with every other field
blank, and is never charged. The two reasons it can be skipped read differently, on purpose:

- **Your spending cap was already reached** — your own limit doing exactly what you set it to do, not
  a failure of this Actor. That row carries **no `error` at all**: it says
  `reviewsIncompleteReason: "spending-cap-reached"` (with `reviewsComplete: false`, since nothing was
  read for it, its reviews included) and counts as delivered in the run summary.
- **This Actor's circuit breaker had already tripped** (three blocked or rate-limited answers in a
  row) — the source would not have answered any further, so that row carries a clear `error` and
  counts as failed.

**One `review` item per review:**

| Field     | Type           | Description                             |
| --------- | -------------- | --------------------------------------- |
| `rating`  | number | null | 1 to 5.                                 |
| `title`   | string         | The review's title.                     |
| `text`    | string | null | The review's body text.                 |
| `version` | string | null | The app version the reviewer was using. |
| `date`    | string | null | When the review was posted.             |
| `appId`   | string         | Which app this review is for.           |

**This Actor never emits who wrote a review.** No reviewer nickname, no reviewer profile URL, no author
field of any kind — only what they rated and wrote. See Limitations.

**An app's reviews can end up incomplete, and the app row says so honestly.** With `includeReviews: true`,
`reviewsComplete: true` means one thing: **every review there was to get was got** — the feed ran out, or
Apple's own 10-page ceiling was reached, which is all Apple publishes. `reviewsComplete: false` means
reviews were left unread, and `reviewsIncompleteReason` says why: `"max-reviews-reached"` (your own
`maxReviewsPerApp` stopped the read — not a failure, and everything fetched is still charged),
`"interrupted"` (a network or blocked error, with the detail in `reviewsError`), or
`"spending-cap-reached"`. **The app row itself is always delivered either way**; only its reviews may be
incomplete. Every review already fetched before the cutoff is still in the dataset and still charged.

### What real output looks like

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel. Here is real output
from a run with `{"searchTerms": ["duolingo"], "maxAppsPerSearch": 1, "includeReviews": true,
"maxReviewsPerApp": 2}`, trimmed for readability: two of the app's real reviews are shown below, with their
real, unedited title/text — one of the app's real reviews at the time of writing used stylised Unicode
lettering that renders poorly in a code block, so a plainer real review is shown instead of it; only the
choice of which reviews to display is trimmed, never a review's own text. The app row's `description`,
`releaseNotes`, `screenshots` and `languages` are also shortened here (the real item carries the app's full
description and release notes, its complete screenshot set, and all 32 supported languages). One more
field is not a literal capture either: the app row's `reviewsComplete: false` and
`reviewsIncompleteReason: "max-reviews-reached"` are derived from this Actor's own logic for
`maxReviewsPerApp: 2` against an app with far more reviews than that — the same values the code produces
for real, not copied verbatim from one specific run and not hand-edited. Reviews are fetched (and appear in
the dataset) before the app row they belong to — that is what lets the app row report `reviewsComplete`
honestly instead of guessing:

```json
[
  {
    "type": "review",
    "appId": "570060128",
    "reviewId": "14566250893",
    "rating": 2,
    "title": "Loot chest are terrible",
    "text": "Stop forcing me to tap repeatedly for a chest. I don’t care about what is in it. It is just annoying trash that gets in the way of learning",
    "version": "7.140.0",
    "date": "2026-09-18T17:24:18-07:00",
    "country": "us"
  },
  {
    "type": "review",
    "appId": "570060128",
    "reviewId": "14566216737",
    "rating": 5,
    "title": "Good app",
    "text": "I love Duolingo but I wish they did gettar",
    "version": "7.140.0",
    "date": "2026-09-18T17:11:09-07:00",
    "country": "us"
  },
  {
    "type": "app",
    "appId": "570060128",
    "bundleId": "com.duolingo.DuolingoMobile",
    "name": "Duolingo: Language Lessons",
    "title": "Duolingo: Language Lessons",
    "developer": "Duolingo",
    "developerId": "570060151",
    "url": "https://apps.apple.com/us/app/duolingo-language-lessons/id570060128?uo=4",
    "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c7/ee/1d/c7ee1d8c-9928-98bf-318a-0713e0351998/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg",
    "price": 0,
    "currency": "USD",
    "isFree": true,
    "rating": 4.7232,
    "ratingCount": 5456627,
    "ratingCurrentVersion": 4.7232,
    "version": "7.140.0",
    "releasedAt": "2012-11-13T08:00:00Z",
    "updatedAt": "2026-09-14T15:53:43Z",
    "releaseNotes": "Our mascot owl, Duo, is trading in his usual diet of mice for bugs this week...",
    "description": "Learn a new language, chess & more with the world's most downloaded education app!...",
    "genres": ["Education", "Social Networking"],
    "primaryGenre": "Education",
    "contentRating": "4+",
    "sizeBytes": 515881984,
    "minimumOsVersion": "17.0",
    "languages": ["AR", "BN", "CS", "NL", "EN", "FR", "DE"],
    "screenshots": [
      "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/6e/ef/30/6eef3023-de04-3b29-21f8-a993245b9abd/iPhone6.5_-_Default_-_first_screen_adapt_Var_2_01.jpg/320x480bb.jpg"
    ],
    "country": "us",
    "searchTerm": "duolingo",
    "reviewsComplete": false,
    "reviewsIncompleteReason": "max-reviews-reached",
    "reviewsError": null,
    "error": null
  }
]
```

`reviewsComplete: false` here because the read stopped at `maxReviewsPerApp: 2` while this app still had
millions of reviews available — nothing went wrong, and both reviews are delivered and charged;
`incompleteReason` just tells you it is not the whole feed.

### How much does it cost?

You pay **$0.002 per app delivered** and **$0.0005 per review delivered**. A search term or app id that
fails — not found, blocked, or a network error — is **never charged**. No start fee.

| Run                       | Cost                                      |
| ------------------------- | ----------------------------------------- |
| 5 apps, no reviews        | 5 × $0.002 = **$0.01**                    |
| 5 apps, 50 reviews each   | 5 × $0.002 + 250 × $0.0005 = **$0.135**   |
| 20 apps, 100 reviews each | 20 × $0.002 + 2,000 × $0.0005 = **$1.04** |

Real example: the run above (`{"searchTerms": ["duolingo"], "maxAppsPerSearch": 1, "includeReviews":
true, "maxReviewsPerApp": 2}`) delivered 1 app and 2 reviews, for **$0.003**.

### Input

See the **Input** tab for the full configuration. The minimal input is one search term or one app id:

```json
{
  "searchTerms": ["duolingo"]
}
```

- **`searchTerms`** / **`appIds`** — provide at least one of either. `appIds` accepts a numeric App Store
  id (`"570060128"` or `570060128`) or an App Store URL containing one
  (`"https://apps.apple.com/us/app/duolingo/id570060128"`). **Duplicate entries are processed once**:
  the same id listed twice is looked up once, and an app found by both a search term and an explicit
  id is delivered — and charged — once.
- **`country`** (optional, default `"us"`) — two-letter App Store storefront code, e.g. `"gb"`, `"de"`.
  Controls which country's catalogue, pricing and reviews are read.
- **`maxAppsPerSearch`** (optional, default `50`, 1 to 200) — how many apps to fetch per search term.
- **`includeReviews`** (optional, default `false`) — also fetch each app's most recent reviews.
- **`maxReviewsPerApp`** (optional, default `100`, 0 to 500) — how many of the most recent reviews to fetch
  per app. Ignored when `includeReviews` is off.
- **`requestDelayMs`** (optional, default `250`) — courtesy pause before each request to Apple's APIs.

### Limitations — read this first

**This Actor never returns reviewer names.** Apple's customer-reviews feed includes a reviewer nickname
and profile URL for every review; this Actor drops both before anything else touches the response. No
`review` item ever carries an author, nickname, or profile URI, no matter what Apple's feed includes.

**Apple exposes at most 500 of the most recent reviews per app per country** (10 pages of 50) — this is a
hard ceiling of Apple's own feed, not a limit this Actor imposes; `maxReviewsPerApp` cannot get you more.
**The Search API returns at most 200 apps per search term.** **Apple rate-limits the Search API to roughly
20 calls per minute per IP address** by its own documentation — the default `requestDelayMs` (250ms) and
the built-in concurrency cap (5) are deliberately gentle to respect that; raising them risks a rate-limited
or blocked response, which this Actor never attempts to get around (no proxy rotation, no fingerprint
spoofing, no CAPTCHA solving). Three consecutive blocked or rate-limited responses stop the run entirely,
with a clear message in the log — and a search term or app id skipped that way, or by your own spending
cap, still leaves its own uncharged `app` row instead of vanishing.

**iOS/macOS App Store only — not Google Play.** This Actor reads Apple's App Store APIs exclusively; it
does not cover Android or the Google Play Store.

### FAQ

**Is this legal?** We can't give you legal advice, but here is exactly what the Actor does: it calls
Apple's own public, documented APIs — the iTunes Search API, the Lookup API, and the customer-reviews
feed — the same data these APIs return to any client that asks. It does not log in, does not get around
any block, and never emits a reviewer's name or profile. You are responsible for the search terms and app
ids you submit and for how you use the results.

**Where does the data come from?** Directly from `itunes.apple.com`'s public Search, Lookup and
customer-reviews endpoints — no third-party database, no cache.

**How do I use this from the API or an AI agent (MCP)?** Call it like any other Apify Actor: via the
[Apify API](https://docs.apify.com/api/v2), the JavaScript or Python client, an integration, or an
MCP-connected AI agent that has access to your Apify Actors. Pass the same `searchTerms`/`appIds` input
and read the results back from the default dataset.

**What happens if I hit my spending cap?** The run stops cleanly: it finishes the app or review already
being processed, writes everything it has, and reports `chargeLimitReached: true` in the run summary.
Nothing already produced is lost, and nothing beyond the cap is charged.

### Changelog

**23 September 2026** — A search term or app id **skipped because your spending cap was already
reached** no longer carries an `error`. That row used to be delivered with
`error: {category: "other", message: "Skipped: the spending limit for this run was reached."}`; it now
carries `error: null`, says why in `reviewsIncompleteReason: "spending-cap-reached"` (with
`reviewsComplete: false`), and still counts as delivered and is still never charged. Your own limit is
not a failure of this Actor, and `error` now means one thing only: this row could not be delivered. A
row skipped because the Actor's circuit breaker tripped is unchanged — it still carries an `error`. If
you filter rows on `error !== null` to spot cap-skipped units, filter on
`reviewsIncompleteReason === "spending-cap-reached"` instead.

**0.1** — Initial release.

# Actor input Schema

## `searchTerms` (type: `array`):

Free-text App Store searches, e.g. "duolingo" or "language learning". Provide at least one search term or app id.

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

Specific apps to fetch directly: a numeric App Store id (e.g. "570060128") or an App Store URL containing one (e.g. "https://apps.apple.com/us/app/duolingo/id570060128"). Provide at least one search term or app id.

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

Two-letter App Store storefront code, e.g. "us", "gb", "de". Controls which country's catalogue, pricing and reviews are read.

## `maxAppsPerSearch` (type: `integer`):

How many apps to fetch for each search term. Apple's Search API serves at most 200 per term.

## `includeReviews` (type: `boolean`):

Also fetch each app's most recent customer reviews. Reviews never include the reviewer's name.

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

How many of the most recent reviews to fetch per app, newest first. Apple exposes at most 500 (10 pages of 50) per app per country. Ignored when Include reviews is off.

## `requestDelayMs` (type: `integer`):

Courtesy pause before each request to Apple's APIs.

## Actor input object example

```json
{
  "searchTerms": [
    "duolingo"
  ],
  "country": "us",
  "maxAppsPerSearch": 5,
  "includeReviews": false,
  "maxReviewsPerApp": 50,
  "requestDelayMs": 250
}
```

# Actor output Schema

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

No description

## `reviews` (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 = {
    "searchTerms": [
        "duolingo"
    ],
    "maxAppsPerSearch": 5,
    "maxReviewsPerApp": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("plainfetch/app-store-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 = {
    "searchTerms": ["duolingo"],
    "maxAppsPerSearch": 5,
    "maxReviewsPerApp": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("plainfetch/app-store-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 '{
  "searchTerms": [
    "duolingo"
  ],
  "maxAppsPerSearch": 5,
  "maxReviewsPerApp": 50
}' |
apify call plainfetch/app-store-scraper --silent --output-dataset

```

## MCP server setup

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