Google Play Store Scraper
Pricing
Pay per usage
Google Play Store Scraper
Fetches Google Play app metadata — ratings, installs, pricing, developer info, and version — with optional version gating.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Milad Amirzadeh
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
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.
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"}],"maxRetries429": 3,"allowApproximateVersion": false}
apps(array, required): list of apps to scrapeurl(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, default3): retry strategy for HTTP 429 responses, same semantics as the App Store actor —false/0disables retrying,trueuses 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, defaultfalse): 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):
{"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):
{"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):
{"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, ornullwhen Google has none to give.versionSource:"play-store-page"(the common, free case),"reviews-heuristic"(see below), ornull.versionApproximate:trueonly whenversionSourceis"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 nulls or wrong values, check first, in order:
src/ds_blobs.py's_KEY_REand block-parsing — theAF_initDataCallback({key: '...'syntax itself (spacing aroundkey:/data:/sideChannel:).- The
ds:5app-detail slot indices insrc/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 unexpectedNones. Re-runpython tools/refresh_fixtures.pyand re-verify indices against the fresh HTML. - Whether
ds:5is 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. - The
IMPERSONATEconstant insrc/play_api.py— keep it a recent supported Chrome target. - 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.