# Google Play Scraper: Reviews, Apps, Charts, ASO (`snow_leo_data/google-play-store-scraper-apps-reviews-charts`) Actor

Google Play keeps reviews in separate buckets per language: 18 languages on one app gave 10800 different reviews and 0 duplicates. One-language scrapers see one bucket. Reviews, app details, search, top charts, developer listings and similar apps in one Actor - 81 columns.

- **URL**: https://apify.com/snow\_leo\_data/google-play-store-scraper-apps-reviews-charts.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** E-commerce, SEO tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.05 / 1,000 result rows

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?

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 Scraper: Reviews, Apps, Charts, ASO

Google Play does not keep one pile of reviews per app. It keeps one pile **per
language the review was written in**, and the piles do not overlap. That single
fact is the reason this Actor exists.

Measured on `com.spotify.music` on 13 September 2026: 18 languages, 600 reviews
pulled from each, **10800 different review IDs and 0 duplicates between
languages**. A scraper with one language field in its input sees one pile out
of many, reports success, and the buyer never learns what was missing.

This Actor takes a list of languages, a list of apps and a list of device
types, and walks all of them in one run. It also does app details, keyword
search, top charts, developer listings and similar apps — 6 modes, one Actor,
one subscription.

#### What it returns

Every row is a flat record with a `kind` field: `review` or `app`. Both kinds
live in the same dataset because a review score is meaningless without the
app's own rating next to it, and splitting them across two datasets would only
push the join onto you. 81 columns are declared; a live app row filled 50 of
them and a live review row filled 20. Turn on **Compact output** and empty
columns disappear from the rows entirely.

Review rows carry the review ID and its permanent URL, the author name, the
author's Google user ID, the avatar, the 1-5 score, the text, the posting time
as both ISO-8601 and a Unix timestamp, the thumbs-up count, the app version the
reviewer was running, **the developer's reply with its own author and date**,
and **Google's own structured quality tags** — machine labels such as
`vaf_app_quality_stability`, `vaf_app_quality_ads_frequency` and
`vaf_app_quality_battery_efficiency` that the store attaches to reviews and
that no other Actor in this niche publishes.

App rows carry the title, the full description in both HTML and plain text, the
short summary, the developer with their store page, e-mail, website and postal
address, the privacy policy link, the genre and every category, the score and
the **complete 1-to-5 rating histogram**, the ratings count and the reviews
count separately, the install band, its lower bound and the exact install
number Google publishes internally, price in both micros and currency units,
the in-app purchase range, the "contains ads" flag, the content rating, the
release and update dates, the version, the minimum Android version, the
**target SDK level**, the "what's new" text, the icon, header image, video and
screenshots, the **Data safety** section, the Android permissions on request,
the **average score per device class**, and the IDs of similar apps.

#### Why one language is not enough

The buckets are the whole story, so here are the numbers behind it.

| what was measured | result |
|---|---|
| languages swept on `com.spotify.music` | 18 |
| reviews taken per language | 600 |
| unique review IDs collected | 10800 |
| duplicates between languages | 0 |
| the same run limited to English only | 600 |

Device type splits the reviews the same way. Phone and tablet on the same app
returned 200 reviews each with 0 in common, so adding a device type adds
reviews instead of repeating them.

The country storefront does **not** split reviews, and this matters because it
is the field people assume does. English/United States and English/Germany
returned the same 200 reviews — an overlap of 200 out of 200. So for reviews,
pick languages, not countries. For charts, prices and availability it is the
other way round: the overall chart of 8 countries returned 351 different apps
against 84 from one country alone, and the same search keyword across those 8
storefronts returned 55 different apps against 30 from one.

#### Speed and the ceiling of one request

The reviews endpoint accepts a page size, and the ceiling was found by halving
the interval: **4500 reviews come back in one call, 4501 returns HTTP 200 with
an empty body**. No error, no message — just nothing. Anything larger is
clamped to the ceiling before the request leaves, because a silent empty answer
is the worst kind of failure.

At 4500 per call the measured rate was 2.32 seconds per request. The default is
1000 reviews in one call, which keeps memory small and is still ten times the
100-per-page default of the largest competing Actor.

#### Modes

- **reviews** — reviews of the apps you list, across every language and device
  type you pick. With app details on, one app row is written per app first.
- **apps** — full app pages for the apps you list, one row per country
  storefront.
- **search** — Google Play's own search for your keywords. One search page
  returns 30 apps.
- **charts** — the top chart of a country, optionally narrowed to one of 51
  categories.
- **developer** — every app published by a developer. Both the developer name
  as shown on the store and the numeric developer ID are accepted, and they
  live on different URLs, so the Actor picks the right one for you.
- **similar** — the apps Google shows as similar. These arrive with the app
  page itself, so this mode costs no extra request.

50 languages and 53 country storefronts are offered as pick-lists; anything
else you type is passed through as-is.

#### Monitoring: pay only for what moved

The niche runs at 90 runs per user, which means scheduled monitoring, not
one-off exports. Turn on **Only new and changed rows** and the Actor keeps a
record in a named key-value store of everything it has already delivered. Every
row then arrives labelled `NEW`, `UPDATED` or `UNCHANGED`, and an updated row
carries a `changed_fields` list with the old and the new value of each field
that moved.

The fingerprint is taken from the fields worth watching, not from the whole
row. For a review that is the score, the text, the thumbs-up count, the
developer reply and the app version — so a developer answering an old review
shows up as `UPDATED` with `reply_text` named, while the row merely being
scraped again does not. For an app it is the score, the ratings and reviews
counts, the installs, the version, the price, the ads flag, the title, the
update date and the chart rank.

A run that finds nothing new writes nothing, charges nothing, and says so by
name rather than finishing silently empty.

#### Empty results always have a name

A green run with an empty dataset is the worst outcome for a buyer: money spent
on the start, nothing to show, no explanation. This Actor names the reason in
the `empty_reason` field of the `REPORT` record and repeats it in the log as a
warning. There are 18 named reasons, and 12 of them are decided from your input
alone, before a single byte is downloaded:

`no-target-given`, `all-app-ids-invalid`, `no-languages-selected`,
`no-device-types-selected`, `star-filter-excludes-every-star`,
`date-not-understood`, `app-filter-without-app-details`,
`date-window-is-empty`, `score-range-is-empty`,
`free-only-and-paid-only-at-once`, `category-not-recognised` and
`charge-limit-reached`.

The rest can only be known after the crawl: `filtered-out`, `nothing-changed`,
`source-returned-nothing`, `all-apps-unreachable`, `app-not-found` and
`source-rate-limited`.

The pattern behind the first group is always the same — one field narrows the
result while another switches off the data the first one needs. Asking to keep
only apps above a million installs while app details are off would drop every
row, because the install count lives on the app page and nowhere else. That is
caught and explained instead of being crawled and thrown away.

#### Filters run before you are charged

Every filter runs on our side, and a row a filter drops is never written to the
dataset, so it is never billed. The `REPORT` record names which filter dropped
how many rows.

One honest detail about how filtering works here. **Google Play has no
server-side star filter.** Six different request shapes were tried — the value
in five different positions of the request and two at the top level — and none
was honoured: the same mix of scores came back every time. So the star filter,
the keyword filter and the date window all run after the download. The crawler
compensates by walking with headroom and stopping on the number of **delivered**
rows rather than crawled ones. Ask for 30 one-star reviews and you get 30, not
an empty dataset with a note that 30 reviews were collected and all of them
dropped.

The other rule in the filters is that **a missing value passes**. An app whose
install count Google does not publish is not dropped by a minimum-installs
filter; absence of data is not a mismatch. Losing half a result set to one
unfilled key at the source, with a green run to show for it, is not something
you would find out about until much later.

#### How it works underneath

No proxy, no browser, no API key. Two paths on `play.google.com` are used: the
store pages, which carry their data inside `AF_initDataCallback` blocks, and
the internal `batchexecute` endpoint, which serves reviews and permissions.

Both were verified **from Apify's own cloud before a single line of this Actor
was written**, using a probe Actor: twelve store URLs across five country
storefronts and six RPC calls, all HTTP 200 with real data. That check is not
ceremony. A neighbouring source in this portfolio answered 200 from a laptop and
403 from every Apify address, and the Actor built on it had to be thrown away
whole.

The parser finds app cards by their **shape**, not by their position in the
page. A card is a list whose first element is a package-name-and-device pair
and whose fourth element is a title. The same cards sit at different paths in
search results, charts, developer pages and the similar-apps block, so a
hard-coded path would break on the first layout change.

By default 4 app pages are fetched at once and the timeout per request is 30
seconds. The defaults ask for 200 reviews per app, 1000 reviews in one call and
120 apps per query.

#### Honest limitations

These are the places where a competing Actor does something we do not, or where
the source itself stops us. None of them is hidden in a footnote elsewhere.

- **No star filter at the source.** As described above, six request shapes were
  tried and none worked. Filtering happens after the download. This costs
  requests, not correctness.
- **Search and charts are not deeply paginated.** One search page returns 30
  apps and a chart page returns roughly a hundred; the continuation token that
  the store's own front end uses was tried in three shapes and answered with an
  error each time. Depth is bought with more storefronts and more keywords
  instead — 8 countries turned 30 search results into 55 and 84 chart entries
  into 351.
- **Per-device scores come as numeric device classes, not names.** Google
  publishes the averages with codes and does not label them anywhere on the
  page in a way that survives checking across apps, so the code is published as
  a code. Inventing names for them would be worse than leaving them numeric.
- **`version` and `android_version` are often empty.** That is the source
  telling the truth: large apps ship per-device builds and the store shows
  "Varies with device". The field is left null rather than filled with a guess.
- **Reviews cannot be filtered by country.** This is a property of Google Play,
  measured above: the storefront does not change which reviews you get.
- **No sentiment, no topic model, no scoring.** Everything in a row comes from
  Google Play. Nothing is inferred, generated or estimated.
- **A free Apify account has a compute quota.** Long multi-language sweeps of
  very large apps are best run on a paid plan or split across scheduled runs
  with monitoring turned on.

#### How we compare

The niche has 97 Actors and 11172 users between them, at 90 runs per user. The
largest of them, `neatrat/google-play-store-reviews-scraper` with 2605 users,
does reviews only: 1 app per run, 1 language storefront per run, 13 published
fields, 100 reviews per page by default, no app details, no charts, no search,
no developer listing, no monitor mode and no named reason when a run comes back
empty.

Against that, this Actor takes a list of apps, a list of languages and a list of
device types in one run; publishes 20 filled fields on a live review row
including the developer reply and Google's quality tags; asks for up to 4500
reviews in a single call; and covers 6 modes where the competition sells them
as separate Actors.

A full line-by-line comparison, including every place where a rival is ahead of
us, is in `COMPARISON.md` in the source repository.

#### FAQ

**Which languages should I pick?** The ones your users write in. Reviews are
bucketed by the language of the text, so picking German gets you German-language
reviews regardless of where the reviewer lives. If you want everything, pick
everything: the buckets do not overlap, so there is no duplicate cost.

**Do I need a proxy?** No. Both paths answer from Apify's cloud without one,
and that was checked from the cloud before the Actor was built.

**Why do I get an app row when I asked for reviews?** Because "Fetch full app
details" is on by default and it writes one app row per app before the reviews,
so the review scores have the app's own rating, histogram and install count to
be read against. Turn it off and you get reviews only.

**How do I get every review of an app?** Set **Max reviews per app** to 0, pick
your languages, and let it run. Google Play gives no total for a language
bucket in advance, so the Actor walks until the cursor stops coming back.

**What does the rank field mean?** In search, charts and developer modes it is
the position the app held in that result list, starting at 1. It is part of the
monitored fields, so in monitor mode a rank change arrives as an `UPDATED` row
with the old and new position.

**Can I chain this with another Actor?** Yes. Put another run's dataset ID into
**Read app IDs from another dataset** and name the field that holds the package
name or the store URL.

**What happens if a package does not exist?** The store answers HTTP 404 and the
package is listed in the `REPORT` record under `not_found`. If every package you
gave is missing, the run ends with `app-not-found` rather than an unexplained
empty dataset.

**Is the data real-time?** It is whatever the store serves at the moment of the
request. Review counts and scores on the app page can lag the review list by a
few minutes; that is the source, not the Actor.

# Actor input Schema

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

Reviews is the default. Every mode runs from the same Actor, so you do not need a second subscription for app details or charts.

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

One per line, for example com.spotify.music or https://play.google.com/store/apps/details?id=com.spotify.music. Unlike a one-app-per-run scraper, the whole list is done in a single run.

## `languages` (type: `array`):

Google Play stores reviews in separate buckets by the language they were written in, and the buckets do not overlap: 18 languages on com.spotify.music gave 10800 different review IDs and 0 duplicates between them. Pick every language your users write in - one language means one bucket.

## `deviceTypes` (type: `array`):

Reviews are split by device as well. Phone and tablet on the same app returned 200 reviews each with 0 in common, so adding a device type adds reviews instead of repeating them.

## `reviewsSort` (type: `string`):

Newest first is the right choice for monitoring. Most relevant and Highest rating are what the store shows by default.

## `maxReviewsPerApp` (type: `integer`):

0 means every review Google Play will give. The limit applies per app per language per device, so 200 with three languages is up to 600 reviews for that app.

## `reviewsPerRequest` (type: `integer`):

How many reviews to ask for in one call. The measured ceiling is 4500 - asking for 4501 returns HTTP 200 and an empty body, so anything larger is clamped. 4500 in one call took 2.32 seconds.

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

Used in Search mode. One search page returns 30 apps; running the same keyword across 8 country storefronts returned 55 different apps.

## `developers` (type: `array`):

Used in Developer mode. Either the developer name as shown on the store (Spotify AB) or the numeric developer ID.

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

Used in Top charts mode. Leave empty for the overall chart of each country.

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

Charts, prices and availability differ by country: the overall chart of 8 countries returned 335 different apps against 69 from one. Reviews are NOT split by country - en/us and en/de returned the same 200 reviews - so for reviews one country is enough.

## `maxAppsPerQuery` (type: `integer`):

How deep to go into one search, chart or developer page. A quota per query stops one large chart from eating the whole row limit.

## `includeAppDetails` (type: `boolean`):

Adds one request per app and fills the rating histogram, install counts, data safety, per-device scores, developer contacts and the changelog. In Reviews mode it also adds one app row per app so the reviews have something to be compared against.

## `includePermissions` (type: `boolean`):

Adds one more request per app and fills the permissions field, grouped the way the store groups them. Off by default because most buyers do not need it.

## `stars` (type: `array`):

Empty means all five.

## `reviewTextContains` (type: `string`):

Case-insensitive. A review with no text is dropped by this filter, because there is nothing to match.

## `appVersions` (type: `array`):

For example 9.1.80.2221. A review whose version Google did not record is kept, not dropped.

## `withReplyOnly` (type: `boolean`):

The developer reply, its author and its date are published as their own fields.

## `newerThanDays` (type: `integer`):

0 turns this off.

## `postedBefore` (type: `string`):

YYYY-MM-DD. Combined with the previous field this makes a window; an empty window is reported before anything is downloaded.

## `minScore` (type: `integer`):

Apps only. An app whose score Google does not publish is kept, not dropped: missing data is not a mismatch.

## `maxScore` (type: `integer`):

Apps only. 0 turns this off.

## `minInstalls` (type: `integer`):

Apps only. Uses the lower bound Google publishes, for example 1000000 for a 1,000,000+ app.

## `minRatings` (type: `integer`):

Apps only. Ratings, not reviews - Google publishes both and they are far apart.

## `freeOnly` (type: `boolean`):

Apps only.

## `paidOnly` (type: `boolean`):

Apps only. Asking for both free-only and paid-only is reported before anything is downloaded.

## `withoutAdsOnly` (type: `boolean`):

Apps only. Uses the store's own 'Contains ads' badge.

## `titleContains` (type: `string`):

Case-insensitive, matched against title, summary, developer and package name.

## `onlyNew` (type: `boolean`):

Every row is labelled NEW, UPDATED or UNCHANGED, and an updated row carries the list of fields with their old and new values. A run that finds nothing new writes nothing and says so by name.

## `emitUnchanged` (type: `boolean`):

Keeps the change labels but writes everything, including rows that did not move.

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

0 means no limit. The limit counts DELIVERED rows, not crawled ones, so a narrow filter still returns the number you asked for.

## `compact` (type: `boolean`):

Drops empty fields from every row. Useful when the rows go straight into an LLM or a spreadsheet.

## `concurrency` (type: `integer`):

4 app pages are fetched at once by default.

## `timeoutSeconds` (type: `integer`):

By default the timeout per request is 30 seconds.

## `inputDatasetId` (type: `string`):

Dataset ID of another Actor run. Its rows are read and the chosen field is added to the app list.

## `appIdField` (type: `string`):

Which field holds the package name or the store address.

## Actor input object example

```json
{
  "mode": "reviews",
  "appIds": [
    "com.spotify.music"
  ],
  "languages": [
    "en"
  ],
  "deviceTypes": [
    "phone"
  ],
  "reviewsSort": "newest",
  "maxReviewsPerApp": 200,
  "reviewsPerRequest": 1000,
  "searchTerms": [
    "habit tracker"
  ],
  "developers": [],
  "categories": [],
  "countries": [
    "us"
  ],
  "maxAppsPerQuery": 120,
  "includeAppDetails": true,
  "includePermissions": false,
  "stars": [],
  "reviewTextContains": "",
  "appVersions": [],
  "withReplyOnly": false,
  "newerThanDays": 0,
  "postedBefore": "",
  "minScore": 0,
  "maxScore": 0,
  "minInstalls": 0,
  "minRatings": 0,
  "freeOnly": false,
  "paidOnly": false,
  "withoutAdsOnly": false,
  "titleContains": "",
  "onlyNew": false,
  "emitUnchanged": false,
  "maxItems": 100,
  "compact": false,
  "concurrency": 4,
  "timeoutSeconds": 30,
  "inputDatasetId": "",
  "appIdField": "app_id"
}
```

# Actor output Schema

## `rows` (type: `string`):

No description

## `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 = {
    "mode": "reviews",
    "appIds": [
        "com.spotify.music"
    ],
    "languages": [
        "en"
    ],
    "deviceTypes": [
        "phone"
    ],
    "reviewsSort": "newest",
    "maxReviewsPerApp": 200,
    "reviewsPerRequest": 1000,
    "searchTerms": [
        "habit tracker"
    ],
    "developers": [],
    "categories": [],
    "countries": [
        "us"
    ],
    "maxAppsPerQuery": 120,
    "stars": [],
    "appVersions": [],
    "newerThanDays": 0,
    "minScore": 0,
    "maxScore": 0,
    "minInstalls": 0,
    "minRatings": 0,
    "maxItems": 100,
    "concurrency": 4,
    "timeoutSeconds": 30
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/google-play-store-scraper-apps-reviews-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 = {
    "mode": "reviews",
    "appIds": ["com.spotify.music"],
    "languages": ["en"],
    "deviceTypes": ["phone"],
    "reviewsSort": "newest",
    "maxReviewsPerApp": 200,
    "reviewsPerRequest": 1000,
    "searchTerms": ["habit tracker"],
    "developers": [],
    "categories": [],
    "countries": ["us"],
    "maxAppsPerQuery": 120,
    "stars": [],
    "appVersions": [],
    "newerThanDays": 0,
    "minScore": 0,
    "maxScore": 0,
    "minInstalls": 0,
    "minRatings": 0,
    "maxItems": 100,
    "concurrency": 4,
    "timeoutSeconds": 30,
}

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/google-play-store-scraper-apps-reviews-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 '{
  "mode": "reviews",
  "appIds": [
    "com.spotify.music"
  ],
  "languages": [
    "en"
  ],
  "deviceTypes": [
    "phone"
  ],
  "reviewsSort": "newest",
  "maxReviewsPerApp": 200,
  "reviewsPerRequest": 1000,
  "searchTerms": [
    "habit tracker"
  ],
  "developers": [],
  "categories": [],
  "countries": [
    "us"
  ],
  "maxAppsPerQuery": 120,
  "stars": [],
  "appVersions": [],
  "newerThanDays": 0,
  "minScore": 0,
  "maxScore": 0,
  "minInstalls": 0,
  "minRatings": 0,
  "maxItems": 100,
  "concurrency": 4,
  "timeoutSeconds": 30
}' |
apify call snow_leo_data/google-play-store-scraper-apps-reviews-charts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/google-play-store-scraper-apps-reviews-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/8dIKaA9Ecoptkbytq/builds/sZaDhmzdp2EUKlJtZ/openapi.json
