# App Store Apps & Charts Scraper (`scrapyx/appstore-apps-scraper`) Actor

Live Top Free and Top Paid charts for any Apple storefront, plus full app metadata by ID or bundle ID. Batches under iTunes lookup's silent 210-result cap, and refuses unknown storefronts because apps.apple.com answers those with HTTP 200 serving the US page.

- **URL**: https://apify.com/scrapyx/appstore-apps-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Business, Developer tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
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 Apps & Charts Scraper

Live **Top Free** and **Top Paid** rankings for any Apple storefront, plus
full app metadata by App Store ID, bundle ID or store URL — straight from
Apple's own public endpoints. No login, no API key, no browser.

### What it collects

| Mode | What you get |
| --- | --- |
| `charts` | The live Top Free / Top Paid rankings for any of ~160 storefronts, with chart position preserved, optionally hydrated with each app's full metadata. |
| `apps` | Full metadata for specific apps — looked up in every storefront you list, so you get an N apps × M countries comparison table in one run. |

### Read this before you compare anything: per-country data really differs

Price, currency, rating, rating count **and the current version** are all
per-storefront facts. Measured on Facebook in a single minute:

| Storefront | Currency | Rating | Rating count | Version |
| --- | --- | ---: | ---: | --- |
| us | USD | 4.52522 | 28,006,734 | 575.0.0 |
| gb | GBP | 4.52205 | 3,173,060 | 576.0.0 |
| jp | JPY | 4.39079 | 1,031,790 | 576.0.0 |
| de | EUR | 4.37808 | 1,386,832 | 576.0.0 |

The US was a **whole release behind** the other three at that moment. So
there is no such thing as "the app's rating" — every row here carries
`countryQueried`, always.

### The traps it handles

- **`lookup` silently caps at 210 results.** 10/50/100/150/200 identifiers all
  come back complete; **300 in returns 210, and 400 in returns 210** — HTTP
  200, no error, no truncation flag. A client that posts 300 IDs loses 90 and
  cannot tell. Batched at 200, with the batch count reported.
- **One vendor, three conventions for a bad country — and one of them lies.**
  `lookup?country=zz` answers **400**. The chart host answers **500 text/html**.
  But `apps.apple.com/zz/app/...` answers **HTTP 200 serving the US page**, so
  an unsupported storefront reads as a real country-specific measurement. Bad
  countries are refused in code *and* every country's rating is compared
  against the US baseline, so a silent fallback shows up as
  `ratingIdenticalToUsBaseline` in the data.
- **`top-grossing` is gone.** Apple no longer serves it, `new-apps-we-love` or
  `new-games-we-love` from this API — all three 404 **exactly like a typo
  would**. Only `top-free` and `top-paid` remain, and asking for a removed one
  is refused with an explanation instead of an error row on every run.
- **Chart limit is 1–100, and 101 is an HTTP 500 HTML page** — not a 400, not a
  clamp. A bad *feed* on the same URL shape is a 404 with a JSON body. Clamped
  in code.
- **Ratings are floats and Apple mixes types on purpose.**
  `averageUserRating` is `4.85717`, `userRatingCount` is an int, and
  `fileSizeBytes` is a **string** of digits. An `int()`-first coercion reports
  ChatGPT's 4.857 as **4** — that bug was caught on this actor's first smoke
  test and there is now a trap case pinning it.
- **The two rating sources disagree.** The store page's JSON-LD said
  28,008,005 while `lookup` said 28,006,734 for the same app, storefront and
  minute — 1,271 apart, and the JSON-LD value is rounded to one decimal
  against lookup's five. Both are published, never merged, with the gap as a
  number in `ratingCountDisagreement`.
- **A missing app is an empty success.** `lookup` answers an unknown
  identifier with HTTP 200 and `resultCount: 0`, not a 404 — turned into an
  `ERROR` row that says so. An app can also genuinely exist in one storefront
  and not another.
- **A chart row carries 8 fields; the app has 30+.** Hydration costs one
  batched request per 200 apps and is on by default; rows say which shape they
  came from in `metadataSource`.

### Why there are no reviews here

Individual review **text** is not reachable over HTTP without something this
actor is not allowed to have, and both routes were taken to the end:

**1. The legacy reviews RSS returns an empty feed on an HTTP 200 and will not
say why.** The same URL, ten times in a row:

| App | Format | Entries returned |
| --- | --- | --- |
| Facebook | xml | `[50, 0, 0, 0, 0, 0, 0, 0, 0, 0]` |
| Facebook | json | `[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]` |
| ChatGPT | xml | `[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]` |
| ChatGPT | json | `[50, 50, 50, 50, 50, 50, 50, 50, 50, 50]` |

The first request after a pause returns real data and the rest come back
empty — per-IP throttling answered with an empty 200 rather than a 429. Worse,
a **bogus app ID returns the identical shape**, so "app doesn't exist", "app
has no reviews" and "you're being throttled" are one response. `sortby` is
inert too: `mostrecent`, `mosthelpful`, `mostfavorable`, `mostcritical` and
`bogus` all return the same thing. Shipping that would mean shipping a scraper
that returns nothing and calls it success.

**2. `amp-api.apps.apple.com` needs a bearer token Apple no longer embeds.**
The 980 KB product page contains no `MEDIA_API` key, no config meta tag, and
the string `token` does not occur anywhere in it.

What *is* exact and reachable is the rating **summary** — value and count, per
storefront, from two independent sources — so that is what this actor
publishes. It does not pretend to have review text.

### Output

One dataset, `recordType` tells the rows apart:

- `SEARCH_SUMMARY` — one per run: storefronts requested vs returned, lookup
  batches and the silent cap, rating disagreements, suspected US fallbacks,
  identifiers not found.
- `APP` — one row per app **per storefront**, with upstream's object passed
  through verbatim alongside the normalised fields.
- `ERROR` — one per input that failed, so every input maps to at least one row.

### Anti-bot and limits

**No WAF.** Seven TLS profiles (chrome124, chrome131, chrome136, safari17\_0,
edge101, firefox133, chrome99\_android) returned 200 on `lookup` with
byte-identical 10,455-byte bodies, cold, no warmup, no proxy. A proxy is
optional and **off by default**.

Apple's chart host is genuinely flaky under load — six identical requests
returned one connection error — so the retry ladder matters more than raw
concurrency here.

### Policy

`itunes.apple.com` disallows `/search*` and `/*/rss/*`; this actor uses
`/lookup`, which is allowed, and does **not** do keyword search.
`rss.marketingtools.apple.com` places no restriction on the chart paths.
`apps.apple.com` disallows `/api/*` and `/v1/*`; the product page
`/{cc}/app/{slug}/id{n}` is allowed and is the only path used there. No AI
crawler group appears on any host. Verified with an RFC 9309 wildcard-correct
matcher — `urllib.robotparser` matches by prefix only and would read
`/*/rss/*` as harmless.

# Actor input Schema

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

charts = the live Top Free / Top Paid rankings for any Apple storefront. apps = full metadata for specific apps, optionally compared across many countries.

## `charts` (type: `array`):

For mode='charts'. Only top-free and top-paid still exist — Apple removed top-grossing, new-apps-we-love and new-games-we-love from this API and now answers them with a 404 that looks exactly like a typo.

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

Two-letter Apple storefront codes, e.g. us, gb, de, jp, br. Price, currency, rating, rating count AND the current version all differ per storefront — the US was a whole release behind gb/jp/de when this was measured — so every row names the storefront it came from. Unknown codes are refused rather than sent, because apps.apple.com answers an unrecognised storefront with HTTP 200 serving the US page.

## `chartLimit` (type: `integer`):

Between 1 and 100. Apple answers 101 or more with an HTTP 500 HTML error page rather than a 400 or a clamp, so this is capped in code.

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

For mode='apps'. A numeric App Store ID (284882215), a bundle ID (com.facebook.Facebook), or a full apps.apple.com URL. Looked up in every storefront listed above, so N apps × M countries rows.

## `hydrateChartApps` (type: `boolean`):

A chart row carries only 8 fields (name, developer, genres, artwork, release date, URL). Turning this on adds the full 30+ field record — rating, rating count, price, description, version, size, minimum OS, languages, screenshots — for one extra batched request per 200 apps. Rows say which shape they came from in metadataSource.

## `includeStorefrontRatings` (type: `boolean`):

Fetches each app's product page for a second, independent rating figure from its JSON-LD. The two sources disagree — measured 28,008,005 against 28,006,734 on the same app, storefront and minute — so both are published with the gap as a number. This also detects Apple silently serving the US page for a storefront, which is otherwise invisible. Costs one request per app per country.

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

Requests in flight at once. Apple's chart host is genuinely flaky under load — six identical requests returned one connection error — so the retry ladder matters more than raw parallelism here.

## `minRequestInterval` (type: `integer`):

Politeness pacing shared across all workers. 0 uses the built-in default.

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

Optional and OFF by default. All three Apple hosts are public and keyless and showed no WAF — seven TLS profiles returned byte-identical responses.

## Actor input object example

```json
{
  "mode": "charts",
  "charts": [
    "top-free",
    "top-paid"
  ],
  "countries": [
    "us",
    "gb",
    "de",
    "jp"
  ],
  "chartLimit": 50,
  "apps": [
    "284882215",
    "com.spotify.client",
    "https://apps.apple.com/us/app/id6448311069"
  ],
  "hydrateChartApps": true,
  "includeStorefrontRatings": false,
  "maxConcurrency": 4,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "charts": [
        "top-free"
    ],
    "countries": [
        "us"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/appstore-apps-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 = {
    "charts": ["top-free"],
    "countries": ["us"],
}

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/appstore-apps-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 '{
  "charts": [
    "top-free"
  ],
  "countries": [
    "us"
  ]
}' |
apify call scrapyx/appstore-apps-scraper --silent --output-dataset

```

## MCP server setup

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