# Google Play Apps Scraper (`scrapyx/googleplay-apps-scraper`) Actor

Apps, keyword search and developer catalogues from the Google Play Store. Publishes both of the rating counts Google ships — they disagree by up to 6% — and refuses an unknown storefront country rather than letting Google price the app from your exit IP.

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

## Pricing

from $1.40 / 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

## Google Play Apps Scraper

Apps, keyword search and developer catalogues from the **Google Play Store**.
HTTP-only, no API key, no login, no browser.

### Modes

| Mode | What you get |
| --- | --- |
| `apps` | The full app document for package ids you name — ratings, install counts, price, developer, category, content rating, release date. |
| `search` | Play Store keyword search. 30 apps per page; add `fetchAppDetails` to enrich each one. |
| `developer` | Every app published by one developer. |

### Four upstream quirks it corrects

#### 1. The page ships **two rating counts, and they disagree** — on every app

An app page carries a JSON-LD `aggregateRating` **and** a `ds:9` data block.
They are not the same numbers. Measured in the same response, the same minute:

| App | JSON-LD count | data layer total | gap |
| --- | ---: | ---: | ---: |
| `com.whatsapp` | 242,062,178 | 237,517,908 | **4,544,270** (1.9%) |
| `com.spotify.music` | 36,189,697 | 34,027,397 | **2,162,300** (6.0%) |
| `com.duolingo` | 49,046,761 | 46,446,301 | **2,600,460** (5.3%) |
| `com.mojang.minecraftpe` | 5,804,850 | 4,566,585 | **1,238,265** (**21.3%**) |

That last row came from the **first cloud run**, after the docs had already
claimed the gap tops out around 6% based on the three apps in reconnaissance.
It does not. Treat the discrepancy as unbounded — there is no ratio you can
correct one figure by to obtain the other.

The averages disagree too — 4.617702 vs 4.6164336, 4.343110 vs 4.3147283,
4.731406 vs 4.728731. And the data layer's own five-star histogram **does not
sum to its own stated total**: WhatsApp's buckets add to 237,517,829 against a
stated 237,517,908.

Nothing in the payload says which figure answers *"how many people rated
this"*. So nothing here reconciles them. Both are published under names that
state their source — `ratingCountFromJsonLd`, `ratingCountFromDataLayer`,
`ratingAverageFromJsonLd`, `ratingAverageFromDataLayer` — the histogram ships
with its own arithmetic as `ratingHistogramSum`, and every row carries
`ratingCountsDisagree` and `ratingCountGap` so the discrepancy is visible on
the record rather than discovered later in aggregate.

#### 2. `country` changes the **price**, and an unknown one is priced from your exit IP

Same app, `com.mojang.minecraftpe`, same minute:

```
country=US -> 6.99 USD      country=GB -> 6.99 GBP      country=ID -> 19000 IDR
country=ZZ -> 19000 IDR   <- bogus, priced from the EXIT IP
```

Google Play does **not** reject an unrecognised country. It silently falls back
to the storefront of whatever IP the request came from — so the same input
would produce different prices depending on where the run executed. This actor
validates `country` against a known list and **refuses** an unknown one before
spending a request. Every row records `countryQueried` so the storefront the
figures came from is never in doubt.

#### 3. …but `country` is **inert on search**

`q=vpn` with `gl=US`, `gl=GB` and `gl=ID` returned the **identical 30 packages
in identical order**. The parameter that decides pricing does nothing to search
results. Stated on every summary as
`countryAffectsPriceNotSearchResults: true`, so nobody builds a
region-comparison on a dimension that does not move.

#### 4. Install counts are rounded down, hard

Installs live only in the data layer, as a display string *and* an exact
figure: WhatsApp shows **"10,000,000,000+"** against an actual
**12,257,877,869**. Both are published (`installsDisplayed`, `installsExact`) —
the rounded one understates by over two billion.

### Output

One `SEARCH_SUMMARY` per run, one `APP` per app, one `ERROR` per failure.

`APP` rows carry `packageId`, `appUrl`, `appName`, `description`, `category`,
`operatingSystem`, `contentRating`, `iconUrl`, `developerName`,
`developerUrl`, both rating pairs, `ratingHistogram`, `ratingHistogramSum`,
`ratingCountsDisagree`, `ratingCountGap`, `price`, `priceCurrency`, `isFree`,
`availability`, `installsDisplayed`, `installsExact`, `releasedOn`,
`countryQueried`, `languageQueried` and `resultRank`.

In `search` and `developer` mode without `fetchAppDetails`, rows are the
ordered package list only — one request total instead of one per app.

### Limits

- **No top-charts mode.** `/store/apps/collection/*` returns 937 KB of HTML
  whose content block is **59 bytes** and which contains zero package ids —
  the chart is rendered client-side. Declared unavailable rather than shipped
  as a mode that silently returns nothing.
- A search page yields 30 apps and exposes no next-page control in the served
  HTML.
- Prices and availability are the storefront's, not a user's — no account, no
  device filter, no regional carrier pricing.
- There is **no WAF on this host**: 8 TLS profiles × 4 surfaces returned 32
  clean responses with a full data layer. A proxy is offered for data-quality
  reasons (see quirk 2), not for access.

# Actor input Schema

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

apps = full detail for package ids you name. search = Play Store keyword search. developer = every app by one developer. There is no top-charts mode: Google Play renders those collections client-side and serves zero package ids in the HTML.

## `packageIds` (type: `array`):

For mode='apps'. A package id like `com.whatsapp` or a full play.google.com/store/apps/details URL. A package that does not exist is an honest 404.

## `searchTerm` (type: `string`):

For mode='search'. One page yields 30 apps. A term that matches nothing returns an honest zero, not a widened list.

## `developerId` (type: `string`):

For mode='developer'. The numeric id from a /store/apps/dev?id=... URL. An unknown id is an honest 404.

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

Two-letter code. THIS CHANGES PRICES: the same paid app is 6.99 USD in US, 6.99 GBP in GB and 19,000 IDR in ID. An unrecognised code is REFUSED here, because Google Play does not reject one — it silently prices the app in the currency of whatever IP the request came from, so the same run would give different prices depending on where it executed. Note it is INERT on search: US, GB and ID returned the identical 30 packages in identical order.

## `language` (type: `string`):

Two-letter code used for descriptions and category names.

## `fetchAppDetails` (type: `boolean`):

search/developer only — apps mode always fetches it. Off returns just the ordered package list at one request total; on adds ratings, install counts, price, developer and description at one request per app.

## `maxResults` (type: `integer`):

Set 0 for unlimited. A search page holds 30 apps and exposes no next-page control in the served HTML.

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

App-detail fetches in flight at once.

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

Politeness pacing shared across all workers. 0 uses the built-in default. Reconnaissance saw zero interstitials across 32 requests; raise this if any appear.

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

No WAF was found on this host at all — 32 of 32 probe requests were clean without a proxy. Residential still defaults on for cloud runs for a data-quality reason rather than an access one: an unrecognised country is priced from the exit IP.

## Actor input object example

```json
{
  "mode": "apps",
  "packageIds": [
    "com.whatsapp",
    "https://play.google.com/store/apps/details?id=com.spotify.music"
  ],
  "searchTerm": "vpn",
  "developerId": "5700313618786177705",
  "country": "US",
  "language": "en",
  "fetchAppDetails": false,
  "maxResults": 30,
  "maxConcurrency": 4,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/googleplay-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/googleplay-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 '{}' |
apify call scrapyx/googleplay-apps-scraper --silent --output-dataset

```

## MCP server setup

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