# Google Play Store Scraper (`miladamirzadeh/google-play-scraper`) Actor

Fetches Google Play app metadata — ratings, installs, pricing, developer info, and version — with optional version gating.

- **URL**: https://apify.com/miladamirzadeh/google-play-scraper.md
- **Developed by:** [Milad Amirzadeh](https://apify.com/miladamirzadeh) (community)
- **Categories:** Automation, E-commerce, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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

## Google Play Scraper

Fetches app metadata from Google Play for a list of app URLs. Optionally gates output on the current store version being strictly newer than a supplied minimum version — useful for polling apps and only processing them when a new release appears. Sibling to the Apple App Store scraper in this workspace; same conventions, same `minVersion` gating idea, adapted to Google Play's data shape.

A pre-configured Apify task with example input is ready to run as-is: [Google Play — App Details](https://console.apify.com/actors/tasks/rZpCgD4NzkdmxufPl).

### Input

```json
{
  "apps": [
    {
      "url": "https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&hl=en&gl=US",
      "minVersion": "6.0"
    },
    {
      "url": "https://play.google.com/store/apps/details?id=org.telegram.messenger&hl=en&gl=US"
    }
  ],
  "maxRetries429": 3,
  "allowApproximateVersion": false
}
```

- `apps` (array, required): list of apps to scrape
  - `url` (string, required): a Google Play app URL, or a bare package name (e.g. `org.telegram.messenger`). Accepts locale/query params (`hl`, `gl`) embedded in the URL.
  - `minVersion` (string, optional): if provided, only emit full data when the current store version is **strictly greater than** this value. Omit to always fetch full data.
  - `hl` / `gl` (string, optional): override the URL's language/country for this item specifically.
- `proxyConfiguration` (object, optional): Apify proxy configuration.
- `maxRetries429` (boolean or number, optional, default `3`): retry strategy for HTTP 429 responses, same semantics as the App Store actor — `false`/`0` disables retrying, `true` uses the default of 3, or supply a custom count. Every attempt draws a fresh proxy URL with no sticky session, so each retry after a 429 goes out over a different IP, combined with exponential backoff (2s, 4s, 8s, ... capped at 30s, plus jitter).
- `allowApproximateVersion` (boolean, optional, default `false`): see "The version field" below.

### Output

One dataset record per input item, in input order. Non-skipped records include the full field set; skipped records include `skipped: true` and a `reason`; failed items include `skipped: true` and an `error`.

**Example — full record** (paid app, single canonical version):

```json
{
  "url": "https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&hl=en&gl=US",
  "appId": "net.dinglisch.android.taskerm",
  "skipped": false,
  "hl": "en",
  "gl": "US",
  "title": "Tasker",
  "description": "...",
  "summary": "Automate everything from settings to photos, SMS to speech. ADC2 prize winner.",
  "developer": "joaomgcd",
  "developerId": "8102570190170276456",
  "developerUrl": "/store/apps/dev?id=8102570190170276456",
  "developerWebsite": "https://tasker.joaoapps.com/dl",
  "developerEmail": "support@joaoapps.com",
  "developerLegalName": "KITXOO",
  "developerAddress": "Rua Elias Garcia\nn17 4A\n2700-310 Amadora\nPortugal",
  "developerPhone": "+351 969 390 591",
  "privacyPolicyUrl": "https://tasker.joaoapps.com/privacy.html",
  "genre": "Tools",
  "genreId": "TOOLS",
  "categories": [{ "name": "Tools", "id": "TOOLS" }],
  "icon": "https://play-lh.googleusercontent.com/...",
  "headerImage": "https://play-lh.googleusercontent.com/...",
  "screenshots": ["https://play-lh.googleusercontent.com/..."],
  "contentRating": "Everyone",
  "contentRatingDescription": "Content is generally suitable for all ages. May contain minimal cartoon, fantasy or mild violence and/or infrequent use of mild language.",
  "released": "Jul 3, 2010",
  "releasedTimestamp": 1278173814,
  "updated": "Feb 24, 2026",
  "updatedTimestamp": 1771934588,
  "installs": "1,000,000+",
  "minInstalls": 1000000,
  "realInstalls": 2592744,
  "score": 4.022774,
  "scoreText": "4.0",
  "ratings": 56074,
  "reviews": 6884,
  "histogram": { "1": 9518, "2": 2553, "3": 2553, "4": 3946, "5": 37497 },
  "free": false,
  "priceText": "$4.49",
  "priceCurrency": "USD",
  "priceMicros": 4490000,
  "offersIAP": false,
  "inAppPurchasePriceRange": null,
  "permissions": ["Device & app history", "Calendar", "Contacts", "Location", "..."],
  "version": "6.6.20",
  "versionSource": "play-store-page",
  "versionApproximate": false,
  "scrapedAt": "2026-09-19T21:46:25.421632+00:00"
}
```

**Example — skipped record** (current version not newer than `minVersion`):

```json
{
  "url": "https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&hl=en&gl=US",
  "appId": "net.dinglisch.android.taskerm",
  "skipped": true,
  "reason": "current version not newer than minVersion",
  "version": "6.6.20",
  "versionSource": "play-store-page",
  "minVersion": "99.0",
  "scrapedAt": "2026-09-19T21:46:25.421632+00:00"
}
```

**Example — error record** (bad package id, delisted app, or unavailable in the requested country):

```json
{
  "url": "https://play.google.com/store/apps/details?id=com.rovio.angrybirds&hl=en&gl=US",
  "appId": "com.rovio.angrybirds",
  "skipped": true,
  "error": "app 'com.rovio.angrybirds' not found or unavailable in gl=US (HTTP 404)",
  "scrapedAt": "2026-09-19T21:46:25.421632+00:00"
}
```

### The version field

Google Play does **not** expose a single "current version" the way the App Store does for every app. For the large majority of apps it's present in the very first page load (`title`, `developer`, etc. come from the same place, no extra request) — but apps published with per-device APK splits (Telegram, and many other very large apps) show **"Varies with device"** in the UI because there genuinely is no single canonical version on Google's side.

- `version`: the resolved version string, or `null` when Google has none to give.
- `versionSource`: `"play-store-page"` (the common, free case), `"reviews-heuristic"` (see below), or `null`.
- `versionApproximate`: `true` only when `versionSource` is `"reviews-heuristic"`.

When `version` is `null` and a `minVersion` was supplied, the item is **never skipped** — it's always fetched in full, per the standing rule that a missing/incomparable version should default to "treat as newer" rather than silently skip an app the caller might actually need.

If `allowApproximateVersion` is enabled, apps with no canonical version fall back to the highest version number seen among that app's own embedded user reviews (already present in the same page fetch — no extra request). This is real user data (what some reviewer had installed when they wrote their review), not Google's own "current version", so it can lag behind the true latest release — it's off by default for that reason.

See `.recon/FINDINGS.md` for the full reconnaissance writeup, including why a `batchexecute` RPC replay (the original plan) turned out to be unnecessary for the common case and unhelpful for the "Varies with device" case.

### Why Python + `curl_cffi`

Following this workspace's App Store actor for consistency, rather than because Google Play strictly requires it: Google Play's page served fine to a plain `curl` with a normal Chrome user agent during testing, with no TLS-level fingerprinting observed. `curl_cffi`'s Chrome impersonation is cheap insurance here, not a hard requirement the way it was for Apple's undocumented storefront API.

### What's fragile

Google Play's data is embedded in the page as `AF_initDataCallback({key: 'ds:N', ...})` blocks — **positional arrays with no field names**, unlike Apple's documented-shape JSON. If this actor starts returning unexpected `null`s or wrong values, check first, in order:

1. **`src/ds_blobs.py`'s `_KEY_RE` and block-parsing** — the `AF_initDataCallback({key: '...'` syntax itself (spacing around `key:`/`data:`/`sideChannel:`).
2. **The `ds:5` app-detail slot indices in `src/extract.py`** — Google reshuffles these between rollouts. `ds_blobs.assert_shape()` raises loudly on a gross structural change (fewer than 146 slots); individual field drift is quieter and shows up as unexpected `None`s. Re-run `python tools/refresh_fixtures.py` and re-verify indices against the fresh HTML.
3. **Whether `ds:5` is still the app-detail blob at all** — `find_detail_blob()` locates it by shape (matching the requested package at index `[77][0]`), so a bare key renumbering shouldn't break anything on its own.
4. **The `IMPERSONATE` constant in `src/play_api.py`** — keep it a recent supported Chrome target.
5. Fields whose position isn't fixed at all, notably `contentRatingDescription` (Google inserts a variable number of descriptor slots ahead of it depending on the app) — handled by a bounded scan (`_content_rating_description`) rather than a hardcoded index, but that scan's range may need widening someday.

Run `.venv/bin/python -m pytest tests/ -v` after any index change — `test_no_fallback_paths_used_on_real_fixtures` in particular will fail loudly if `link_url`/`image_url` had to fall back to a deep search on the real fixtures, which is the drift alarm for those two families of fields.

# Actor input Schema

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

List of Google Play apps to crawl.

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

If you want to use a proxy(recommended), you can configure it here.

## `maxRetries429` (type: `integer`):

How many times to retry a request after an HTTP 429 (Too Many Requests) response, rotating to a fresh proxy before each retry. Accepts `false`/`0` to disable retrying, `true` for the default of 3 retries, or any other number for a custom retry count.

## `allowApproximateVersion` (type: `boolean`):

Some apps (published with per-device APK splits) show 'Varies with device' and have no single canonical version on Google's side. When enabled, the actor falls back to the highest version number seen among the app's own embedded user reviews as an approximate stand-in — labelled `versionSource: "reviews-heuristic"` and `versionApproximate: true` in the output. This can lag behind the true latest release, so it's off by default; without it, such apps get `version: null` and are always treated as newer for minVersion gating.

## Actor input object example

```json
{
  "apps": [
    {
      "url": "https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&hl=en&gl=US",
      "minVersion": "6.0"
    },
    {
      "url": "https://play.google.com/store/apps/details?id=org.telegram.messenger&hl=en&gl=US"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxRetries429": 3,
  "allowApproximateVersion": false
}
```

# Actor output Schema

## `overview` (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 = {
    "apps": [
        {
            "url": "https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&hl=en&gl=US",
            "minVersion": "6.0"
        },
        {
            "url": "https://play.google.com/store/apps/details?id=org.telegram.messenger&hl=en&gl=US"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("miladamirzadeh/google-play-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 = {
    "apps": [
        {
            "url": "https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&hl=en&gl=US",
            "minVersion": "6.0",
        },
        { "url": "https://play.google.com/store/apps/details?id=org.telegram.messenger&hl=en&gl=US" },
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("miladamirzadeh/google-play-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 '{
  "apps": [
    {
      "url": "https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&hl=en&gl=US",
      "minVersion": "6.0"
    },
    {
      "url": "https://play.google.com/store/apps/details?id=org.telegram.messenger&hl=en&gl=US"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call miladamirzadeh/google-play-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,miladamirzadeh/google-play-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/ygXlcxtwPQq1GCKL7/builds/DhKUqpXzrEF1VaHXE/openapi.json
