# App Store top charts & Google Play top charts scraper (`petersutarik/app-store-top-charts`) Actor

Top free, top paid and top grossing app rankings from the App Store and Google Play top charts — overall and per category, in any country — with each app's rank change since the previous day, new entries and apps that dropped out. Built by the team behind trysonar.app.

- **URL**: https://apify.com/petersutarik/app-store-top-charts.md
- **Developed by:** [Peter Šutarík](https://apify.com/petersutarik) (community)
- **Categories:** Developer tools, Marketing, SEO tools
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 chart entries

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 top charts & Google Play top charts scraper

Get the **App Store top charts** and the **Google Play top charts** — **top free, top paid and top grossing apps**, overall or per category, in any country, up to 200 deep — with **each app's rank change since the previous day**, the apps that are **new to the chart**, and the ones that **dropped out**. No account, no API key, no setup: pick the charts, run, get daily app rankings.

Built by the team behind [Sonar](https://trysonar.app), the ASO tool for indie app developers. The charts come from the same snapshots Sonar's own Top Charts page runs on, so the movement is computed against a real stored history, not guessed from a single fetch.

### What data does this top charts scraper give you?

| Feature | Output |
|-|-|
| **Top charts, 200 deep** | Top free, top paid and top grossing for the App Store and Google Play, the store-wide chart or any of 34 categories |
| **Daily movement** | `previousRank` and `delta` for every app (positive = moved up), `isNew` for apps that weren't in the chart the previous day |
| **Dropped apps** | Apps that were in your top N on the previous day and left the chart altogether |
| **App details** | Name, developer, rating, icon and a direct store link, plus the App Store numeric ID and bundle ID or the Google Play package name |
| **Any country** | Every Google Play market and 117 App Store storefronts, up to 25 countries per run |

### How to track App Store and Google Play top charts every day

1. Pick the store (`ios`, `android` or `both`), the countries, the charts (free, paid, grossing) and the categories.
2. Set how many apps you want per chart (up to 200).
3. Run — you get one row per app per chart, with its rank today and its rank on the previous snapshot.
4. **Add an Apify Schedule** (e.g. daily at 08:00 UTC) and every run lands a dated snapshot in its dataset. Join them on `appId` and `measuredAt` and you have a chart-history database — rank curves, new entrants, who climbs after an update — for a fraction of what a chart-tracking seat costs.

The charts are refreshed once a day, so one scheduled run per day is all you need.

### Use cases

- **Indie developers** — watch your category's top 200 and see the day your app breaks in (or slips out)
- **Market research** — spot new entrants and fast climbers before they show up in anyone's newsletter
- **Competitor monitoring** — track where competing apps sit in the grossing charts, country by country
- **Agencies & analysts** — daily chart snapshots for client reports and app-market newsletters
- **AI agents & pipelines** — call it through the Apify API and get clean, typed JSON rows

### Input example

```json
{
    "store": "both",
    "countries": ["us", "gb", "de"],
    "charts": ["free", "grossing"],
    "categories": ["overall", "HEALTH_AND_FITNESS"],
    "limit": 100,
    "includeDropped": true
}
```

That's 24 charts: 2 stores × 3 countries × 2 charts × 2 categories. A run can fetch up to 30.

With `store: "both"`, each category is mapped to the other store's equivalent — Games is `GAMES` on the App Store and `GAME` on Google Play, Utilities is `TOOLS`, Social Networking is `SOCIAL`, and so on. The category picker notes the Google Play name wherever it differs. Categories only Google Play has (Dating, Beauty, Comics, …) are skipped on the App Store with a warning in the log.

### Output example

One dataset row per app per chart, discriminated by `type`:

```json
{ "type": "entry", "store": "ios", "country": "us", "chart": "free", "category": "overall", "categoryLabel": "All categories", "rank": 4, "previousRank": 9, "delta": 5, "isNew": false, "appId": "6448311069", "bundleId": "com.openai.chat", "name": "ChatGPT", "developer": "OpenAI", "rating": 4.9, "iconUrl": "https://is1-ssl.mzstatic.com/…/100x100bb.jpg", "storeUrl": "https://apps.apple.com/us/app/id6448311069", "measuredAt": "2026-10-28", "previousMeasuredAt": "2026-10-27", "stale": false, "checkedAt": "2026-10-28T08:00:04.112Z" }
{ "type": "entry", "store": "android", "country": "us", "chart": "free", "category": "GAME", "categoryLabel": "Games", "rank": 12, "previousRank": null, "delta": null, "isNew": true, "appId": "com.example.puzzle", "bundleId": null, "name": "…", "developer": "…", "rating": 4.69, "iconUrl": "…", "storeUrl": "https://play.google.com/store/apps/details?id=com.example.puzzle&gl=US", "measuredAt": "2026-10-28", "previousMeasuredAt": "2026-10-27", "stale": false, "checkedAt": "…" }
{ "type": "dropped", "store": "ios", "country": "us", "chart": "free", "category": "overall", "categoryLabel": "All categories", "rank": null, "previousRank": 86, "delta": null, "isNew": false, "appId": null, "bundleId": "com.example.rewards", "name": "…", "developer": null, "rating": null, "iconUrl": "…", "storeUrl": null, "measuredAt": "2026-10-28", "previousMeasuredAt": "2026-10-27", "stale": false, "checkedAt": "…" }
```

| Field | Meaning |
|-|-|
| `type` | `entry` = an app in the chart, `dropped` = an app that left the chart since the previous snapshot, `error` = a chart that couldn't be served (not charged) |
| `rank` | Position in the chart today (1 = top). `null` on dropped rows |
| `previousRank` | Position on the previous snapshot. `null` for apps new to the chart |
| `delta` | `previousRank - rank`: positive = moved up, negative = moved down, `null` when new or there's no previous snapshot |
| `isNew` | `true` when the app wasn't in the previous snapshot |
| `appId` | App Store: the numeric Apple ID (as a string). Google Play: the package name |
| `bundleId` | App Store bundle ID. `null` on Google Play |
| `category` / `categoryLabel` | The store's own category key and name (`GAME` / "Games" on Google Play, `GAMES` / "Games" on the App Store) |
| `storeUrl` | Link to the listing in that country's store |
| `measuredAt` | Date of the chart snapshot served |
| `previousMeasuredAt` | Date the movement is compared against — usually the day before |
| `stale` | `true` when today's chart couldn't be read in time and the newest stored snapshot (at most 7 days old) was served instead |
| `error` | Only on `error` rows: what went wrong, in Sonar's words |

The dataset's **Top charts** table shows every row; filter on `type` to separate entries, dropped apps and errors.

### Why movement needs a previous day

Movement is today's chart compared with the previous stored snapshot of the same chart. The popular charts (US top free and the like) have been snapshotted daily for months, so they come with movement from the first run. A chart nobody has asked for before — a category in a smaller market, say — is fetched live the first time, and that first snapshot has nothing to compare against: `previousRank` and `delta` are `null` and `previousMeasuredAt` is `null`. From the next day on it has movement like any other chart. If a chart wasn't requested for a few days, `previousMeasuredAt` tells you which day the movement is measured from.

### Pricing (pay per event)

| Event | Price | Meaning |
|-|-|-|
| `chart-entry` | $0.001 | One app in a top chart, including apps that dropped out |

You only pay for the rows the run produces — no platform usage on top, so the event price is the whole bill. Charts that can't be served come back as `error` rows and are not charged.

**Worked example:** the US top free 200 every day is 200 entries plus the day's dropouts (around a dozen on a typical day), about 210 rows a day × $0.001 × 30 days ≈ **$6 a month**. A one-off look at the top 10 grossing in 5 countries is 50 rows, or 5 cents.

The run respects your maximum charge per run: once it's reached, the run stops and nothing beyond it is stored or charged.

### Related actors

- [App Store & Google Play ASO keyword rank tracker + popularity](https://apify.com/petersutarik/aso-keyword-intel) — use it when you care where apps rank in keyword search rather than in the charts: tracked app ranks or the top apps per keyword, with Apple's own Search Popularity and difficulty.
- [App Store keyword research & Google Play ASO keyword ideas](https://apify.com/petersutarik/aso-keyword-finder) — use it to find keywords worth targeting for your app, from seed words or any app ID, sorted by opportunity.

### FAQ

**Do I need an API key or any setup?** No. Pick the charts, run, get data. Everything is charged through your Apify account, pay per event.

**Which countries are supported?** Every Google Play market, and 117 of the 175 App Store storefronts — all the large ones (the rest are small storefronts Apple can't be asked about directly). A country a store has no chart for comes back as an `error` row (not charged) and the run carries on with the rest.

**Which charts and categories?** Top free, top paid and top grossing; the store-wide chart plus 23 App Store and 32 Google Play categories (34 in the picker, with the shared ones mapped across stores). Game sub-genres aren't included — Games is one chart.

**How fresh is the data?** Each chart is a daily snapshot: the first request of the day reads the chart from the store, later requests that day get the same snapshot. `measuredAt` is the snapshot's date. If the store can't be read, or reading it takes too long, the newest stored snapshot (up to 7 days old) is served with `stale: true` rather than failing.

**Why is a chart sometimes an `error` row?** A chart nobody has asked for today is fetched from the store during the run. The run waits for it and retries a few times; if the store is slow or unavailable, that chart comes back as an `error` row and isn't charged, and the next run usually has it.

**Why at most 30 charts per run?** Charts are fetched one at a time, and a chart nobody has asked for today can take a minute or more to read from the store. Thirty keeps a run predictable; for more, split them across several runs or schedules.

**Why do some dropped App Store apps have no `appId`?** When the chart data doesn't include a departed app's numeric Apple ID, the app is only known by its bundle ID: `appId` and `storeUrl` are `null` on that `dropped` row and `bundleId` is set. When the numeric ID is there, a dropped row carries `appId` and `storeUrl` exactly like an entry. Google Play dropped rows always carry the package name.

**Can I use it from the API or an AI agent?** Yes, like any Apify actor: start a run through the API and read the dataset as JSON or CSV. Rows are typed (`entry` / `dropped` / `error`) for easy filtering.

**Need keyword rankings and search popularity too?** The companion actor [App Store & Google Play ASO keyword rank tracker + popularity](https://apify.com/petersutarik/aso-keyword-intel) tracks where apps rank for keywords and returns Apple's own Search Popularity and difficulty. For alerts, competitor tracking, review analysis and an MCP server for AI agents, see [Sonar](https://trysonar.app).

**Something not working?** Open an issue in the Issues tab — this actor runs on our own product's data, so it's actively maintained.

# Changelog

This Actor's version history is a separate document: https://apify.com/petersutarik/app-store-top-charts/changelog.md

# Actor input Schema

## `store` (type: `string`):

Which store's charts to fetch. With both, each category is mapped to the other store's equivalent (Games is GAMES on the App Store and GAME on Google Play); a category one store doesn't have is skipped there with a warning in the log.

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

Two-letter country codes of the storefronts (e.g. us, gb, de, jp), up to 25. A country a store has no chart for comes back as a row with an error (not charged) and the run carries on.

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

Which top charts to fetch: top free apps, top paid apps and/or top grossing apps.

## `categories` (type: `array`):

All categories is the store-wide chart; pick any number of category charts on top. Labels note the Google Play name where it differs; categories marked Google Play only are skipped on the App Store. A run fetches at most 30 charts (stores × countries × charts × categories).

## `limit` (type: `integer`):

How many apps to return per chart, from the top (1–200). Charts are 200 deep; on the rare day the App Store's full list can't be read, an App Store chart comes back 100 deep.

## `includeDropped` (type: `boolean`):

Also return the apps that were in the top N (the number above) on the previous day and are no longer in the chart at all, as rows with type dropped and rank null.

## Actor input object example

```json
{
  "store": "ios",
  "countries": [
    "us"
  ],
  "charts": [
    "free"
  ],
  "categories": [
    "overall"
  ],
  "limit": 100,
  "includeDropped": true
}
```

# Actor output Schema

## `overviewDatasetUrl` (type: `string`):

One row per app per chart: rank, movement since the previous day, app details. Rows of type dropped are apps that left the chart; rows of type error are charts that couldn't be served (not charged).

## `allRowsDatasetUrl` (type: `string`):

Every row with every field, including bundle IDs and snapshot dates.

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

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

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

```

## MCP server setup

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

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/ag4y5xALHMI0tMV1p/builds/4TjSjclcmPm6jAx2I/openapi.json
