# Google Play Apps Scraper (`dami_studio/google-play-apps-scraper`) Actor

Scrape Google Play app listings by keyword, category or package id: title, developer, rating, review count, installs, price, in-app purchases, content rating, version, last update, description, icon and screenshots. $0.25 per 1,000 apps - the cheapest on the market. No key, no login.

- **URL**: https://apify.com/dami\_studio/google-play-apps-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** E-commerce, Developer tools, Integrations
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / 1,000 google play apps

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/platform/actors/running/actors-in-store#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

Give it search terms, a Play Store category, or Android package ids, and get the app listings back as flat rows: title, developer, star rating, rating and review counts, install band, price and currency, in-app purchase range, whether it carries ads, content rating, release date, last update, version, the full description, the icon and every screenshot. No account, no cookies, no login, no browser.

- Three ways in — search terms, a store category, or a list of package ids or Play links — all producing the same row shape.
- Reads the Play Store in whichever country and language you ask for, so prices, install bands and availability are the ones a user there would see.
- Every row carries the numeric field next to the display field: `score` as well as `4.5`, `minInstalls` and `realInstalls` as well as `10,000,000+`, `price` in units as well as `$0.99`.
- Runs with empty input return a labelled sample row, free, so you can see the shape before you spend anything.
- Apps that do not exist, or are not distributed in the country you asked for, come back as free diagnostic rows rather than as silence or as a failed run.

### Price

**$0.25 per 1,000 apps**, plus a **$0.001 start fee per run** (billed per gigabyte of run memory, so exactly that on the default 1 GB).

This is **the cheapest Google Play apps scraper on the market**, and it is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

| Apps | Total cost |
|---|---|
| 100 | $0.026 |
| 1,000 | $0.251 |
| 10,000 | $2.501 |
| 100,000 | $25.001 |

#### What is actually charged

- **One `app-scraped` event per app row written to the dataset.** Nothing else is metered per row.
- **Free:** the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry `"charged": false`.
- An app that appears under two of your search terms is delivered and charged once, not twice.
- Package ids that Play has no listing for produce an uncharged diagnostic row, never a charged one.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, so there is no price quietly attached to `apify-default-dataset-item` — the trick that makes some scrapers bill you for their own error messages.

### Input

```json
{
  "searchTerms": [
    "budget tracker",
    "habit tracker"
  ],
  "appIds": [
    "com.whatsapp",
    "https://play.google.com/store/apps/details?id=com.spotify.music"
  ],
  "category": "PRODUCTIVITY",
  "country": "us",
  "language": "en",
  "maxItems": 50
}
```

| Field | What it does |
|---|---|
| `searchTerms` | Words or phrases to search the Play Store for, the same way the store search box works. Up to 20 per run. Each term contributes its share of the row budget. |
| `appIds` | Android package ids (`com.whatsapp`) or full Play links (`https://play.google.com/store/apps/details?id=com.whatsapp`). Both forms are accepted in the same list, and these are fetched before anything else because they are exactly what you asked for. |
| `category` | A Play Store category id such as `PRODUCTIVITY`, `COMMUNICATION`, `GAME_PUZZLE`, or the roll-ups `APPLICATION` and `GAME`. Returns the apps Play itself features on that category page — the top free, top grossing, top paid and trending shelves — in Play's own order. |
| `country` | Two-letter country code, default `us`. Play is a per-country store: price, currency, install band, content rating and even whether the app exists all depend on this. |
| `language` | Two-letter language code, default `en`. Controls the language of the title, summary, description, changelog and content-rating text. |
| `maxItems` | Total rows to return across everything you asked for. Package ids are taken first, then the remaining budget is split evenly between the search terms and the category, so two terms and 50 rows gives 25 of each. Default 20, hard ceiling 2,000. Keep it low while testing — you pay per row. |
| `proxyUrls` | Leave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for, as `http://user:pass@host:port`. |

Run it with **empty input** and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

### Output

One row per app. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "app",
  "query": "habit tracker",
  "searchType": "search",
  "appId": "org.isoron.uhabits",
  "url": "https://play.google.com/store/apps/details?id=org.isoron.uhabits&hl=en&gl=us",
  "title": "Loop Habit Tracker",
  "summary": "Create good habits and track their progress over time",
  "description": "Loop helps you create and maintain good habits, allowing you to achieve your long-term goals. Detailed charts and statistics show you how your habits improved over time. The app is completely ad-free, open source and it respects your privacy.\n\nSimple, beautiful and modern interface",
  "developer": "Álinson S Xavier",
  "developerId": "Álinson S Xavier",
  "developerUrl": "https://play.google.com/store/apps/developer?id=%C3%81linson%20S%20Xavier",
  "developerEmail": "dev@loophabits.org",
  "developerWebsite": null,
  "genre": "Productivity",
  "genreId": "PRODUCTIVITY",
  "score": 4.718845,
  "scoreText": "4.7",
  "ratings": 63027,
  "reviews": 3406,
  "histogram": {
    "1": 1819,
    "2": 950,
    "3": 1819,
    "4": 3925,
    "5": 54494
  },
  "installs": "5,000,000+",
  "minInstalls": 5000000,
  "realInstalls": 7750308,
  "price": 0,
  "currency": "USD",
  "priceText": "Free",
  "free": true,
  "offersIAP": false,
  "inAppPurchaseRange": null,
  "containsAds": false,
  "contentRating": "Everyone",
  "contentRatingDescription": null,
  "released": "Feb 22, 2016",
  "updated": "2025-09-14T22:56:06.000Z",
  "updatedText": "Sep 14, 2025",
  "version": "2.3.1",
  "androidVersion": "9",
  "icon": "https://play-lh.googleusercontent.com/p0AgkzxC9Vus-E82psxrb7p8VtrVJMfXKcU9hNTW8tTG7G3yAGBqxCIyUmwibUyT1Mx7DAzkrJnPLo4VEYDwlbI",
  "headerImage": "https://play-lh.googleusercontent.com/saJuIprQv_9zeut6372XuLUEWSSB9iAJIwFc2oASfly5r3AQaNZiVWQ9_Rc1fLe2Y4bNF6fljlze-I7yorTP",
  "screenshots": [
    "https://play-lh.googleusercontent.com/KrxN9bOtspk63xhtNJx2uyaHZ3GOYK_IJ-VRIhMlh2ChFCQuBxXLWoRJd48uFRU_Rk97Nz3gHiI0eteLn2KDFQ",
    "https://play-lh.googleusercontent.com/2nZZDFddPS4-wHVgPvAfeYik3vMTsrV-Mgys3g1A6s-KLCcUWsf1PlYLJfKXGyxUnUHkSKX8sESQkrEx6mQN",
    "https://play-lh.googleusercontent.com/8BBDln1lKgX6ErWkH-_tKlfoD2TEfpcVzrgFlzJO91wK0HiYac_W0wJV7hHhGi8aeqk-8xRI051bK6mlsOLiZw",
    "https://play-lh.googleusercontent.com/OBzxPYCYyPhV4C3O70raVjvafSDynHVAvGZyVsSsj4EtrsW1OrgV11Jq-MTLqq0dOZSOSfG47IUR8Z1Uw50Hs1U",
    "https://play-lh.googleusercontent.com/h8BsLCGeOvth65FDex4F3eyobKutYjISeo31DXSZPAgr7HHqlEBgiEjI-z48NTlQVZdcito_AgyXKRfxQdXAovg",
    "https://play-lh.googleusercontent.com/LbTcXRMXTapKyajRNqnrYROysAc1QOVAZ1eigPmVDNqJmymjqAtqEDketTnMeSCRvUg4jH53T47Ds1jshCIMww"
  ],
  "privacyPolicy": "http://loophabits.org/privacy",
  "available": true,
  "topChartRank": null,
  "country": "us",
  "language": "en",
  "scrapedAt": "2026-08-15T22:19:24.290Z"
}
```

#### Field notes

- `query` — the search term, category id or package id that produced this row, so a multi-term run can be split back apart afterwards. `searchType` says which of the three it was.
- `appId` — the Android package id. Stable, unique, and the right primary key when you re-run.
- `score` — the star rating as a float (`4.5214686`); `scoreText` is the rounded string Play prints (`4.5`).
- `ratings` — how many people left a star rating. `reviews` is the smaller number who also wrote text. `histogram` breaks the ratings down one star at a time.
- `installs` — Play only publishes a band (`10,000,000+`). `minInstalls` is that band as a number and `realInstalls` is the finer figure Play exposes behind it.
- `price` — in whole currency units, so 6.99 not 6990000. `priceText` is what the store button says, and `free` is simply `price === 0`.
- `inAppPurchaseRange` — the "$0.99 - $54.99 per item" line, present only when the app sells anything inside itself. `offersIAP` is the boolean form.
- `updated` — the last update as an ISO 8601 timestamp in UTC. `updatedText` is Play's own localised rendering of the same moment, so the two can differ by a day either side of midnight.
- `version` — null for apps Google delivers per device rather than as one build — the store shows "Varies with device" for those, and so there is no version string to report. Same for `androidVersion`.
- `screenshots` — every screenshot on the listing, in the order Play shows them, as full-size image URLs. `icon` and `headerImage` are separate fields.
- `available` — whether Play will actually let somebody in the country you asked for install it. A listing that exists but is not distributed there comes back with its metadata and `available: false`.
- `topChartRank` — set only while the app is sitting in a Play top chart, as "#1 in top free communication".

Every real row carries `"charged": true`. Sample rows carry `"_sample": true` and diagnostic rows carry `"_diagnostic": true` with an `errorCode` you can filter on, and neither is ever billed.

### How it works

- It talks to the Play Store the way the store's own web front end does: one request per app for the listing data, and one request per search term or category to find which apps to look up. No page rendering, no headless browser, no login, no cookies.
- Because it asks for data rather than for a page, a listing costs about thirteen kilobytes instead of the better part of a megabyte — which is most of the reason this is the cheapest way to read Play at volume.
- Requests leave through a large pool of rotating datacenter addresses, so a per-address rate limit is answered by moving rather than by waiting.
- Country and language are passed through to the store on every request, so one run can only ever describe one storefront. Run it twice for two countries.

### What people use it for

- App store optimisation: track where your app and the other apps in your niche rank for the search terms that matter, and watch rating, review count and install band move week by week.
- Market research on a category — pull `GAME_PUZZLE` or `FINANCE` and get a clean table of who is in it, how big they are, what they charge and what they charge for inside the app.
- Release monitoring: schedule the same package ids daily and diff on `version`, `updated` and `recentChanges` to see exactly when each app shipped and what its changelog said.
- Lead lists for app-industry services — `developer`, `developerEmail`, `developerWebsite` and `developerAddress` come straight off the listing.
- Pricing research across countries: the same `appIds` with a different `country` shows what each storefront charges, in its own currency.
- Feeding an internal dashboard or a model with a typed, flat table instead of parsing a rendered store page.

### Reading the output

Every run writes three kinds of row, and they are easy to tell apart:

- **Real rows** carry `"charged": true` and `"recordType": "app"`. These are the rows you paid for, one billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There is exactly one, it only appears when the input had no search terms, package ids or category, and it exists so you can look at the shape of the output before you spend anything.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode` you can switch on: `NOT_FOUND` when Play has no listing for a package id in that country, `NO_RESULTS` when a search term or category matched nothing, `BLOCKED` when the store refused the request, `NETWORK` when it could not be reached, `TIME_BUDGET` when the run ran out of time before reaching an item, and `BAD_INPUT` when a category id was not one Play recognises. Each one carries a plain-English `error` and the `query` and `appId` it belongs to.

If you only want the data, filter on `charged == true`. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.

### Country and language

Play is not one store, it is a store per country, and the difference is not cosmetic. An app can be free in one country and paid in another, carry a different content rating, sit in a different chart, or simply not be distributed there at all.

`country` picks the storefront (`us`, `gb`, `de`, `in`, `br`, …) and decides price, currency, install band, content rating and availability. `language` picks the language of the text — title, summary, description, changelog — and does not change any of the numbers.

A single run describes a single storefront. If you need five countries, run it five times with the same `appIds` and compare on the `country` column.

### Limitations

- A search term returns roughly the first 20-30 apps Play ranks for it, which is what the store's own search results page shows. For deeper coverage, use more specific terms or add a category.
- A category returns the apps Play features on that category page — its top free, top grossing, top paid and trending shelves, usually 40-80 apps — not the entire category, which Play does not publish.
- `version` and `androidVersion` are null for apps Google delivers per device rather than as a single build. The store itself shows "Varies with device" for those; there is no version string behind it to report.
- Ratings, review counts and install bands are a snapshot at read time and keep moving; two runs minutes apart will differ in the last digits.
- Install counts are bands, not exact figures — that is all Play publishes. `realInstalls` is the finest number available and is still rounded by Google.
- Prices are the storefront price before tax, in that storefront's currency. Regional promotions and personalised offers are not visible to a logged-out reader.
- Editorial fields Play only shows to some readers — pre-registration state, early-access badges, A/B-tested store listings — are not returned.
- App reviews are not included. This Actor reads the listing; review text is a different job.
- The hard ceiling is 2,000 rows per run, 20 search terms and 500 package ids. For more than that, split the work across runs.

### Questions

**What do I put in "appIds" — the package id or the link?**

Either, and you can mix them in the same list. `com.whatsapp` and `https://play.google.com/store/apps/details?id=com.whatsapp&hl=de` are both understood; the id is read out of the link.

**What happens if a package id does not exist?**

You get one uncharged diagnostic row for it with `errorCode: "NOT_FOUND"`, and the run carries on with the rest. You are never billed for an app that could not be read. The same happens when the app exists but is not distributed in the country you asked for.

**Why is "version" empty for some apps?**

Because Google does not publish one for them. Large apps are often delivered as per-device builds, and the store shows "Varies with device" instead of a version. Returning null is the honest answer; inventing a number would not be.

**Can I get reviews too?**

Not from this Actor. It reads the app listing — metadata, ratings summary, media, developer details. Review text is a separate job with a very different row shape.

**Do I need a proxy?**

No. The run brings its own egress and the cost of it is already inside the price you see. The `proxyUrls` field exists only for callers who specifically want traffic to leave through servers they already own.

**Will the run fail if something goes wrong?**

No. A blocked, empty or broken target produces an uncharged diagnostic row explaining what happened and the run still finishes as succeeded. A failed run would still bill you the start fee, which would mean paying to be told something went wrong.

**Can I run this on a schedule?**

Yes. Nothing in the run holds state between runs, so a schedule with the same input is safe. Key on `appId` and diff on `version`, `updated`, `score` and `ratings` to see what moved since last time.

**How do I get exactly the rows I paid for?**

Filter the dataset on `"charged": true`. Sample and diagnostic rows are always `false`, and the number of charged rows always equals the number of billed events.

# Actor input Schema

## `searchTerms` (type: `array`):

Words or phrases to search the Google Play Store for, exactly as you would type them into the store search box. Up to 20 per run. Each term contributes its share of the row limit below.

## `appIds` (type: `array`):

Android package ids such as com.whatsapp, or full Play links such as https://play.google.com/store/apps/details?id=com.whatsapp. You can mix both forms in the same list. These are fetched before anything else, one row each.

## `category` (type: `string`):

Optional. Returns the apps Google Play features on that category page - its top free, top grossing, top paid and trending shelves - in Play's own order. Leave empty if you are searching or passing app ids.

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

Two-letter country code for the storefront to read, for example us, gb, de, in, br. Google Play is a store per country: price, currency, install band, content rating and even whether the app exists depend on this.

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

Two-letter language code for the text, for example en, de, es, pt, fr. Controls the title, summary, description and changelog. It does not change any of the numbers.

## `maxItems` (type: `integer`):

Total number of apps to return. App ids are taken first, then whatever budget is left is split evenly between the search terms and the category. Keep it low while you are testing - you pay per row.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## Actor input object example

```json
{
  "searchTerms": [
    "budget tracker",
    "habit tracker"
  ],
  "country": "us",
  "language": "en",
  "maxItems": 20
}
```

# Actor output Schema

## `results` (type: `string`):

Every row in the default dataset: query, appId, title, developer, score, ratings, installs, priceText, offersIAP, contentRating, updated, version, url, icon, searchType, summary, description, descriptionHtml, genre, genreId, scoreText, reviews, histogram, minInstalls, realInstalls, price, currency, free, inAppPurchaseRange, containsAds, adsLabel, contentRatingDescription, released, releasedAt, updatedText, recentChanges, androidVersion, androidMinSdk, developerId, developerUrl, developerLegalName, developerEmail, developerWebsite, developerAddress, privacyPolicy, headerImage, screenshots, video, topChartRank, available, country, language, scrapedAt. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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 = {
    "searchTerms": [
        "budget tracker",
        "habit tracker"
    ],
    "country": "us",
    "language": "en",
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/google-play-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 = {
    "searchTerms": [
        "budget tracker",
        "habit tracker",
    ],
    "country": "us",
    "language": "en",
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/google-play-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 '{
  "searchTerms": [
    "budget tracker",
    "habit tracker"
  ],
  "country": "us",
  "language": "en",
  "maxItems": 20
}' |
apify call dami_studio/google-play-apps-scraper --silent --output-dataset

```

## MCP server setup

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