DEV Community

Cover image for There's No Google Play API. Here's How I Scrape the Play Store in Node.js (2026)
MrAdex77
MrAdex77

Posted on

There's No Google Play API. Here's How I Scrape the Play Store in Node.js (2026)

Ask Google for an API to read Play Store listings, search results, or reviews. The answer is the same in 2026 as it was a decade ago: there isn't one. The Google Play Developer API only manages apps you publish. Reading anyone else's public store data has no official endpoint at all.

That's a strange hole for a store this size. Google Play lists about 1.9 million apps (AppBrain, 2026) and serves 2.5 billion+ monthly users across 190+ countries. It made $49.2 billion in 2025 (Business of Apps, Google Play Statistics, 2026). Every ASO tool, market report, and research paper built on that data got it one way: scraping.

I maintain @mradex77/google-play-scraper, an open-source, strict-TypeScript Google Play scraper for Node.js. This post is the full working guide: install, app details, search, top charts, paginated reviews, and not getting rate limited. Every snippet comes from the repo's runnable examples, so you can clone and reproduce all of it.

GitHub logo MrAdex77 / google-play-scraper

Google Play scraper for Node.js with a fully typed TypeScript API. App details, search, top charts, reviews, permissions and data safety. A modern replacement for the unmaintained google-play-scraper.

google-play-scraper

npm version npm downloads CI Live contract tests types node license

Scrape Google Play app data in Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, developer pages, similar apps, user reviews, permissions and data safety information directly from the Play Store. Built for ASO research, app market analysis, competitor tracking, review monitoring and data pipelines.

This is a modern TypeScript rewrite of the popular but unmaintained google-play-scraper package. The public method names, options and constants match the original, so migrating is usually just a matter of swapping the import.

Why this library

  • Fully typed results. Every method returns a precise TypeScript type derived from a zod schema that validates the scraped data at runtime.
  • No HTTP dependency. Runs on the native fetch of Node.js 22 and newer.
  • Resilient by design. Every fragile Google Play array path lives behind a spec layer with ordered fallback paths, so a single moved index does not take a…

Key Takeaways

  • No official Google Play Store API exists for listings, search, or reviews; scraping public pages is how every app-intelligence dataset gets built.
  • @mradex77/google-play-scraper rewrites the unmaintained classic in strict TypeScript, validates everything with zod at runtime, and tests against live Google Play daily in CI.
  • npm install, one import, await app({ appId }): 55 typed fields per app.

What Makes Google Play So Hard to Scrape?

Not the fetching. The parsing. Google Play ships its data as enormous, deeply nested JavaScript arrays with zero labels, and the array positions shift a few times a year. Your scraper doesn't break because Google blocks you. It breaks because data[1][2][72][0][1] quietly became data[1][2][74][0][1].

The monetization layout raises the stakes. In 2026, roughly 97% of Play Store listings are free (42matters, Google Play Statistics and Trends, 2026). That makes reviews, install counts, and IAP flags the analytically interesting fields. Those live deepest in the unlabeled arrays. Android's ~72% global OS share (StatCounter Global Stats, 2026) means that data describes most of the world's mobile users.

So the real engineering question isn't "how do I scrape Google Play?" It's "how do I keep a Google Play scraper alive?" Everything below is designed around that question.

Why Not Just Use the Original google-play-scraper?

Because nobody's maintaining it. facundoolano/google-play-scraper defined this space and still moves about 94,000 npm downloads a week as of July 2026 (npm registry download counts, 2026). But when Google shuffles an index now, the fix has to come from a fork or a monkey-patch in your own code.

My rewrite keeps the exact public surface — same method names, same options, same constants — and rebuilds the internals:

Capability @mradex77/google-play-scraper Original google-play-scraper
Language TypeScript, strict mode JavaScript
Type definitions Generated from zod schemas Community typings
Runtime validation zod on every input and output None
Error handling Typed error classes Plain Error
Module formats ESM + CommonJS + .d.ts ESM only
Breakage detection Daily live contract tests in CI None
Maintenance Active Unmaintained

The two internals that matter most:

  1. Spec layer with fallback paths. Every fragile array index lives in a per-feature specs.ts file as a typed constant with ordered fallback candidates. A moved index degrades one field, and extraction throws a single SpecError listing every broken field plus every path it tried. Nothing silently returns garbage.
  2. Daily live contract tests. CI runs the e2e suite against real play.google.com every day and auto-opens a labeled GitHub issue on failure. The maintainer finds out before your pipeline does.

Installation

Node.js 22.12+, native fetch, no HTTP dependency:

npm install @mradex77/google-play-scraper
Enter fullscreen mode Exit fullscreen mode

Named exports or an aggregate default export (the default export is drop-in compatible with the original package):

import { app, search, reviews } from '@mradex77/google-play-scraper';
Enter fullscreen mode Exit fullscreen mode
import gplay from '@mradex77/google-play-scraper';

const details = await gplay.app({ appId: 'com.google.android.apps.translate' });
console.log(details.title, details.score);
Enter fullscreen mode Exit fullscreen mode

CommonJS also works:

const gplay = require('@mradex77/google-play-scraper').default;

gplay.search({ term: 'panda' }).then((results) => {
  console.log(results.length);
});
Enter fullscreen mode Exit fullscreen mode

No API keys, no headless browser, no Python sidecar.

How Do You Fetch App Details?

app() takes an appId and returns 55 typed fields: title, description, rating histogram, installs, price, screenshots, developer info, and the rest. The appId is sitting in every Play Store URL as the ?id= parameter:

Annotated Google Play store URL showing that the id query parameter is the appId used by the scraper

Adapted from examples/all-methods.ts:

import { app } from '@mradex77/google-play-scraper';

const details = await app({ appId: 'com.adex77.WhereAmI' });

console.log('Title:', details.title);
console.log('Developer:', details.developer);
console.log('Score:', `${details.score?.toFixed(2)} / 5 (${details.reviews} reviews)`);
console.log('Installs:', details.installs);
console.log('Price:', details.free ? 'Free' : details.priceText);
console.log('Genre:', details.genre);
console.log('Updated:', new Date(details.updated).toDateString());
Enter fullscreen mode Exit fullscreen mode

Here's the repo's example script hitting live Google Play while I wrote this post:

Terminal output of pnpm example from the google-play-scraper repository showing live app details and search results scraped from Google Play

The zod layer is what makes this pleasant in a TypeScript codebase. details autocompletes all 55 fields, and if Google ships something malformed, you get a validation error at the boundary instead of undefined surfacing three layers into your pipeline.

Different market? Every method takes lang and country:

const german = await app({
  appId: 'com.adex77.WhereAmI',
  lang: 'de',
  country: 'de',
});
Enter fullscreen mode Exit fullscreen mode

How Do You Search the Play Store Programmatically?

search() returns up to 250 results per query. Roughly 2,400 new apps land on Google Play every day in 2026 (Business of Apps, 2026), so keyword rankings rot fast. Automating this is the only way to keep up.

import { search, suggest } from '@mradex77/google-play-scraper';

const results = await search({ term: 'where am i geography quiz', num: 5 });

results.forEach((item, index) => {
  console.log(`#${index + 1}: ${item.title} (${item.appId})`);
});
Enter fullscreen mode Exit fullscreen mode

Results carry title, appId, icon, developer, price, and score; fullDetail: true upgrades each to the full 55-field record. There's also suggest(), which exposes Play's own autocomplete. That's free ASO keyword research:

const suggestions = await suggest({ term: 'where am' });
console.log(suggestions.join(', '));
Enter fullscreen mode Exit fullscreen mode

How Do You Pull Top Charts and Competitors?

list() fetches the ranked collections (top free, top paid, top grossing) per category:

import { list, collection, category } from '@mradex77/google-play-scraper';

const topGames = await list({
  collection: collection.TOP_FREE,
  category: category.GAME,
  num: 5,
});

topGames.forEach((item, index) => {
  console.log(`#${index + 1}: ${item.title}`);
});
Enter fullscreen mode Exit fullscreen mode

Constants are exported, frozen, and typed (collection.TOP_FREE, category.GAME_PUZZLE, category.FAMILY, …), so the editor autocompletes what other libraries make you copy from a README.

developer() returns a publisher's entire portfolio. similar() returns the "similar apps" rail. That's effectively Google naming your competitor set for you:

import { developer, similar } from '@mradex77/google-play-scraper';

const portfolio = await developer({ devId: '5700313618786177705' });
const competitors = await similar({ appId: 'com.adex77.WhereAmI' });
Enter fullscreen mode Exit fullscreen mode

How Do You Scrape Reviews (All of Them)?

Reviews hide behind Google's internal batchexecute endpoint with token-based pagination — the single most annoying thing to reverse-engineer by hand. reviews() wraps it, and every response is a { data, nextPaginationToken } envelope:

import { reviews, sort } from '@mradex77/google-play-scraper';

const firstPage = await reviews({
  appId: 'com.adex77.WhereAmI',
  sort: sort.NEWEST,
  num: 5,
});

firstPage.data.forEach((review) => {
  console.log(`${review.score}★ by ${review.userName}: ${review.text}`);
});

if (firstPage.nextPaginationToken) {
  const nextPage = await reviews({
    appId: 'com.adex77.WhereAmI',
    paginate: true,
    nextPaginationToken: firstPage.nextPaginationToken,
  });
}
Enter fullscreen mode Exit fullscreen mode

Loop until the token comes back undefined and you've walked every review: score, text, ISO 8601 date, thumbs-up count, app version, developer reply.

Bonus pair almost no scraper exposes — permissions() and datasafety():

import { permissions, datasafety } from '@mradex77/google-play-scraper';

const perms = await permissions({ appId: 'com.adex77.WhereAmI' });
const safety = await datasafety({ appId: 'com.adex77.WhereAmI' });

console.log('Permissions:', perms.length);
console.log('Collected data types:', safety.collectedData.length);
console.log('Privacy policy:', safety.privacyPolicyUrl);
Enter fullscreen mode Exit fullscreen mode

That's the full permission list plus the parsed Data Safety panel: collected data, shared data, declared security practices. Useful far beyond ASO — privacy research runs on exactly these fields.

How Do You Stay Under the Rate Limits?

Three levers, all built in. Use all three for anything bigger than a handful of requests.

throttle caps requests per second; exponential backoff with Retry-After support is automatic:

import { app } from '@mradex77/google-play-scraper';

const details = await app({
  appId: 'com.adex77.WhereAmI',
  throttle: 5,
  requestOptions: { timeoutMs: 15000, retries: 3 },
});
Enter fullscreen mode Exit fullscreen mode

memoized() wraps every method in an in-memory LRU cache, so repeated calls inside the TTL never touch Google. In the repo's example run the second call returns in a fraction of a millisecond:

import { memoized } from '@mradex77/google-play-scraper';

const client = memoized({ maxAgeMs: 60000, max: 500 });

const first = await client.app({ appId: 'com.adex77.WhereAmI' });
const second = await client.app({ appId: 'com.adex77.WhereAmI' });
Enter fullscreen mode Exit fullscreen mode

Typed errors let your pipeline branch on the cause instead of regex-matching messages:

import { app, NotFoundError, RateLimitError, SpecError } from '@mradex77/google-play-scraper';

try {
  const details = await app({ appId: 'com.does.not.exist' });
} catch (error) {
  if (error instanceof NotFoundError) {
    console.error('No such app');
  } else if (error instanceof RateLimitError) {
    console.error('Backing off — Google is rate limiting');
  } else if (error instanceof SpecError) {
    console.error('Google changed its layout:', error.failures);
  } else {
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

SpecError.failures is the underrated one. It names every field that stopped resolving and every path that was tried — a complete, machine-generated repair ticket for the next layout change.

Is It Legal?

The library reads only what any browser can: public pages. Scraping still lives in a gray zone, so the Google Play Terms of Service and your local laws are on you, particularly for the personal data inside reviews. My working rules:

  • Throttle everything; spread big jobs over time
  • Cache hard; never re-fetch what you already hold
  • Analyze, don't republish Google's content wholesale
  • Treat reviewer names and avatars as personal data under GDPR-style regimes

Not affiliated with or endorsed by Google. Scrape responsibly.

FAQ

Is there any official API for Google Play Store data?

No. Google's APIs only cover apps you publish yourself. For reading public listings, search results, top charts, or reviews of arbitrary apps, no official API exists in 2026. That's why open-source scrapers parsing play.google.com are the standard approach.

How do I scrape Google Play reviews in Node.js?

Install @mradex77/google-play-scraper, then call reviews() with the target appId and paginate: true. Keep feeding the returned nextPaginationToken back until it's undefined. Add throttle so apps with millions of reviews don't trip Google's rate limits.

What data can a Google Play scraper get?

App details (55 fields including installs, ratings histogram, and screenshots), search results, autocomplete suggestions, top charts by category, developer portfolios, similar apps, paginated reviews, permissions, and the parsed Data Safety section. That coverage spans the ~1.9 million apps live on Google Play (AppBrain, 2026).

Can I migrate from the original google-play-scraper package?

Yes, usually by swapping one import. Method names, options, and constants match. You gain strict TypeScript types from zod schemas, runtime validation, typed error classes, dual ESM/CommonJS builds, and daily live contract tests that the unmaintained original lacks.

Why can't I run this in the browser?

Google Play's pages don't send CORS headers permitting cross-origin reads, so no browser-based scraper can work. The library targets Node.js 22.12+ and uses its native fetch.

Wrap-Up

Google Play scraping in 2026 in three sentences. There's no official API and there won't be one soon. The data sits in unlabeled arrays that shift under you. Pick a tool that treats breakage as a first-class design problem, or become the maintainer yourself at 2 a.m.

npm install @mradex77/google-play-scraper
Enter fullscreen mode Exit fullscreen mode
import gplay from '@mradex77/google-play-scraper';

const details = await gplay.app({ appId: 'com.google.android.apps.translate' });
Enter fullscreen mode Exit fullscreen mode

The README documents every method and return shape, and examples/all-methods.ts runs the entire API end to end.

If this saved you from reverse-engineering batchexecute, a star on GitHub genuinely helps the project. And if a field ever stops resolving, open an issue with the SpecError output. Fastest bug report format there is.


Sources: Business of Apps, Google Play Statistics, retrieved 2026-07-08. AppBrain, Number of Android apps on Google Play, retrieved 2026-07-08. StatCounter Global Stats, Mobile OS Market Share Worldwide, retrieved 2026-07-08. 42matters, Google Play Statistics and Trends, retrieved 2026-07-08. npm registry, weekly download counts for google-play-scraper, retrieved 2026-07-08.

Top comments (0)