--- name: app-store-rankings-scraper description: Read Apple App Store top charts as ranked rows via the Apify Actor arman-bd/app-store-rankings-scraper. Returns rank, app id, app name, publisher, price, currency, category, release date, icon and the full store description for the top free, top paid and top grossing charts of any storefront and any Apple genre. Use when a task needs rank tracking, ASO competitor monitoring, breakout detection or a cross-market snapshot of who is charting. Not for ratings, review counts, download estimates, app detail lookups by id, or iPad and Mac charts. --- # App Store Rankings Scraper Apify Actor `arman-bd/app-store-rankings-scraper`. Give it storefronts, chart types and optional Apple genre ids, get one dataset record per ranked app per chart. It runs without credentials and without proxy configuration. ## When to use it - A rank time series: run daily, filter to your `appId`, plot `rank` against `scrapedAt`. - Competitive ASO: pull `toppaid` and `topgrossing` for a category and watch publishers move week over week, with `summary` as their live store copy. - Breakout detection: diff consecutive runs on `appId` inside one category chart. - A cross-market snapshot: the same chart across `us`, `gb`, `de`, `jp`, `br` in one run. - Price and currency comparison for the same app across storefronts. ## When not to use it - Ratings, review counts, review text or download estimates. None of those are in the chart data and none are returned. - Looking up one known app by id. This Actor only walks charts; an app outside the top 100 of every chart you request will not appear at all. - iPad, Mac, Apple TV or Watch charts. Only the three iPhone chart types are exposed. - Google Play. Apple storefronts only. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/app-store-rankings-scraper').call({ countries: ['us', 'gb'], chartTypes: ['topfree', 'toppaid'], categoryIds: ['6014'], limit: 25, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~app-store-rankings-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"countries":["us"],"chartTypes":["topgrossing"],"categoryIds":["6014"],"limit":50}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/app-store-rankings-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `countries` | string[] | no | `["us"]` | Two-letter storefront codes, lowercase. Uppercase is lowered, blanks and duplicates are dropped. An unknown code is recorded as a failure and the run continues. | | `chartTypes` | string[] | no | `["topfree"]` | Any of `topfree`, `toppaid`, `topgrossing`. An unrecognised value aborts the whole run before any request, so validate this one yourself. | | `categoryIds` | string[] | no | `[]` | Apple genre ids as strings. Empty means the overall chart. Common ids: `6014` Games, `6015` Finance, `6016` Entertainment, `6017` Education, `6018` Books, `6020` Medical, `6023` Food & Drink, `6002` Utilities, `6005` Social Networking, `6008` Photo & Video, `6012` Lifestyle. | | `limit` | integer | no | `100` | Chart depth. Minimum 1, values above 200 are capped at 200. Apple truncates at roughly 100 in practice. | **The cross product is the whole cost model.** The Actor fetches every combination of country, chart type and category as its own chart, sequentially. Three countries by three chart types by two categories is 18 charts and up to 1,800 records, not 8. Widen one axis at a time and keep `limit` proportionate: breadth across storefronts wants a small `limit`, depth in one category wants a large one. ## Output One record per ranked app per chart. A record carries no key of its own; the tuple `country` + `chartType` + `categoryId` + `appId` is the natural primary key. | Field | Type | Notes | |---|---|---| | `country` | string | Storefront the chart came from, lowercase, as normalised from your input. | | `chartType` | string | `topfree`, `toppaid` or `topgrossing`. | | `categoryId` | string \| null | The genre id you requested for this chart. `null` for the overall chart. | | `rank` | number | Position in that chart, 1-based, in the order the source returned. | | `appId` | string | Apple's numeric app id as a string. The join key across runs and storefronts. | | `appName` | string | App name as the storefront shows it, so it is localised per country. | | `publisher` | string | Developer or seller name. | | `price` | number \| null | Numeric price in the storefront's currency. `0` for free apps. `null` if the value did not parse. | | `currency` | string \| null | ISO currency code of that storefront, for example `USD`, `GBP`. | | `category` | string \| null | The app's own primary category name. This is not the chart you asked for. | | `releaseDate` | string | First release date, ISO 8601 with the storefront's timezone offset, so not plain UTC. | | `iconUrl` | string \| null | Highest-resolution icon in the record, currently 100x100. | | `summary` | string \| null | Full store description text. Long, and useful for keyword work. | | `rights` | string \| null | Copyright line. | | `scrapedAt` | string | Run timestamp, ISO 8601 UTC. Diff on this. | A real record, long strings trimmed: ```json { "country": "us", "chartType": "topfree", "categoryId": "6014", "rank": 1, "appId": "6748084174", "appName": "Smash Fest!", "publisher": "Flow Games Bilisim Yazilim ve Pazarlama Anonim Sirketi", "price": 0, "currency": "USD", "category": "Games", "releaseDate": "2025-12-16T00:00:00-07:00", "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/74/15/3a/74153a85-8436-259e-5297-63fbd718af84/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/100x100bb.png", "summary": "Welcome to Smash Fest! Get ready for the most explosive festival in town! …", "rights": "© Flow Games 2025", "scrapedAt": "2026-08-06T11:31:55.000Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "chartsRequested": 6, "chartsFailed": 2, "failures": [ { "chart": "zz/topfree/6014", "error": "no such chart, check country \"zz\" (HTTP 400)" }, { "chart": "zz/toppaid/6014", "error": "no such chart, check country \"zz\" (HTTP 400)" } ], "appsSaved": 100, "appsPerChart": { "us/topfree/6014": 25, "us/toppaid/6014": 25, "gb/topfree/6014": 25, "gb/toppaid/6014": 25 }, "filters": { "countries": ["us", "gb", "zz"], "chartTypes": ["topfree", "toppaid"], "categoryIds": ["6014"], "limit": 25 }, "finishedAt": "2026-08-06T11:31:55.771Z" } ``` `appsPerChart` is keyed `country/chartType/categoryId`, with the category segment omitted for the overall chart. Compare its key set against the cross product implied by `filters`: any missing key is a chart that failed and will be named in `failures`. `chartsFailed` above zero with `appsSaved` above zero is a partial run, not a failed one. Do not treat a short `appsPerChart` value as a failure on its own, since a chart can legitimately return fewer rows than `limit`. ## Behaviour to plan around - **A bad `chartTypes` value kills the run; a bad country does not.** An unrecognised chart type throws during validation before any chart is fetched. An unknown storefront code is a per-chart failure that lands in `failures` and the run carries on. - **Asking for 200 legitimately returns about 100.** The Actor caps `limit` at 200 and the source accepts it, but Apple currently truncates every chart at roughly 100. The paid chart commonly serves 99. Neither is a bug here. - **`category` is the app's own category, not the chart's.** An app pulled from the Games chart can carry a different primary category. Group by `categoryId` when you mean "which chart did this come from". - **Ranks are the source's ordering, unchanged.** The Actor assigns `rank` by position in what it received and does no sorting or scoring. - **Prices are per storefront.** The same `appId` on `gb` returns GBP, on `de` returns EUR. Never compare `price` across countries without converting, and never assume `currency` from `country`. - **`appName` is localised.** The same app can appear under different names on `jp` and `us`. Join on `appId`, never on `appName`. - **Transient failures are retried** three times with linear backoff. An unknown storefront and a malformed response fail fast, because retrying those never helps. - **The run only throws when every chart failed.** Any surviving chart makes the run succeed with a non-empty `failures` list. - **One record has no url field.** There is no store link in the output. Build one from `appId` yourself if you need it. ## Recipes **Daily rank tracking for your own app.** Schedule this and keep every run's dataset. ```json { "countries": ["us", "gb", "de", "jp", "br"], "chartTypes": ["topfree", "topgrossing"], "categoryIds": ["6014"], "limit": 100 } ``` Filter to your `appId`, then plot `rank` against `scrapedAt` grouped by `country` and `chartType`. An `appId` absent from a run means it fell out of the top 100 there, which is a data point, not a gap. **Competitive ASO sweep across categories.** Paid and grossing carry the revenue signal. ```json { "countries": ["us"], "chartTypes": ["toppaid", "topgrossing"], "categoryIds": ["6015", "6017", "6023"], "limit": 100 } ``` Group by `publisher` to see who owns how many slots, and mine `summary` for the keywords the leaders are ranking on. **Breakout detection in one market.** Narrow and deep, so movement is legible. ```json { "countries": ["us"], "chartTypes": ["topfree"], "categoryIds": ["6014", "6005", "6008"], "limit": 100 } ``` Diff against yesterday on `appId` within the same `country` + `chartType` + `categoryId`. A first appearance, or a jump of more than about 20 places, is a candidate. Use `releaseDate` to separate genuinely new apps from old ones resurging. **Cheap cross-market overview.** Overall charts only, no categories, shallow depth. ```json { "countries": ["us", "gb", "de", "fr", "jp"], "chartTypes": ["topfree"], "limit": 25 } ``` Five charts, 125 rows. `categoryId` comes back `null` throughout, which is how you tell overall rows from category rows if you merge datasets.