# Google Play App Metadata — Prices, Ratings & Categories (`kaz_kakyo/google-play-metadata`) Actor

Get Google Play app snapshots for US, Singapore and Germany in English: titles, upfront prices, currencies, Google-reported ratings, rating counts and category codes. Up to 100 app-country pairs. No review text or contacts; unsuccessful records have no result fee.

- **URL**: https://apify.com/kaz\_kakyo/google-play-metadata.md
- **Developed by:** [Heim AI](https://apify.com/kaz_kakyo) (community)
- **Categories:** Developer tools, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 app-country snapshot returneds

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

## Google Play App Metadata — Prices, Ratings & Categories

Give this Actor a list of known **Android package IDs**. For each app and requested country (**US, Singapore or Germany, in English**) it returns one snapshot from the public Google Play web listing:

- the app title;
- the **current upfront price as decimal text**, with its ISO currency;
- the purchase-control text actually shown on the page (for example `Install` or `$3.49`);
- the **rating, rating count and category code that Google declares** in the listing's structured data;
- the rating and abbreviated count text shown in the listing header, where present.

Use it to compare regional upfront prices of paid apps, or to keep a small, repeatable catalogue snapshot. It does not search Google Play or discover apps. It does not collect review texts, reviewer or developer details, descriptions, screenshots, versions, install counts or permissions.

### Quick start

```json
{"appIds":["net.dinglisch.android.taskerm","com.mojang.minecraftpe"],"countries":["us","sg","de"],"maxResults":6}
```

This asks for Tasker and Minecraft in three countries: 6 app-country pairs, so at most 6 paid records.

- `appIds`: package IDs such as `org.videolan.vlc`, or exact `https://play.google.com/store/apps/details?id=…` URLs. Capitalisation is kept, so `com.Slack` stays `com.Slack`. Duplicates are removed, and invalid entries are reported for free next to the valid ones.
- **URLs are never fetched as given.** Only their `id` parameter is read. Any `gl`, `hl` or other parameter in a pasted URL is **ignored**; the `countries` field decides which listing is requested, always with `hl=en`.
- `countries`: any of `us`, `sg`, `de` (default `us`).
- `maxResults`: 1–100 paid records (default 100).

A run accepts **at most 100 unique app-country pairs**. A larger input is rejected before any request is made; it is not silently truncated.

### Price

**$0.001 per returned app-country snapshot ($1 per 1,000), plus the platform's $0.00005 Actor-start event.** Six returned pairs cost $0.00605. Current prices are shown on the Actor's pricing tab.

The **result fee** applies only to validated, returned snapshots. Invalid entries, listings Google did not return, missing offers, mismatched or unsupported data, source errors and pairs not attempted have no result fee; the platform start fee still applies. Set `maxResults` and a maximum charge per run. Once the next result can no longer be paid for, the Actor makes no further requests. Every new run is a new snapshot and is charged again; there is no change-only monitor.

For **upfront-price comparisons of free apps**, extra countries can return the same zero price. Use one country when you do not need regional listing observations. Each additional requested country is separately billed; do not assume the reported metrics describe distinct regional populations.

### Output

The results dataset holds one flat row per validated app-country pair:

| Field | Meaning |
| --- | --- |
| `recordId` | Stable key `play/<country>/en/<packageId>` |
| `appId` | Package ID, capitalisation preserved |
| `title` | Listing title. The structured-data name must equal the visible title heading. |
| `requestedCountry`, `requestedLanguage` | The `gl` (`us`/`sg`/`de`) and `hl` (`en`) that were requested |
| `price` | Current upfront price returned as decimal **text** (e.g. `"0"`, `"4.99"`) and compared as decimal text. Numeric source values use standard JSON parsing; no floating-point price arithmetic is performed. |
| `currency` | ISO 4217 code declared with the offer |
| `observedOfferText` | The visible purchase control the price was checked against |
| `priceScope` | Plain-language scope of the price (see below) |
| `sourceRatingValue` | `aggregateRating.ratingValue` as Google declared it, e.g. `4.044929504394531`; `null` if not declared |
| `sourceRatingCount` | `aggregateRating.ratingCount` as Google declared it, e.g. `2004769`; `null` if not declared, `0` only if declared as zero |
| `sourceCategoryCode` | `applicationCategory` code, e.g. `VIDEO_PLAYERS`; `null` if not declared |
| `headerRatingText`, `headerRatingStatus` | Rating the listing header displayed (e.g. `"4.0"`), and how it relates to the source value (see below) |
| `headerReviewCountText`, `headerReviewCountStatus` | Count text the header displayed, kept as shown (e.g. `"2M reviews"`), and its relation to the source count |
| `metricsScope` | Plain-language provenance and limits of the rating, count and category fields |
| `sourceUrl`, `source`, `fetchedAt` | Exact listing URL requested, source attribution, and fetch time |

A row is delivered only when every check below passes:

- The listing's structured data names exactly this package ID.
- Its title equals the page's primary title heading.
- There is exactly one offer, with a valid decimal price and ISO currency.
- The visible purchase control shows the same current price.
  - English grouping such as `$1,299.99` is read as 1299.99.
  - Decimal commas (`7,49 €`, `1.299,99 €`) are read only with a euro marker.
  - Invalid or ambiguous grouping (`$1,29.99`, `€1,299`), numbers without a currency marker, and several different prices all fail the check.
  - A price inside semantic strike-through markup (`<s>`, `<del>`) counts as an original price and is ignored. Sale layouts without such markup are not guessed at.
- A paid app is priced in the local currency: USD for `us`, SGD for `sg`, EUR for `de`.
- Any declared rating, rating count or category is well formed. A malformed value makes the record unchargeable; it is never silently dropped.
- Where the header shows a rating or count next to a declared source value, the two are compatible (see below).

**What the price means, and what it doesn't:**

- It is the upfront price shown on the public listing that was requested. It is not a checkout price.
- `"0"` says nothing about ads, subscriptions or in-app purchases.
- A returned listing for a country does not prove the app can be installed or bought there.
- For zero-price apps, the currency is reported as declared and may not be the local currency.

### Interpreting source metrics

The source rating count is not a count of written reviews. This response does not establish whether the metrics are global, country-specific or device-specific. Do not add counts across requested countries. Values may be cached and change over time. Category codes come from structured data; translated category labels are not inferred from navigation links.

Recognizable header values are checked against the corresponding source values using display rounding or truncation. A plain, unabridged header count must match exactly; no invented cache-drift percentage is allowed. A contradiction produces an uncharged status. Formatting compatibility does not establish that both values describe the same population.

Header statuses:

- `consistent_with_source` / `compatible_with_source`: a recognized display value is compatible with the source value.
- `present_source_absent`: the header is readable, but the corresponding structured field is absent.
- `not_found`: no comparable header value was found.
- `unsupported`: the header could not be isolated or parsed reliably. Its text is null; well-formed structured values remain explicitly source-reported, not canonical verified ratings.

Missing source metrics are null, not invented zeroes. Malformed declared metrics make the record unchargeable. Versions, install counts, permissions and Data Safety are not collected.

### Status and run summary

The separate **free status dataset** has one row per invalid entry and one per requested pair that did not become a paid row, plus a `summary` row. The same summary is saved as `OUTPUT` in the key-value store. Check `complete` even when the run succeeded. `complete: true` means every requested entry became a paid row.

Row kinds:

- `invalid_input`: malformed ID, or a URL that is not an exact Play detail URL.
- `no_listing_returned`: HTTP 404 for that request. It does not prove the app doesn't exist worldwide.
- `no_offer`: the listing declared no offer.
- `parse_mismatch`: identity, title, visible price, currency or a recognizable metric comparison disagreed. The `mismatches` field lists which.
- `unsupported_field`: malformed or unsupported declared price, currency, rating, count or category data.
- `source_access_denied`: HTTP 403 or 429, a redirect, a consent or challenge page, or robots.txt disallows the request.
- `source_unavailable`: other HTTP errors, timeouts, the 3 MiB page limit, or robots.txt unavailable.
- `not_attempted`: the run stopped first. The `reason` field holds `budget_limit`, `result_limit`, `request_limit`, `deadline`, `interrupted`, `billing_unknown` or the source stop.

Access denial, a redirect, a consent/challenge page and HTTP 503 **stop the run politely**. Nothing further is requested and nothing further is charged. The run itself still succeeds, with an explicit reason.

### Limits and source behaviour

- Only `https://play.google.com/robots.txt` and generated `/store/apps/details?id=…&gl=…&hl=en` listing URLs are ever requested.
- robots.txt is read first, and each exact listing path is checked against it. If robots.txt is unavailable, the run fails closed.
- No login, cookies, proxy, browser, search, reviews, Data Safety or private endpoints are used.
- Redirects are never followed.
- Requests are sequential and at least 0.5 s apart.
- Each request has a 20 s timeout and a 3 MiB page limit.
- New source work has an internal 270-second window, leaving time to save results inside the default 300-second timeout. A longer platform timeout does not extend it.
- The supported memory allocation is 512 MiB (minimum, default and maximum).
- Each run has a hard budget of **102 source requests**: 100 listings, robots.txt, and one robots.txt re-check after a resurrection.
- There are no retries. A failed pair appears in the status dataset; start a new run for it.

Page structure can change. When it does, affected pairs become free status rows rather than guessed data.

**Large-batch limit:** 100 pairs is an input cap, not a promise that all100 finish within the time window. A private production test with live PPE billing requested99 pairs, returned92 and marked7 unattempted when the deadline was reached. This is an observed workload, not a guarantee for other apps or source response times. Prefer smaller batches, inspect `complete` and retry only the unreturned pairs in a new run. The returned records are new snapshots, not a change-only feed.

### Interruptions

If a run is interrupted after a charge, resurrect **the same run**. Pending paid records are then delivered without a second charge, and pairs already delivered are skipped without being fetched again. Keep run storage intact: the recovery ledger lives in the run's own storage and is not tamper-proof. Saved limits that look edited stop new work once pending paid records have been recovered. Starting a separate run is a new, separately charged snapshot.

An unresolved storage failure or crash can make an obligation's charge outcome impossible to reconstruct. Recovery then favors a possibly free delivery over withholding data that may have been paid for. A known zero-charge result is removed from the recovery ledger; failed cleanup is retried before successful completion. Persistent storage failure fails the run rather than claiming that cleanup succeeded.

### Privacy and telemetry

Pseudonymous operational telemetry records actor and run IDs, run origin, timestamps, paying-plan status, a stable hashed caller identifier, and cumulative confirmed event counts. These identifiers are linkable, not anonymous. The telemetry dataset restricts unauthenticated access. Input and result content is not sent. The Actor never stores or logs page bodies.

Source: the public Google Play website. This Actor is not affiliated with or endorsed by Google, and source access and page structure can change. Keep the `sourceUrl` links with any data you reuse.

# Actor input Schema

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

Known Android package IDs (for example org.videolan.vlc; capitalisation is kept, so com.Slack stays com.Slack) or exact https://play.google.com/store/apps/details?id=… URLs. Only the id parameter of a URL is used: gl, hl and other parameters are ignored in favour of the countries field below, and submitted URLs are never fetched. Invalid entries become free status rows. No search.

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

One or more of: us, sg, de. Each package ID is requested once per country, in English (hl=en). Maximum 100 unique app-country pairs per run; larger inputs are rejected before any request. The country only selects the requested listing locale; it does not prove the app can be installed or bought there.

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

Hard cap on paid app-country records (1–100). Status rows are free. Once the cap or your maximum charge is reached, no further requests are made.

## Actor input object example

```json
{
  "appIds": [
    "org.videolan.vlc",
    "net.dinglisch.android.taskerm"
  ],
  "countries": [
    "us"
  ],
  "maxResults": 100
}
```

# Actor output Schema

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

Only validated app-country snapshots, each with every documented field: price facts plus source-reported rating, rating count and category code (null when Google did not declare them).

## `status` (type: `string`):

One row per invalid input and per requested pair that was not returned, plus a run summary. Check it even when the run succeeds.

## `summary` (type: `string`):

complete, stop reason, per-outcome counts and request count.

# 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 = {
    "appIds": [
        "org.videolan.vlc",
        "net.dinglisch.android.taskerm"
    ],
    "countries": [
        "us"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kaz_kakyo/google-play-metadata").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 = {
    "appIds": [
        "org.videolan.vlc",
        "net.dinglisch.android.taskerm",
    ],
    "countries": ["us"],
}

# Run the Actor and wait for it to finish
run = client.actor("kaz_kakyo/google-play-metadata").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 '{
  "appIds": [
    "org.videolan.vlc",
    "net.dinglisch.android.taskerm"
  ],
  "countries": [
    "us"
  ]
}' |
apify call kaz_kakyo/google-play-metadata --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kaz_kakyo/google-play-metadata"
        }
    }
}
```

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/kQFeTl7w7f5skTlwa/builds/UvRexaRArwcokbDKy/openapi.json
