{"_id":"@mradex77/google-play-scraper","_rev":"9-1e610e20634a5489fee57443f0671343","name":"@mradex77/google-play-scraper","dist-tags":{"latest":"1.3.0"},"versions":{"0.1.1":{"name":"@mradex77/google-play-scraper","version":"0.1.1","keywords":["google-play","google-play-scraper","play-store","scraper","android","app-store","aso","typescript"],"license":"MIT","_id":"@mradex77/google-play-scraper@0.1.1","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"dist":{"shasum":"d4b282cac052ac4fbbe41ab7a73f292bfaca5d50","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-0.1.1.tgz","fileCount":9,"integrity":"sha512-wBCVVcaw+1D6px2qZkOcBPg+NodfSRTloIVyl2owY9LCDFkjNeqe5GevopAgT3LxMNhIiH1379grdGUaeBF9sQ==","signatures":[{"sig":"MEYCIQDOgMPUEZxloFX6nEyVJTqPqNJXrt2VFgDslV6rd9uWxwIhANJxU5bdZE7O0n+mvlC9DX0uqRXBqxfRfPsc6w4sFvfs","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":592137},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"565bda98b2faa8a2a009974947e23d2bf7bc9ea5","scripts":{"lint":"eslint .","test":"vitest run --project unit","build":"tsdown","format":"prettier --write .","example":"tsx examples/all-methods.ts","prepare":"husky","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"tsx scripts/update-fixtures.ts"},"_npmUser":{"name":"mradex77","email":"mradex77@gmail.com"},"_npmVersion":"11.18.0","description":"TypeScript client for scraping public Google Play app data","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"zod":"^4.4.3","cheerio":"1.2.0","lru-cache":"^11.5.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"tsx":"^4.23.0","husky":"^9.1.7","eslint":"^10.6.0","tsdown":"^0.22.3","vitest":"^4.1.10","publint":"^0.3.21","prettier":"^3.9.4","@eslint/js":"^10","typescript":"^6.0.3","@types/node":"^22.20.0","lint-staged":"^17.0.8","@commitlint/cli":"^21.2.0","typescript-eslint":"^8.62.1","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.4","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/google-play-scraper_0.1.1_1783505527883_0.2981194460301939","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@mradex77/google-play-scraper","version":"0.2.0","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod"],"author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"license":"MIT","_id":"@mradex77/google-play-scraper@0.2.0","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"homepage":"https://github.com/MrAdex77/google-play-scraper#readme","bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"dist":{"shasum":"eb9b47b58e101da677ac2548cfb537f04b23ed68","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-0.2.0.tgz","fileCount":9,"integrity":"sha512-6zA8RGCs4sEyOJtr6Y6JqToOcHDCATcMnBhEaBId1ZCsRVh/MeIU/D77V5pwHORV5uxbpmcmviAuQak3mvOxBA==","signatures":[{"sig":"MEYCIQCV9oTNiA1U7OXDQ4Dg5ntRLd0hX6bNn6QDUvibfwZvWgIhAOc1gpjZcPHrEDFZ8NZ9nvAyWiFJ5Qu5cJnQfF4fWAsV","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@mradex77%2fgoogle-play-scraper@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":608577},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"d5c78733711bafd96c80073dc9153751f6fb4914","scripts":{"lint":"eslint .","test":"vitest run --project unit","build":"tsdown","format":"prettier --write .","example":"tsx examples/all-methods.ts","prepare":"husky","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"tsx scripts/update-fixtures.ts"},"_npmUser":{"name":"mradex77","email":"mradex77@gmail.com"},"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"_npmVersion":"11.16.0","description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"zod":"^4.4.3","cheerio":"1.2.0","lru-cache":"^11.5.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"tsx":"^4.23.0","husky":"^9.1.7","eslint":"^10.6.0","tsdown":"^0.22.3","vitest":"^4.1.10","publint":"^0.3.21","prettier":"^3.9.4","@eslint/js":"^10","typescript":"^6.0.3","@types/node":"^22.20.0","lint-staged":"^17.0.8","@commitlint/cli":"^21.2.0","typescript-eslint":"^8.62.1","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.4","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/google-play-scraper_0.2.0_1783515254338_0.11956831109336852","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@mradex77/google-play-scraper","version":"0.3.0","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod"],"author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"license":"MIT","_id":"@mradex77/google-play-scraper@0.3.0","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"homepage":"https://github.com/MrAdex77/google-play-scraper#readme","bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"dist":{"shasum":"9532a66cb60303260e7fabccdc48f39e036b4fb7","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-0.3.0.tgz","fileCount":9,"integrity":"sha512-jDaZMazJwuPFZI72SwdmWVGCCtnMfcB46bLlcNsIviCgEBw87qJFspXHHMXYZupcFh0oMrU7l3pA1DabXZjnpQ==","signatures":[{"sig":"MEUCIG56hpeaIXOle7I9GfdItOWtRWp+w+AM2pbyJrptkV2yAiEA0nlKwCffbCgKMoJWRKCJ1OG/OKTqIz/I09R3L8tNiVg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@mradex77%2fgoogle-play-scraper@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":630161},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"e90f2c43c1f106b83a452122cb229d9b38734002","scripts":{"lint":"eslint .","test":"vitest run --project unit","build":"tsdown","format":"prettier --write .","example":"tsx examples/all-methods.ts","prepare":"husky","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"tsx scripts/update-fixtures.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:09f5f2a8-153f-45d3-9ee5-3ddb800678db"}},"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"_npmVersion":"11.16.0","description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"zod":"^4.4.3","cheerio":"1.2.0","lru-cache":"^11.5.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"tsx":"^4.23.0","husky":"^9.1.7","eslint":"^10.6.0","tsdown":"^0.22.3","vitest":"^4.1.10","publint":"^0.3.21","prettier":"^3.9.4","@eslint/js":"^10","typescript":"^6.0.3","@types/node":"^22.20.0","lint-staged":"^17.0.8","@commitlint/cli":"^21.2.0","typescript-eslint":"^8.62.1","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.4","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/google-play-scraper_0.3.0_1783589513642_0.32646618233662617","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@mradex77/google-play-scraper","version":"0.4.0","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod"],"author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"license":"MIT","_id":"@mradex77/google-play-scraper@0.4.0","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"homepage":"https://github.com/MrAdex77/google-play-scraper#readme","bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"dist":{"shasum":"00a3d9719a8ba377b4ab271b38ef95bb3ace8a0e","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-0.4.0.tgz","fileCount":9,"integrity":"sha512-wyS3DuHvb92emtNjNeVEyQdiRqgumb6y9tTG6GXZStj9ZYDXohoif7TJTJJihGSXvahHhEdzJyg9Q3xYHhWTeA==","signatures":[{"sig":"MEYCIQCTD41GvyZ69aMXznuGgkJh7CUbOStcPTJCTOBLJBQKEwIhANT0OZuNEfKyOj8NJdHGozKbk7moTweJCK0qvyFE++Kc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@mradex77%2fgoogle-play-scraper@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":755022},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"6b04ea2013bc0e5c81c196b7bba92675674cd61d","scripts":{"lint":"eslint .","test":"vitest run --project unit","build":"tsdown","format":"prettier --write .","example":"tsx examples/all-methods.ts","prepare":"husky","docs:api":"typedoc","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"tsx scripts/update-fixtures.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:09f5f2a8-153f-45d3-9ee5-3ddb800678db"}},"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"_npmVersion":"11.16.0","description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"zod":"^4.4.3","cheerio":"1.2.0","lru-cache":"^11.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"tsx":"^4.23.1","husky":"^9.1.7","eslint":"^10.7.0","tsdown":"^0.22.7","vitest":"^4.1.10","publint":"^0.3.21","typedoc":"^0.28.20","prettier":"^3.9.5","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.1","lint-staged":"^17.0.8","@commitlint/cli":"^21.2.1","typescript-eslint":"^8.64.0","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.5","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/google-play-scraper_0.4.0_1784106899087_0.6425125414501616","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@mradex77/google-play-scraper","version":"0.4.1","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod"],"author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"license":"MIT","_id":"@mradex77/google-play-scraper@0.4.1","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"homepage":"https://github.com/MrAdex77/google-play-scraper#readme","bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"dist":{"shasum":"b203144a99d7591c8be6a2a5148d17f0fbf7f9d2","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-0.4.1.tgz","fileCount":9,"integrity":"sha512-yHO60Q1DfNYgkEEa4Sjf8RsIJ6pdvTH9jAxO57t0QdYy2tEP7AcYt9M0GMz8/Z3jHN48dDpeYoieCzLJEPhkQA==","signatures":[{"sig":"MEMCIEKhNbj4SXMo3aaJ5Wz6obh7AhJLsUOphd7qPxJ9TCkFAh98WQF+qEzD2mIBQu36dxkAt6NME1hpxR/4OFB2iZFH","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@mradex77%2fgoogle-play-scraper@0.4.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":755922},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"abe8c5331e69687f6ba767281d00c68df1cce52e","scripts":{"lint":"eslint .","test":"vitest run --project unit","build":"tsdown","format":"prettier --write .","example":"tsx examples/all-methods.ts","prepare":"husky","docs:api":"typedoc","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"tsx scripts/update-fixtures.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:09f5f2a8-153f-45d3-9ee5-3ddb800678db"}},"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"_npmVersion":"11.16.0","description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"zod":"^4.4.3","cheerio":"1.2.0","lru-cache":"^11.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"tsx":"^4.23.1","husky":"^9.1.7","eslint":"^10.7.0","tsdown":"^0.22.7","vitest":"^4.1.10","publint":"^0.3.21","typedoc":"^0.28.20","prettier":"^3.9.5","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.1","lint-staged":"^17.0.8","@commitlint/cli":"^21.2.1","typescript-eslint":"^8.64.0","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.5","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/google-play-scraper_0.4.1_1784145647644_0.8512725347091841","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@mradex77/google-play-scraper","version":"1.0.0","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod","cli"],"author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"license":"MIT","_id":"@mradex77/google-play-scraper@1.0.0","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"homepage":"https://github.com/MrAdex77/google-play-scraper#readme","bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"bin":{"google-play-scraper":"dist/cli.js"},"dist":{"shasum":"598c72269538fdd8826ec732c2b70bf75274706b","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-1.0.0.tgz","fileCount":11,"integrity":"sha512-uQspjlqG8P6MN2I2b6D2PnLNIq9o1iuE2my8E6V9H6wmlPnFLFGqHboa5/1T+HFPSqzlIOp4A7/j8hTUAT8ExA==","signatures":[{"sig":"MEQCIFtIcD+3z1UT+Gou/HvsRWK8GFheWH6EMPANXLE35+Z1AiBZpb0WzenD6GvyVZgV2nrn7BXUEgeib5+rY52cqXq7Hg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@mradex77%2fgoogle-play-scraper@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1175112},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"f57fed88ee423fc93088d6391675058a91d228c4","scripts":{"lint":"eslint .","test":"vitest run --project unit","bench":"vitest bench --run --project unit","build":"tsdown","format":"prettier --write .","example":"tsx examples/all-methods.ts","prepare":"husky","docs:api":"typedoc","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","coverage:live":"tsx scripts/live-coverage-report.ts","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"tsx scripts/update-fixtures.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:09f5f2a8-153f-45d3-9ee5-3ddb800678db"}},"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"_npmVersion":"11.16.0","description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"zod":"^4.4.3","lru-cache":"^11.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"tsx":"^4.23.1","husky":"^9.1.7","eslint":"^10.7.0","tsdown":"^0.22.7","vitest":"^4.1.10","cheerio":"1.2.0","publint":"^0.3.21","typedoc":"^0.28.20","prettier":"^3.9.5","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.1","lint-staged":"^17.0.8","@commitlint/cli":"^21.2.1","typescript-eslint":"^8.64.0","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.5","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/google-play-scraper_1.0.0_1784309528004_0.10915230628580908","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@mradex77/google-play-scraper","version":"1.1.0","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod","cli"],"author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"license":"MIT","_id":"@mradex77/google-play-scraper@1.1.0","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"homepage":"https://github.com/MrAdex77/google-play-scraper#readme","bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"bin":{"google-play-scraper":"dist/cli.js"},"dist":{"shasum":"6dee9e6316db14140d769adf4df4de1a857874db","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-1.1.0.tgz","fileCount":11,"integrity":"sha512-XQ1HrQRmNHUWFhYF5JT4oZNSWj3yVDR1wfS0dhz7JCi5nvkf/QOoo/c1TOouTj56Va4Pcuh78qSgYvrNU3QoJw==","signatures":[{"sig":"MEYCIQDEd0367s/bJAOAXw9uMyA8ovO2GtmT2wKW5NwbXxUVmgIhAO4Gb2FTUKkRsMsnos4roQMIg2kCZMaMuuTOJSf5He2m","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@mradex77%2fgoogle-play-scraper@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1341722},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"916a61116e27f727e626e5af64414974c44282c3","scripts":{"lint":"eslint .","test":"vitest run --project unit","bench":"vitest bench --run --project unit","build":"tsdown","format":"prettier --write .","example":"tsx examples/all-methods.ts","prepare":"husky","docs:api":"typedoc","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","coverage:live":"tsx scripts/live-coverage-report.ts","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"tsx scripts/update-fixtures.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:09f5f2a8-153f-45d3-9ee5-3ddb800678db"}},"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"_npmVersion":"11.16.0","description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"zod":"^4.4.3","lru-cache":"^11.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"tsx":"^4.23.1","husky":"^9.1.7","eslint":"^10.7.0","tsdown":"^0.22.7","vitest":"^4.1.10","cheerio":"1.2.0","publint":"^0.3.21","typedoc":"^0.28.20","prettier":"^3.9.5","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.1","lint-staged":"^17.0.8","@commitlint/cli":"^21.2.1","typescript-eslint":"^8.64.0","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.5","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/google-play-scraper_1.1.0_1785494176693_0.3771673358445995","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@mradex77/google-play-scraper","version":"1.2.0","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod","cli"],"author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"license":"MIT","_id":"@mradex77/google-play-scraper@1.2.0","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"homepage":"https://github.com/MrAdex77/google-play-scraper#readme","bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"bin":{"google-play-scraper":"dist/cli.js"},"dist":{"shasum":"a3941b09565a425dad270c35bafa3a75a21b765f","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-1.2.0.tgz","fileCount":11,"integrity":"sha512-40zyOcvfatylVJc+Dx7kTOhhe3kCmOzl9+kzlbtdl2xpw8HoO3pdXR+5vVeOA9SWgjoK+C4AfrROTIo/JuPUXQ==","signatures":[{"sig":"MEQCIA2Sp2Ual1XXyScG0X7Nw5009l9d1ZLC9tikSHGkoijFAiBNGqovGmVCkIRZO8VTL7V8pQbC7t/q3kgaINe3CGKr7A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIHj5zykcWIz7U6UrHIwbrgEvZ/1bdEZnw+50/VcwxJ/OAiA1dUXpE7ZJrZC3kCiEoa4Nfu6YuRXMhA2B3kpWbYchWQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@mradex77%2fgoogle-play-scraper@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1363289},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"61d599b9049514a38ec6af8d8529f8bfa02c5af8","scripts":{"lint":"eslint .","test":"vitest run --project unit","bench":"vitest bench --run --project unit","build":"tsdown","format":"prettier --write .","example":"tsx examples/all-methods.ts","prepare":"husky","docs:api":"typedoc","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","coverage:live":"tsx scripts/live-coverage-report.ts","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"tsx scripts/update-fixtures.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:09f5f2a8-153f-45d3-9ee5-3ddb800678db"}},"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"_npmVersion":"11.19.0","description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.20.0","dependencies":{"zod":"^4.4.3","lru-cache":"^11.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"tsx":"^4.23.5","husky":"^9.1.7","eslint":"^10.8.0","tsdown":"^0.22.14","vitest":"^4.1.10","cheerio":"1.2.0","publint":"^0.3.22","typedoc":"^0.28.20","prettier":"^3.9.6","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.2","lint-staged":"^17.3.0","@commitlint/cli":"^21.2.1","typescript-eslint":"^8.65.0","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.5","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/google-play-scraper_1.2.0_1789127135993_0.2872744597340149","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"_id":"@mradex77/google-play-scraper@1.3.0","bin":{"google-play-scraper":"dist/cli.js"},"bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"dist":{"shasum":"152fe05eec9be91a430a2ec35164fe47788c0233","tarball":"https://registry.npmjs.org/@mradex77/google-play-scraper/-/google-play-scraper-1.3.0.tgz","fileCount":11,"integrity":"sha512-3cS7Kvq52QqXQt6kn+R45+zN6jNCIgAn29upOgefjfImqmSQIbsNDrw1aIrYBDHLTlgqMsIqHOn18VOEqqHyJw==","signatures":[{"sig":"MEUCIC261R9+BObbCq2ydnuCoQGi3ttjuNEX7mh4weHidd1RAiEAzttqhG+qXLgSo+7NlO8GaBAyHrE6ANPEHq8vx9XI1ug=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCxPUC3CwIjqyMvSsUp5cHnBP4yb55PTd+O9+sgV+kswgIgLpC2gtpc7AMsTCBcANtsHEporxD9Ordt10S8wSu7mHc="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@mradex77%2fgoogle-play-scraper@1.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1432730},"main":"./dist/index.cjs","name":"@mradex77/google-play-scraper","type":"module","types":"./dist/index.d.ts","author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"module":"./dist/index.js","engines":{"node":">=22.12.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"ccac78ac93d525e4e895e43203aaa744de3afaf3","license":"MIT","scripts":{"lint":"eslint .","test":"vitest run --project unit","bench":"vitest bench --run --project unit","build":"tsdown","format":"prettier --write .","example":"node examples/all-methods.ts","prepare":"husky","docs:api":"typedoc","lint:fix":"eslint . --fix","test:e2e":"vitest run --project e2e","typecheck":"tsc --noEmit","test:watch":"vitest --project unit","format:check":"prettier --check .","check:package":"pnpm build && publint && attw --pack .","coverage:live":"node scripts/live-coverage-report.ts","test:coverage":"vitest run --project unit --coverage","prepublishOnly":"pnpm build","fixtures:update":"node scripts/update-fixtures.ts"},"version":"1.3.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:09f5f2a8-153f-45d3-9ee5-3ddb800678db"}},"homepage":"https://github.com/MrAdex77/google-play-scraper#readme","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod","cli"],"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"_npmVersion":"11.19.0","description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"],"*.{js,json,md,yml,yaml}":["prettier --write"]},"maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"sideEffects":false,"_nodeVersion":"24.21.0","dependencies":{"zod":"^4.4.3","lru-cache":"^11.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"husky":"^9.1.7","eslint":"^10.8.0","tsdown":"^0.23.0","vitest":"^4.1.10","cheerio":"1.2.0","publint":"^0.3.22","typedoc":"^0.28.20","prettier":"^3.9.6","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.2","lint-staged":"^17.3.0","@commitlint/cli":"^21.2.1","typescript-eslint":"^8.65.0","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.5","eslint-config-prettier":"^10.1.8","@commitlint/config-conventional":"^21.2.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/google-play-scraper_1.3.0_1790239078272_0.33663296078807625"}}},"time":{"created":"2026-07-08T10:12:07.615Z","modified":"2026-09-24T08:37:58.663Z","0.1.1":"2026-07-08T10:12:08.043Z","0.2.0":"2026-07-08T12:54:14.493Z","0.3.0":"2026-07-09T09:31:53.795Z","0.4.0":"2026-07-15T09:14:59.267Z","0.4.1":"2026-07-15T20:00:47.813Z","1.0.0":"2026-07-17T17:32:08.180Z","1.1.0":"2026-07-31T10:36:16.840Z","1.2.0":"2026-09-11T11:45:36.099Z","1.3.0":"2026-09-24T08:37:58.349Z"},"bugs":{"url":"https://github.com/MrAdex77/google-play-scraper/issues"},"author":{"url":"https://github.com/MrAdex77","name":"MrAdex77"},"license":"MIT","homepage":"https://github.com/MrAdex77/google-play-scraper#readme","keywords":["google-play","google-play-scraper","google-play-api","play-store","play-store-api","playstore","scraper","web-scraping","android","android-apps","app-store","app-data","app-reviews","reviews","aso","app-store-optimization","market-research","typescript","nodejs","zod","cli"],"repository":{"url":"git+https://github.com/MrAdex77/google-play-scraper.git","type":"git"},"description":"Google Play scraper for Node.js with a fully typed TypeScript API. Fetch app details, search results, top charts, reviews, permissions and data safety from the Play Store.","maintainers":[{"name":"mradex77","email":"mradex77@gmail.com"}],"readme":"# google-play-scraper\n\n[![npm version](https://img.shields.io/npm/v/@mradex77/google-play-scraper.svg)](https://www.npmjs.com/package/@mradex77/google-play-scraper)\n[![npm downloads](https://img.shields.io/npm/dm/@mradex77/google-play-scraper.svg)](https://www.npmjs.com/package/@mradex77/google-play-scraper)\n[![CI](https://github.com/MrAdex77/google-play-scraper/actions/workflows/ci.yml/badge.svg)](https://github.com/MrAdex77/google-play-scraper/actions/workflows/ci.yml)\n[![Live contract tests](https://github.com/MrAdex77/google-play-scraper/actions/workflows/e2e.yml/badge.svg)](https://github.com/MrAdex77/google-play-scraper/actions/workflows/e2e.yml)\n[![API docs](https://github.com/MrAdex77/google-play-scraper/actions/workflows/docs.yml/badge.svg)](https://github.com/MrAdex77/google-play-scraper/actions/workflows/docs.yml)\n[![coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/MrAdex77/5aa7b737d6fb2adae68cb61693cfa123/raw/google-play-scraper-coverage.json)](https://github.com/MrAdex77/google-play-scraper/actions/workflows/ci.yml)\n[![types](https://img.shields.io/npm/types/@mradex77/google-play-scraper.svg)](https://www.npmjs.com/package/@mradex77/google-play-scraper)\n[![node](https://img.shields.io/node/v/@mradex77/google-play-scraper.svg)](https://nodejs.org)\n[![license](https://img.shields.io/npm/l/@mradex77/google-play-scraper.svg)](LICENSE)\n\n📖 **[API reference](https://mradex77.github.io/google-play-scraper/)** — browsable, always-current typed signatures for every method, option and error class, generated from the source on each release.\n\n**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.\n\nThis is a modern TypeScript rewrite of the popular but unmaintained [`google-play-scraper`](https://github.com/facundoolano/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.\n\n## Why this library\n\n- **Fully typed results.** Every method returns a precise TypeScript type derived from a [zod](https://zod.dev) schema that validates the scraped data at runtime.\n- **No HTTP dependency.** Runs on the native `fetch` of Node.js 22 and newer.\n- **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 whole call down.\n- **Verified against live Google Play daily.** Contract tests run against play.google.com on a daily schedule in CI and open a labeled issue the moment Google changes its layout.\n- **Typed errors.** Branch on `NotFoundError`, `RateLimitError` or `SpecError` instead of parsing message strings.\n- **Throttling, retries and caching included.** Rate limiting, exponential backoff with `Retry-After` support and an optional memoized client come standard.\n- **ESM and CommonJS.** Ships both module formats plus type declarations from a single package.\n\n## Comparison with the original google-play-scraper\n\n| Capability           | @mradex77/google-play-scraper               | facundoolano/google-play-scraper           |\n| -------------------- | ------------------------------------------- | ------------------------------------------ |\n| Language             | TypeScript in strict mode                   | JavaScript                                 |\n| Type definitions     | Generated from zod schemas                  | Community typings                          |\n| Runtime validation   | zod/mini on every input and output boundary | None                                       |\n| Error handling       | Typed error classes                         | Plain `Error`                              |\n| Module formats       | ESM and CommonJS with `.d.ts`               | ESM only                                   |\n| Runtime dependencies | `zod`, `lru-cache`                          | `cheerio`, `got`, `memoizee`, `ramda`, ... |\n| Breakage detection   | Daily live contract tests in CI             | None                                       |\n| Maintenance          | Actively maintained                         | Unmaintained                               |\n\n## Table of contents\n\n- [Installation](#installation)\n- [CLI](#cli)\n- [Quick start](#quick-start)\n- [Common options](#common-options)\n- [Shared client](#shared-client)\n- [Methods](#methods)\n- [Streaming and bulk reads](#streaming-and-bulk-reads)\n- [Constants](#constants)\n- [Error handling](#error-handling)\n- [Throttling and requestOptions](#throttling-and-requestoptions)\n- [Resilience](#resilience)\n- [Monitoring drift](#monitoring-drift)\n- [Versioning and API stability](#versioning-and-api-stability)\n- [Migrating from google-play-scraper](#migrating-from-google-play-scraper)\n- [FAQ](#faq)\n- [Related projects](#related-projects)\n- [Contributing](#contributing)\n- [Disclaimer](#disclaimer)\n- [License](#license)\n\n## Installation\n\n```\nnpm install @mradex77/google-play-scraper\n```\n\nRequires Node.js 22.12 or newer.\n\nEvery release is published from GitHub Actions with [npm provenance](https://docs.npmjs.com/generating-provenance-statements), so the package on npm is cryptographically linked to the exact commit and workflow that built it. Verify your installed tree with:\n\n```sh\nnpm audit signatures\n```\n\n## CLI\n\nEvery method with a JSON-friendly surface is also available from the command line, no install required:\n\n```sh\nnpx @mradex77/google-play-scraper app com.spotify.music\nnpx @mradex77/google-play-scraper search \"sleep tracker\" --num 5 --country de\nnpx @mradex77/google-play-scraper reviews com.mojang.minecraftpe --sort rating --num 20\nnpx @mradex77/google-play-scraper availability com.adex77.WhereAmI --countries us,pl,de\n```\n\n| Command                | Positional              | Own flags                                                       |\n| ---------------------- | ----------------------- | --------------------------------------------------------------- |\n| `app <appId>`          | app id                  |                                                                 |\n| `apps <appIds>`        | comma-separated app ids | `--concurrency`                                                 |\n| `search <term>`        | search term             | `--num`, `--price`, `--full-detail`                             |\n| `suggest <term>`       | search term             |                                                                 |\n| `list`                 |                         | `--collection`, `--category`, `--age`, `--num`, `--full-detail` |\n| `developer <devId>`    | developer id            | `--num`, `--full-detail`                                        |\n| `similar <appId>`      | app id                  | `--full-detail`                                                 |\n| `reviews <appId>`      | app id                  | `--num`, `--sort`, `--paginate`, `--token`                      |\n| `permissions <appId>`  | app id                  | `--short`                                                       |\n| `data-safety <appId>`  | app id                  |                                                                 |\n| `categories`           |                         |                                                                 |\n| `availability <appId>` | app id                  | `--countries` (comma-separated, required), `--concurrency`      |\n\nEvery command also accepts `--lang <code>`, `--country <code>` and `--throttle <requestsPerSecond>`, plus `--help`/`-h` (global or per command) and the global `--version`.\n\nResults are pretty-printed JSON on stdout, so the output pipes straight into `jq`:\n\n```sh\nnpx @mradex77/google-play-scraper app com.spotify.music | jq '{title, score, installs}'\n```\n\nExit codes: `0` success, `1` scrape failure (not found, rate limited, blocked, network), `2` usage error (unknown command or flag, missing argument, invalid option value).\n\n## Quick start\n\nThe package exposes named exports and an aggregate default export. Use whichever style you prefer.\n\n```typescript\nimport gplay from '@mradex77/google-play-scraper';\n\nconst app = await gplay.app({ appId: 'com.google.android.apps.translate' });\nconsole.log(app.title, app.score);\n```\n\n```typescript\nimport { app, type App } from '@mradex77/google-play-scraper';\n\nconst details: App = await app({ appId: 'com.google.android.apps.translate' });\nconsole.log(details.installs);\n```\n\nCommonJS works too:\n\n```javascript\nconst gplay = require('@mradex77/google-play-scraper').default;\n\ngplay.search({ term: 'panda' }).then((results) => {\n  console.log(results.length);\n});\n```\n\nMore runnable examples live in [examples/](https://github.com/MrAdex77/google-play-scraper/tree/main/examples).\n\n## Common options\n\nEvery method accepts a single options object. These options are available on all of them:\n\n| Option             | Type       | Default | Description                                                                                                    |\n| ------------------ | ---------- | ------- | -------------------------------------------------------------------------------------------------------------- |\n| `lang`             | `string`   | `'en'`  | Two letter language code used to fetch the page.                                                               |\n| `country`          | `string`   | `'us'`  | Two letter country code. Needed for apps available only in some regions.                                       |\n| `throttle`         | `number`   | none    | Maximum requests per second across a single call.                                                              |\n| `requestOptions`   | `object`   | none    | HTTP overrides. See [Throttling and requestOptions](#throttling-and-requestoptions).                           |\n| `onDegradation`    | `function` | none    | Callback fired when a call degrades gracefully. See [Monitoring drift](#monitoring-drift).                     |\n| `onIntegrityEvent` | `function` | none    | Callback fired when routing, parsing or pagination needs attention. See [Monitoring drift](#monitoring-drift). |\n\n## Shared client\n\nThe top-level functions each build their own HTTP client, so a `throttle` passed to one call only limits the requests made inside that single call, such as the pagination pages of one `search()`, `list()`, `developer()`, or `reviews()`. Ten parallel `app()` calls with `throttle: 1` each get their own limiter and still reach Google Play at the same time.\n\n`createClient` fixes this by sharing one rate limiter, one set of request options, and one pair of `lang`/`country` defaults across every method of the client:\n\n```typescript\nimport { createClient } from '@mradex77/google-play-scraper';\n\nconst client = createClient({ country: 'pl', lang: 'pl', throttle: 5 });\n\nconst ids = ['com.foo', 'com.bar', 'com.baz'];\nconst details = await Promise.all(ids.map((appId) => client.app({ appId })));\n```\n\nThat whole batch never exceeds five requests per second in total, across every method of the client, including the `fullDetail` app lookups made by `search`, `list`, `developer`, and `similar`.\n\nPrecedence rules:\n\n1. A client-level `throttle` creates one shared limiter that every method call goes through, including pagination pages and `fullDetail` lookups.\n2. A per-call `throttle` passed to a client method is superseded by the client limiter. When the client was created without `throttle`, a per-call value behaves exactly like the top-level functions do today (a call-scoped limiter).\n3. `requestOptions` merge shallowly, with per-call keys winning: `{ ...client, ...call }`.\n4. Per-call `lang` and `country` win over the client defaults; the built-in `en`/`us` defaults apply last.\n5. A client-level `onDegradation` or `onIntegrityEvent` applies to every call, including the `fullDetail` and batch lookups the client makes on your behalf. A per-call callback replaces the client-level one for that call; callbacks do not chain.\n\nEvery method from the [reference below](#methods) is available on the client, alongside the exported constants. Two clients created with `createClient` are fully independent and never share a limiter. [`memoized`](#memoized) accepts the same options and adds an LRU cache on top.\n\n> A per-call `throttle` on the top-level functions only rate-limits the requests within that single call, such as its pagination pages. Reach for `createClient` when you need one limit to span many calls.\n\n## Methods\n\n- [app](#app): full detail of a single application\n- [search](#search): apps matching a search term\n- [suggest](#suggest): search term autocompletions\n- [list](#list): a ranked collection of apps\n- [developer](#developer): other apps by the same developer\n- [similar](#similar): apps related to a given app\n- [reviews](#reviews): user reviews for an app\n- [permissions](#permissions): permissions an app requests\n- [dataSafety](#datasafety): the data safety section of an app\n- [categories](#categories): the Google Play category taxonomy\n- [memoized](#memoized): a shared client that caches equivalent calls\n\n### app\n\nRetrieves the full detail of an application.\n\n| Option  | Type     | Default  | Description                                    |\n| ------- | -------- | -------- | ---------------------------------------------- |\n| `appId` | `string` | required | The Google Play id (the `?id=` url parameter). |\n\n```typescript\nimport { app } from '@mradex77/google-play-scraper';\n\nconst details = await app({ appId: 'com.google.android.apps.translate' });\n```\n\nReturns an `App` with 55 fields. Trimmed:\n\n```javascript\n{\n  title: 'Google Translate',\n  description: 'Translate between up to 133 languages...',\n  descriptionHTML: 'Translate between up to 133 languages...<br>...',\n  summary: 'Instantly translate text, speech and images in over 100 languages',\n  installs: '1,000,000,000+',\n  minInstalls: 1000000000,\n  score: 4.48,\n  scoreText: '4.5',\n  ratings: 8765432,\n  reviews: 5678901,\n  histogram: { '1': 123456, '2': 45678, '3': 90123, '4': 234567, '5': 4567890 },\n  price: 0,\n  free: true,\n  currency: 'USD',\n  priceText: 'Free',\n  available: true,\n  offersIAP: false,\n  androidVersion: '8.0',\n  androidVersionText: '8.0 and up',\n  developer: 'Google LLC',\n  developerId: '5700313618786177705',\n  developerEmail: 'apps-help@google.com',\n  developerWebsite: 'http://support.google.com/translate',\n  genre: 'Tools',\n  genreId: 'TOOLS',\n  categories: [{ name: 'Tools', id: 'TOOLS' }],\n  icon: 'https://play-lh.googleusercontent.com/...',\n  screenshots: ['https://play-lh.googleusercontent.com/...'],\n  contentRating: 'Everyone',\n  adSupported: false,\n  updated: 1719878400000,\n  version: 'Varies with device',\n  comments: [],\n  appId: 'com.google.android.apps.translate',\n  url: 'https://play.google.com/store/apps/details?id=com.google.android.apps.translate'\n}\n```\n\nA listing that has not been released yet carries no offer at all. For those apps\n`preregister` is `true`, and `price`, `free` and `priceText` fall back to `0`, `false` and\n`'Free'` rather than describing a real offer. Rating, install and release fields are\nabsent too. Read `preregister` before trusting any price field:\n\n```ts\nconst details = await app({ appId: 'com.ironhidegames.android.kingdomrush6.genesis' });\n\nif (details.preregister) {\n  console.log(`${details.title} has no price yet`);\n}\n```\n\nThe same applies to a `search()` row for an unreleased app: it is returned with\n`price: 0` and `free: false`, so a `price: 'free'` filter will not match it.\n\n### Batch details\n\nFetches the full detail of many apps in one call, with a concurrency limit and per-id\nfailure capture. One missing app never rejects the whole batch, and results stay aligned\nwith the input order.\n\n| Option        | Type       | Default  | Description                                                        |\n| ------------- | ---------- | -------- | ------------------------------------------------------------------ |\n| `appIds`      | `string[]` | required | Between `1` and `250` Google Play ids.                             |\n| `concurrency` | `number`   | `5`      | Maximum number of app lookups in flight at once, from `1` to `20`. |\n\n```typescript\nimport { apps } from '@mradex77/google-play-scraper';\n\nconst result = await apps({\n  appIds: ['com.whatsapp', 'com.spotify.music', 'com.gone.app'],\n  concurrency: 5,\n});\n```\n\nEach entry is a discriminated union keyed on `status`; narrow on it to read the parsed\n`App` or the typed error:\n\n```typescript\nfor (const entry of result) {\n  if (entry.status === 'fulfilled') {\n    console.log(entry.appId, entry.app.title);\n  } else {\n    console.error(entry.appId, entry.error.name);\n  }\n}\n```\n\n```typescript\ntype AppsEntry =\n  | { appId: string; status: 'fulfilled'; app: App }\n  | { appId: string; status: 'rejected'; error: GooglePlayError };\n```\n\n`apps` does not deduplicate `appIds` within a call: a list with the same id twice fetches\nit twice. Use a [`memoized`](#memoized) client when you want repeated ids, across or within\nbatches, served from cache.\n\n`concurrency` bounds parallelism; a client [`throttle`](#throttling-and-requestoptions)\nbounds the request rate. When both apply, the effective rate is the smaller of the two\nconstraints, so `createClient({ throttle: 2 }).apps({ appIds, concurrency: 5 })` still\nissues at most two requests per second.\n\n### Country availability\n\nAnswers \"in which countries is this app published?\" by probing the Google Play details\npage once per country. Each probe is a plain `GET` whose HTTP status is the whole answer,\nso the body is never parsed and a probe is an order of magnitude cheaper than an `app`\nlookup. No other scraper offers this as a first-class call.\n\n| Option        | Type       | Default  | Description                                                      |\n| ------------- | ---------- | -------- | ---------------------------------------------------------------- |\n| `appId`       | `string`   | required | The Google Play id to probe.                                     |\n| `countries`   | `string[]` | required | Between `1` and `50` ISO 3166-1 alpha-2 codes, case insensitive. |\n| `lang`        | `string`   | `'en'`   | The `hl` language of each probe.                                 |\n| `concurrency` | `number`   | `5`      | Maximum number of probes in flight at once, from `1` to `20`.    |\n\n```typescript\nimport { availability } from '@mradex77/google-play-scraper';\n\nconst result = await availability({\n  appId: 'com.adex77.WhereAmI',\n  countries: ['us', 'pl', 'de', 'jp'],\n});\n```\n\n```typescript\n{\n  appId: 'com.adex77.WhereAmI',\n  countries: {\n    us: { status: 'available' },\n    pl: { status: 'available' },\n    de: { status: 'available' },\n    jp: { status: 'unavailable' },\n  },\n}\n```\n\nEach country resolves to one of three statuses:\n\n- `available` — the probe returned `200`; the app is published in that storefront.\n- `unavailable` — the probe returned `404`; the app is not published there.\n- `error` — the probe could not tell: a block (consent wall or captcha), a rate limit, any\n  other HTTP failure, or a network error, with the original `message` preserved.\n\n`error` is deliberately never collapsed into `unavailable`. A blocked or rate-limited probe\nmeans \"we could not determine availability\", which must not masquerade as \"not published\"\nin data used for release decisions. Country codes are returned lowercased, duplicates\n(ignoring case) are rejected, and a single call probes at most `50` countries.\n\nBecause Google Play geoblocking depends on the caller's IP as much as on the `gl`\nparameter, pair `availability` with [`createCountryFetch`](#routing-by-country) so each\nprobe exits through a proxy located in the country it probes:\n\n```typescript\nimport { ProxyAgent, fetch as undiciFetch, type RequestInit } from 'undici';\nimport { availability, createCountryFetch } from '@mradex77/google-play-scraper';\n\nconst throughProxy = (proxyUrl: string): typeof fetch => {\n  const dispatcher = new ProxyAgent(proxyUrl);\n  return ((input: string | URL, init?: RequestInit) =>\n    undiciFetch(input, { ...init, dispatcher })) as unknown as typeof fetch;\n};\n\nconst fetchImpl = createCountryFetch({\n  perCountry: {\n    us: throughProxy('http://us-proxy.internal:8080'),\n    de: throughProxy('http://de-proxy.internal:8080'),\n  },\n});\n\nconst result = await availability({\n  appId: 'com.adex77.WhereAmI',\n  countries: ['us', 'de'],\n  requestOptions: { fetchImpl },\n});\n```\n\nEach probe carries `gl=<country>`, so `createCountryFetch` routes the `us` probe through\nthe US proxy and the `de` probe through the German proxy.\n\n### search\n\nRetrieves apps that match a search term.\n\n| Option       | Type                        | Default  | Description                                                                  |\n| ------------ | --------------------------- | -------- | ---------------------------------------------------------------------------- |\n| `term`       | `string`                    | required | The search query.                                                            |\n| `num`        | `number`                    | `20`     | Number of results, up to `250`. Best-effort above the Google cap, see below. |\n| `price`      | `'all' \\| 'free' \\| 'paid'` | `'all'`  | Filter results by price.                                                     |\n| `fullDetail` | `boolean`                   | `false`  | When `true`, fetch and return the full `App` for each result.                |\n\n```typescript\nimport { search } from '@mradex77/google-play-scraper';\n\nconst results = await search({ term: 'panda', num: 5 });\n```\n\nReturns `SearchResult[]` (or `App[]` when `fullDetail` is `true`). Trimmed:\n\n```javascript\n[\n  {\n    title: 'Panda VPN',\n    appId: 'com.example.pandavpn',\n    url: 'https://play.google.com/store/apps/details?id=com.example.pandavpn',\n    icon: 'https://play-lh.googleusercontent.com/...',\n    developer: 'Panda Labs',\n    developerId: '1234567890',\n    currency: 'USD',\n    price: 0,\n    free: true,\n    summary: 'Fast and secure VPN',\n    scoreText: '4.3',\n    score: 4.3,\n  },\n];\n```\n\nGoogle Play currently serves only the first result page, roughly 30 apps and fewer for narrow terms, and provides no continuation token (verified July 2026). A `num` above that cap is best-effort: the returned array may be shorter than requested.\n\n### suggest\n\nGiven a partial term, returns up to five search completions.\n\n| Option | Type     | Default  | Description               |\n| ------ | -------- | -------- | ------------------------- |\n| `term` | `string` | required | The partial search query. |\n\n```typescript\nimport { suggest } from '@mradex77/google-play-scraper';\n\nconst suggestions = await suggest({ term: 'pand' });\n```\n\nReturns `string[]`:\n\n```javascript\n['panda', 'pandora', 'panda vpn', 'panda pop', 'pandora music'];\n```\n\n### list\n\nRetrieves a ranked collection of apps, optionally scoped to a category and an age bracket.\n\n| Option       | Type         | Default                | Description                                              |\n| ------------ | ------------ | ---------------------- | -------------------------------------------------------- |\n| `collection` | `Collection` | `collection.TOP_FREE`  | One of `TOP_FREE`, `TOP_PAID`, `GROSSING`.               |\n| `category`   | `Category`   | `category.APPLICATION` | Any [category](#constants) constant.                     |\n| `age`        | `Age`        | none                   | One of `age.FIVE_UNDER`, `age.SIX_EIGHT`, `age.NINE_UP`. |\n| `num`        | `number`     | `500`                  | Number of results.                                       |\n| `fullDetail` | `boolean`    | `false`                | When `true`, return the full `App` for each result.      |\n\n```typescript\nimport { list, collection, category } from '@mradex77/google-play-scraper';\n\nconst items = await list({\n  collection: collection.TOP_FREE,\n  category: category.GAME,\n  num: 5,\n});\n```\n\nReturns `ListItem[]` (or `App[]` when `fullDetail` is `true`), each shaped like a [search](#search) result.\n\nGoogle Play serves at most roughly 200 list items per request (verified July 2026), so a `num` above that ceiling, including the default `500`, returns fewer items than requested.\n\n### developer\n\nReturns other apps published by the same developer. The `devId` is either the numeric developer id or the developer name, exactly as it appears on Google Play.\n\n| Option       | Type      | Default  | Description                                         |\n| ------------ | --------- | -------- | --------------------------------------------------- |\n| `devId`      | `string`  | required | Numeric developer id or developer name.             |\n| `num`        | `number`  | `60`     | Number of results.                                  |\n| `fullDetail` | `boolean` | `false`  | When `true`, return the full `App` for each result. |\n\n```typescript\nimport { developer } from '@mradex77/google-play-scraper';\n\nconst apps = await developer({ devId: '5700313618786177705' });\n```\n\nReturns `DeveloperApp[]` (or `App[]` when `fullDetail` is `true`), each shaped like a [search](#search) result.\n\n### similar\n\nReturns apps related to a given app.\n\n| Option       | Type      | Default  | Description                                         |\n| ------------ | --------- | -------- | --------------------------------------------------- |\n| `appId`      | `string`  | required | The Google Play id of the reference app.            |\n| `fullDetail` | `boolean` | `false`  | When `true`, return the full `App` for each result. |\n\n```typescript\nimport { similar } from '@mradex77/google-play-scraper';\n\nconst apps = await similar({ appId: 'com.google.android.apps.translate' });\n```\n\nReturns `SimilarApp[]` (or `App[]` when `fullDetail` is `true`), each shaped like a [search](#search) result.\n\n### reviews\n\nRetrieves reviews for an app. Reviews always come back inside a `{ data, nextPaginationToken }` envelope so paging is uniform.\n\n| Option                | Type      | Default       | Description                                              |\n| --------------------- | --------- | ------------- | -------------------------------------------------------- |\n| `appId`               | `string`  | required      | The Google Play id of the app.                           |\n| `sort`                | `Sort`    | `sort.NEWEST` | One of `sort.NEWEST`, `sort.RATING`, `sort.HELPFULNESS`. |\n| `num`                 | `number`  | `150`         | Number of reviews to fetch.                              |\n| `paginate`            | `boolean` | `false`       | When `true`, fetch a single page and return its token.   |\n| `nextPaginationToken` | `string`  | none          | Continue from a token returned by a previous call.       |\n\n```typescript\nimport { reviews, sort } from '@mradex77/google-play-scraper';\n\nconst first = await reviews({ appId: 'com.google.android.apps.translate', sort: sort.NEWEST });\n\nif (first.nextPaginationToken) {\n  const next = await reviews({\n    appId: 'com.google.android.apps.translate',\n    paginate: true,\n    nextPaginationToken: first.nextPaginationToken,\n  });\n}\n```\n\nReturns `ReviewsResult`. Trimmed:\n\n```javascript\n{\n  data: [\n    {\n      id: 'gp:AOqpTOH...',\n      userName: 'Ada Lovelace',\n      userImage: 'https://play-lh.googleusercontent.com/...',\n      date: '2026-06-30T12:00:00.000Z',\n      score: 5,\n      title: null,\n      text: 'Works offline and the camera translation is great.',\n      thumbsUp: 42,\n      version: '8.9.0',\n      criterias: []\n    }\n  ],\n  nextPaginationToken: 'CqYBCqMB...'\n}\n```\n\n### permissions\n\nReturns the permissions an app requests.\n\n| Option  | Type      | Default  | Description                                                       |\n| ------- | --------- | -------- | ----------------------------------------------------------------- |\n| `appId` | `string`  | required | The Google Play id of the app.                                    |\n| `short` | `boolean` | `false`  | When `true`, return a flat `string[]` of common permission names. |\n\n```typescript\nimport { permissions } from '@mradex77/google-play-scraper';\n\nconst detailed = await permissions({ appId: 'com.google.android.apps.translate' });\nconst names = await permissions({ appId: 'com.google.android.apps.translate', short: true });\n```\n\nReturns `AppPermission[]` (or `string[]` when `short` is `true`):\n\n```javascript\n[\n  { permission: 'take pictures and videos', type: 0 },\n  { permission: 'view network connections', type: 1 },\n];\n```\n\nThe `type` is `permission.COMMON` (`0`) or `permission.OTHER` (`1`).\n\n### dataSafety\n\nReturns the data safety section of an app.\n\n| Option  | Type     | Default  | Description                    |\n| ------- | -------- | -------- | ------------------------------ |\n| `appId` | `string` | required | The Google Play id of the app. |\n\n```typescript\nimport { dataSafety } from '@mradex77/google-play-scraper';\n\nconst safety = await dataSafety({ appId: 'com.google.android.apps.translate' });\n```\n\nReturns `DataSafety`. Trimmed:\n\n```javascript\n{\n  sharedData: [\n    { data: 'Approximate location', optional: false, purpose: 'App functionality', type: 'Location' }\n  ],\n  collectedData: [\n    { data: 'Email address', optional: false, purpose: 'Account management', type: 'Personal info' }\n  ],\n  securityPractices: [\n    { practice: 'Data is encrypted in transit', description: 'Your data is transferred over a secure connection' }\n  ],\n  privacyPolicyUrl: 'https://policies.google.com/privacy'\n}\n```\n\n### categories\n\nReturns the Google Play category taxonomy as a list of category ids.\n\n| Option           | Type     | Default | Description                            |\n| ---------------- | -------- | ------- | -------------------------------------- |\n| `throttle`       | `number` | none    | See [common options](#common-options). |\n| `requestOptions` | `object` | none    | See [common options](#common-options). |\n\n```typescript\nimport { categories } from '@mradex77/google-play-scraper';\n\nconst ids = await categories();\n```\n\nReturns `string[]`:\n\n```javascript\n[\n  'APPLICATION',\n  'ANDROID_WEAR',\n  'ART_AND_DESIGN',\n  'AUTO_AND_VEHICLES',\n  'GAME',\n  'GAME_ACTION',\n  'FAMILY',\n];\n```\n\n### memoized\n\nReturns a [shared client](#shared-client) whose promise-returning methods share an LRU cache held in memory, so equivalent calls made within the TTL resolve from cache instead of hitting Google Play again. It accepts every `createClient` option plus the two cache bounds:\n\n| Option             | Type       | Default  | Description                                                                                                                                  |\n| ------------------ | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |\n| `lang`             | `string`   | `'en'`   | Language default applied to every call that does not set its own.                                                                            |\n| `country`          | `string`   | `'us'`   | Country default applied to every call that does not set its own.                                                                             |\n| `throttle`         | `number`   | none     | One shared limiter for every request the client makes, cache misses included.                                                                |\n| `requestOptions`   | `object`   | none     | Client-level HTTP overrides, merged under per-call ones. See [requestOptions](#throttling-and-requestoptions).                               |\n| `onDegradation`    | `function` | none     | Client-level degradation callback. See [Monitoring drift](#monitoring-drift).                                                                |\n| `onIntegrityEvent` | `function` | none     | Client-level integrity callback. See [Monitoring drift](#monitoring-drift).                                                                  |\n| `maxAgeMs`         | `number`   | `300000` | Time to live per cache entry, in milliseconds, counted from the moment the filling request completes. A positive integer up to `2147483647`. |\n| `max`              | `number`   | `1000`   | Maximum number of cached entries. A positive integer up to `1000000`.                                                                        |\n\n```typescript\nimport { memoized } from '@mradex77/google-play-scraper';\n\nconst client = memoized({ country: 'pl', lang: 'pl', throttle: 5, maxAgeMs: 60000, max: 500 });\n\nawait client.app({ appId: 'com.google.android.apps.translate' });\nawait client.app({ appId: 'com.google.android.apps.translate' });\n```\n\nInvalid options throw `ValidationError` before any cache or HTTP state exists.\n\n**What counts as the same call.** Entries are keyed by the options _after_ validation and defaulting, so these all share one entry: `{}` and `{ lang: 'en', country: 'us' }`, `country: 'US'` and `country: 'us'`, and the same options in any property order. Anything that changes the response or how it is obtained separates entries: `lang`, `country`, every feature option, and the `headers`, `timeoutMs`, `retries`, `fetchImpl` and `signal` fields of `requestOptions` (the last two by object identity, so two callers sharing a signal share an entry and one caller's abort never rejects an unrelated caller; a fresh signal on every call therefore never hits, so prefer `timeoutMs` for per-call deadlines). Pacing and telemetry never fragment the cache: `throttle`, `concurrency`, `onDegradation`, `onIntegrityEvent`, `onRequest`, `onResponse` and `onRetry` are ignored by the key, so inline callbacks are fine.\n\n**What is cached.** `app`, `availability`, `search`, `suggest`, `list`, `categories`, `developer`, `similar`, `reviews`, `permissions` and `dataSafety`. Concurrent equivalent calls share one in-flight request. A hit resolves to the same object the filling call returned, so treat cached results as read-only. A rejected call is never cached, so the next call retries. `apps` and the `fullDetail` lookups of `search`, `list`, `developer` and `similar` are served from the `app` entries they warm. `reviewsIterator`, `searchIterator`, `developerIterator`, `reviewsAll` and `apps` are not cached themselves; they share the client's defaults, limiter and request options and pass through to Google Play.\n\n**Cache hits replay their events.** Degradation and integrity events recorded while an entry was fetched are delivered again, in their original order, to the callbacks of every later call that hits the entry. A hit performs no request, so `onRequest`, `onResponse` and `onRetry` are not replayed. A replayed callback that throws rejects only the call it was passed to; the entry stays valid for other callers. Events recorded before a call fails are still delivered to every caller that shared the failed request. Events raised by the app lookups a `fullDetail` call or an `apps` batch makes on your behalf belong to that outer call: they reach its callbacks, and a cached `fullDetail` result replays them with its entry.\n\n**Cache controls.** The client exposes explicit control without global state:\n\n```typescript\nclient.cache.size;\nclient.cache.invalidate('app', { appId: 'com.google.android.apps.translate' });\nclient.cache.clear();\n```\n\n`invalidate` takes the method name and the same options you would pass to that method, applies the client defaults and validation to them (throwing `ValidationError` for options the method itself would reject), and drops the matching entry, returning whether one existed. The next equivalent call requests again while unrelated entries survive. `size` counts live entries and falls as entries expire or are evicted.\n\n## Streaming and bulk reads\n\n`reviews()`, `search()`, and `developer()` collect a fixed `num` of items before they\nresolve. When you want to walk results lazily and stop exactly when you have seen enough,\nuse the async iterators. They fetch nothing until the first `for await`, and they stop\nfetching the moment the consumer `break`s, so \"first ten reviews then stop\" costs exactly\none page request.\n\nThe iterators are available both as standalone functions and as methods on\n[`createClient`](#shared-client), where they share the client's limiter and defaults.\n\n### reviewsIterator\n\nStreams reviews one at a time across pages.\n\n```typescript\nimport { reviewsIterator } from '@mradex77/google-play-scraper';\n\nfor await (const review of reviewsIterator({ appId: 'com.whatsapp' })) {\n  if (review.score < 3) {\n    break;\n  }\n  console.log(review.userName, review.score);\n}\n```\n\nPass `nextPaginationToken` to resume a stream where a previous run left off. The token\ncomes from a prior `reviews({ paginate: true })` call or from persisted state:\n\n```typescript\nconst page = await reviews({ appId: 'com.whatsapp', paginate: true });\n\nconst resumed = reviewsIterator({\n  appId: 'com.whatsapp',\n  nextPaginationToken: page.nextPaginationToken ?? undefined,\n});\n```\n\n### reviewsAll\n\nDrains `reviewsIterator` into an array. Popular apps hold millions of reviews, so pass\n`maxReviews` to cap the read; the generator never fetches a page beyond the one that\ncontains the last item you asked for. Combine it with a client `throttle` to stay within\nGoogle Play's rate limits.\n\n```typescript\nimport { createClient } from '@mradex77/google-play-scraper';\n\nconst client = createClient({ throttle: 5 });\nconst recent = await client.reviewsAll({ appId: 'com.whatsapp', maxReviews: 2000 });\n```\n\nWithout `maxReviews`, `reviewsAll` walks the entire review history, which can be very large.\n\n### searchIterator and developerIterator\n\n```typescript\nimport { searchIterator, developerIterator } from '@mradex77/google-play-scraper';\n\nfor await (const result of searchIterator({ term: 'geography quiz' })) {\n  console.log(result.appId);\n}\n\nfor await (const item of developerIterator({ devId: 'Google LLC' })) {\n  console.log(item.title);\n}\n```\n\n### Non-goals\n\n- **No `listIterator`.** `list()` sends a single batchexecute RPC that returns up to `num`\n  items in one response; there is no server-side pagination token to iterate. Faking an\n  iterator over one response would misrepresent the network behavior.\n- **Iterators do not accept `fullDetail` or `num`.** Streaming consumers call\n  `client.app()` per item when they need full details, and the consumer controls the end\n  by breaking; baking N+1 lookups or a fixed count into a generator hides its real cost.\n  Use `reviewsAll({ maxReviews })` for the bounded case.\n\n## Constants\n\nThe library exports the same constant sets as the original, frozen and typed.\n\n| Constant     | Values                                                                                                            |\n| ------------ | ----------------------------------------------------------------------------------------------------------------- |\n| `category`   | All app and game categories plus the `FAMILY` set (e.g. `APPLICATION`, `TOOLS`, `GAME`, `GAME_PUZZLE`, `FAMILY`). |\n| `collection` | `TOP_FREE`, `TOP_PAID`, `GROSSING`.                                                                               |\n| `sort`       | `NEWEST` (`2`), `RATING` (`3`), `HELPFULNESS` (`1`).                                                              |\n| `age`        | `FIVE_UNDER` (`'AGE_RANGE1'`), `SIX_EIGHT` (`'AGE_RANGE2'`), `NINE_UP` (`'AGE_RANGE3'`).                          |\n| `permission` | `COMMON` (`0`), `OTHER` (`1`).                                                                                    |\n\n```typescript\nimport { category, collection, sort, age, permission } from '@mradex77/google-play-scraper';\n```\n\n## Error handling\n\nEvery failure surfaces as a typed subclass of `GooglePlayError`, so you can branch on the exact cause instead of parsing message strings.\n\n| Error             | Extends           | Thrown when                                                                                 |\n| ----------------- | ----------------- | ------------------------------------------------------------------------------------------- |\n| `GooglePlayError` | `Error`           | Base class for every error the library throws.                                              |\n| `ValidationError` | `GooglePlayError` | The options you passed fail their zod schema.                                               |\n| `HttpError`       | `GooglePlayError` | A request fails with an unsuccessful status or a network error. Carries `status` and `url`. |\n| `NotFoundError`   | `HttpError`       | Google Play responds `404`, e.g. an unknown `appId`.                                        |\n| `RateLimitError`  | `HttpError`       | Google Play responds `429` after retries are exhausted.                                     |\n| `BlockedError`    | `GooglePlayError` | A consent wall redirect, or a captcha challenge redirect that retries cannot clear.         |\n| `ParseError`      | `GooglePlayError` | A batchexecute response cannot be parsed.                                                   |\n| `SpecError`       | `ParseError`      | Extraction fails; lists every failing field and the paths that were tried.                  |\n\n```typescript\nimport { app, NotFoundError, SpecError } from '@mradex77/google-play-scraper';\n\ntry {\n  const details = await app({ appId: 'com.does.not.exist' });\n} catch (error) {\n  if (error instanceof NotFoundError) {\n    console.error('No such app');\n  } else if (error instanceof SpecError) {\n    console.error('Google Play changed its layout:', error.failures);\n  } else {\n    throw error;\n  }\n}\n```\n\n### Failure behavior\n\nAn unknown `appId` or `devId` does not fail the same way everywhere, because Google Play itself does not: HTML details surfaces respond `404`, while report surfaces serve an empty payload. Both behaviors are pinned by the live test suite.\n\n| Behavior for a missing app        | Methods                                                                                                                                                        |\n| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Throws `NotFoundError`            | `app`, `apps` (per entry, as a rejected `PromiseSettledResult`), `similar`, `developer`                                                                        |\n| Resolves with a typed empty value | `search` and `suggest` (`[]`), `reviews` (`{ data: [], nextPaginationToken: null }`), `permissions` (`[]`), `dataSafety` (empty arrays, no `privacyPolicyUrl`) |\n| Maps the throw to a status        | `availability` reports `unavailable`                                                                                                                           |\n\nTyped empties are limited to the documented response shapes above and other schema-backed empty variants. A structurally missing or malformed response root throws `ParseError`; a missing required field inside a valid root throws `SpecError`. Neither case is converted into an empty result, `0`, `false`, `Free`, or `VARY` unless that exact fallback is part of the documented field contract.\n\n## Throttling and requestOptions\n\nPass `throttle` to cap requests per second, and `requestOptions` to override the HTTP layer:\n\n| requestOptions field | Type                             | Description                                                                                                                                                               |\n| -------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `headers`            | `Record<string, string>`         | Extra headers merged into every request.                                                                                                                                  |\n| `fetchImpl`          | `typeof fetch`                   | A custom `fetch` implementation, useful for proxies and tests. Combine with [`createCountryFetch`](#routing-by-country) to route each storefront country separately.      |\n| `timeoutMs`          | `number`                         | Timeout per request, up to `120000`. Default `30000`.                                                                                                                     |\n| `retries`            | `number`                         | Retry count for `429`, `5xx` and captcha challenge redirects, `0` to `5`. Default `2`.                                                                                    |\n| `signal`             | `AbortSignal`                    | Cancels the call at once, including an in-flight request, a body read, retry backoff, a `Retry-After` wait and a queued throttle slot. See [Cancellation](#cancellation). |\n| `onRequest`          | `(event: RequestEvent) => void`  | Called before every attempt, including retries. See [Request lifecycle hooks](#request-lifecycle-hooks).                                                                  |\n| `onResponse`         | `(event: ResponseEvent) => void` | Called for each settled response — after the body is read on success — with `status` and `durationMs`.                                                                    |\n| `onRetry`            | `(event: RetryEvent) => void`    | Called when a retry is scheduled, with `delayMs` and `reason`.                                                                                                            |\n\n```typescript\nimport { app } from '@mradex77/google-play-scraper';\n\nconst details = await app({\n  appId: 'com.google.android.apps.translate',\n  throttle: 5,\n  requestOptions: {\n    timeoutMs: 15000,\n    retries: 3,\n    headers: { 'Accept-Language': 'de' },\n    fetchImpl: myProxiedFetch,\n  },\n});\n```\n\nRetries use exponential backoff with jitter. A `Retry-After` response header overrides the backoff when it is well formed, in either form RFC 9110 allows: a count of seconds, or an HTTP date, which is resolved against the response `Date` header when the server sends one and against the local clock otherwise.\n\nA `Retry-After` longer than 60 seconds is not waited on. The call ends immediately with the mapped status error, `RateLimitError` for a `429` or `BlockedError` for a captcha challenge redirect, so a server cannot park a request for minutes. The ceiling is fixed and independent of `timeoutMs`, which bounds a single attempt. A malformed, past or negative value is ignored and normal jittered backoff applies.\n\n### Request lifecycle hooks\n\nThe three `on*` callbacks make request telemetry observable at every level — direct calls, `createClient`, and `memoized` — including what wrapping `fetchImpl` cannot see: retries and backoff decisions.\n\n```typescript\nimport { app } from '@mradex77/google-play-scraper';\n\nconst details = await app({\n  appId: 'com.google.android.apps.translate',\n  requestOptions: {\n    onRequest: (event) => log.debug('gplay request', event),\n    onResponse: (event) => metrics.timing('gplay.latency', event.durationMs),\n    onRetry: (event) => metrics.increment('gplay.retry', { reason: event.reason }),\n  },\n});\n```\n\nEvery event carries `url`, `method`, and a 1-based `attempt`: the first try is attempt 1, a `RetryEvent` carries the number of the attempt that just failed, and the subsequent `RequestEvent` is that attempt plus one. `ResponseEvent` adds `status` and `durationMs`, measured from just before the fetch (throttle wait excluded) and, on success, through the transfer of the full body. `RetryEvent` adds the scheduled `delayMs`, a `reason` of `'status'` or `'network'`, and the HTTP `status` when the reason is a retryable status. A retry-free happy path emits exactly one `RequestEvent` and one `ResponseEvent`.\n\nHooks are telemetry, never control flow. A hook that throws or returns a rejecting promise is swallowed and the request proceeds unchanged — the deliberate opposite of [`onDegradation` and `onIntegrityEvent`](#monitoring-drift), which rethrow because they are data-integrity signals. When a client-level and a call-level `requestOptions` set the same hook, the call-level one replaces it for that call; hooks do not chain.\n\n### Routing requests through a proxy\n\nThe native `fetch` of Node.js ignores the `HTTP_PROXY` and `HTTPS_PROXY` environment variables by default. On Node.js 24 and newer, opt in when launching your process:\n\n```\nNODE_USE_ENV_PROXY=1 node app.js\n```\n\nFor per-call control, or on Node.js 22, inject a proxied `fetch` from [undici](https://github.com/nodejs/undici) through `fetchImpl`:\n\n```typescript\nimport { ProxyAgent, fetch as proxiedFetch, type RequestInit } from 'undici';\nimport { app } from '@mradex77/google-play-scraper';\n\nconst dispatcher = new ProxyAgent('http://user:password@proxy.example.com:8080');\n\nconst fetchImpl = ((input: string | URL, init?: RequestInit) =>\n  proxiedFetch(input, { ...init, dispatcher })) as unknown as typeof fetch;\n\nconst details = await app({\n  appId: 'com.google.android.apps.translate',\n  requestOptions: { fetchImpl },\n});\n```\n\n`ProxyAgent` also accepts an options object when the proxy needs more configuration, such as a `token` carrying a preformatted `Proxy-Authorization` header or TLS settings for the proxy connection.\n\n### Rotating proxies when blocked\n\nGoogle answers a burst of requests from one IP address with a redirect to its captcha challenge at `www.google.com/sorry`. The client retries that redirect like a `429`, and most challenges clear on the next attempt. When they keep coming until retries run out, or the challenge asks to wait longer than the `Retry-After` ceiling, the call rejects with a `BlockedError` whose message names the cause (`captcha challenge` or `consent wall`), which is the signal to slow down or switch to another exit IP:\n\n```typescript\nimport { ProxyAgent, fetch as undiciFetch, type RequestInit } from 'undici';\nimport { BlockedError, createClient } from '@mradex77/google-play-scraper';\n\nconst throughProxy = (proxyUrl: string): typeof fetch => {\n  const dispatcher = new ProxyAgent(proxyUrl);\n  return ((input: string | URL, init?: RequestInit) =>\n    undiciFetch(input, { ...init, dispatcher })) as unknown as typeof fetch;\n};\n\nconst routes = [\n  'http://user:password@proxy-1.example.com:8080',\n  'http://user:password@proxy-2.example.com:8080',\n].map((proxyUrl) =>\n  createClient({ throttle: 2, requestOptions: { fetchImpl: throughProxy(proxyUrl) } }),\n);\n\nasync function searchWithRotation(term: string) {\n  for (const client of routes) {\n    try {\n      return await client.search({ term });\n    } catch (error) {\n      if (!(error instanceof BlockedError)) {\n        throw error;\n      }\n    }\n  }\n  throw new Error('Every proxy is blocked');\n}\n```\n\nThe challenge is driven by request rate per IP, so each route gets its own `createClient` whose limiter spans every call through that proxy, concurrent ones included, and prevents most blocks before rotation is needed. A per-call `throttle` on `search` would only pace the requests inside that one call. Detection reads only the final response URL, never page text, so a search term or review that quotes a captcha message is returned as data.\n\n### Routing by country\n\n`createCountryFetch` builds a `fetch` implementation that picks a route per storefront country, so requests for each `country` option go through their own proxy. It reads the `gl` query parameter that Google Play requests carry, matches it against `perCountry` (ISO 3166-1 alpha-2, case insensitive), and falls back to `fallback` or the global `fetch` when no route matches:\n\n```typescript\nimport { ProxyAgent, fetch as undiciFetch, type RequestInit } from 'undici';\nimport { app, createCountryFetch } from '@mradex77/google-play-scraper';\n\nconst throughProxy = (proxyUrl: string): typeof fetch => {\n  const dispatcher = new ProxyAgent(proxyUrl);\n  return ((input: string | URL, init?: RequestInit) =>\n    undiciFetch(input, { ...init, dispatcher })) as unknown as typeof fetch;\n};\n\nconst fetchImpl = createCountryFetch({\n  perCountry: {\n    us: throughProxy('http://us-proxy.internal:8080'),\n    de: throughProxy('http://user:password@de-proxy.internal:8080'),\n  },\n  fallback: throughProxy('http://default-proxy.internal:8080'),\n});\n\nconst details = await app({\n  appId: 'com.google.android.apps.translate',\n  country: 'de',\n  requestOptions: { fetchImpl },\n});\n```\n\nRoutes accept any `typeof fetch`, so the same helper also works for per-country rate limiting, logging or fixtures in tests. Omit `fallback` to send unmatched countries through a direct connection. The only request without a `gl` parameter is the `dataSafety` page fetch, which always uses the fallback route.\n\n### Cancellation\n\nPass an `AbortSignal` to cancel long running calls, such as a `reviews` fetch that walks many pages. An aborted call rejects with the signal's reason and is never retried:\n\n```typescript\nimport { reviews } from '@mradex77/google-play-scraper';\n\nconst controller = new AbortController();\nsetTimeout(() => controller.abort(), 10000);\n\nconst result = await reviews({\n  appId: 'com.google.android.apps.translate',\n  num: 5000,\n  requestOptions: { signal: controller.signal },\n});\n```\n\nCancellation is immediate at every point in the request lifecycle, not only while a request is on the wire:\n\n| Where the abort lands                        | What happens                                                                              |\n| -------------------------------------------- | ----------------------------------------------------------------------------------------- |\n| Before the call starts                       | Rejects without contacting Google Play; no `onRequest` event is emitted                   |\n| During the request or the body read          | Rejects with the signal's reason, not a wrapped `HttpError`                               |\n| During retry backoff or a `Retry-After` wait | Rejects before the delay expires; no further attempt is made                              |\n| While queued behind a `throttle` slot        | Rejects at once; the slot is released and the other queued calls keep the configured rate |\n\nThe reason you pass to `abort()` is the value that reaches you, by identity, so a sentinel object survives the round trip:\n\n```typescript\nimport { search } from '@mradex77/google-play-scraper';\n\nconst budgetExceeded = { reason: 'budget-exceeded' };\nconst controller = new AbortController();\nsetTimeout(() => controller.abort(budgetExceeded), 5000);\n\ntry {\n  await search({ term: 'panda', num: 250, requestOptions: { signal: controller.signal } });\n} catch (error) {\n  if (error !== budgetExceeded) {\n    throw error;\n  }\n}\n```\n\nCalling `abort()` with no argument produces the standard `AbortError` `DOMException`. A per-request timeout is separate: `timeoutMs` ends one attempt and may still be retried, while your own abort is terminal.\n\nA custom `fetchImpl` should honor `init.signal` so an aborted request is cut short on the wire. One that ignores it cannot be interrupted mid-request, but the call still rejects with your reason as soon as that request settles, and its body is discarded.\n\n## Resilience\n\nGoogle Play serves its data as deeply nested, unlabeled arrays whose positions shift a few times a year. That is what breaks scrapers. Every positional path in this library lives as a typed constant in a `specs.ts` file per feature, never inline in logic, and each field is resolved through an ordered list of candidate paths so a single moved index does not take the whole call down. Extraction collects all field failures in one pass and throws a single `SpecError` naming every broken field and the paths that were tried, which is exactly the input the maintenance runbook needs. Unknown data enters as `unknown` and only leaves through a zod schema, so a layout change fails loudly at the boundary rather than three layers up.\n\nTo catch breakage before users do, the `e2e/` suite runs against live Google Play on a daily GitHub Actions schedule and opens a labeled issue on failure. Repairing a break is a spec diff confined to one file, walked through step by step in [docs/RUNBOOK.md](docs/RUNBOOK.md).\n\n## Monitoring drift\n\nCluster pagination degrades gracefully: when a continuation page from Google stops parsing, the call keeps everything collected so far and returns instead of throwing. The `onDegradation` callback makes every swallowed continuation failure observable. The separate `onIntegrityEvent` callback reports routing fallbacks, skipped best-effort sections, and pagination token cycles without widening the existing `DegradationEvent` contract.\n\nThe option exists on every method for uniformity, but only the methods that paginate through the cluster endpoint can emit: `search`, `searchIterator`, `similar`, `developer`, and `developerIterator`. Each event carries the feature `context`, the machine readable `reason` (`'cluster-page-parse'`), and the underlying `ParseError`:\n\n```typescript\nimport { search, type DegradationEvent } from '@mradex77/google-play-scraper';\n\nconst onDegradation = (event: DegradationEvent): void => {\n  metrics.increment('gplay.degradation', {\n    context: event.context,\n    reason: event.reason,\n  });\n};\n\nconst results = await search({ term: 'panda', num: 100, onDegradation });\n```\n\nA degraded call still resolves with the pages that parsed, so wire the callback to a metrics counter or log sink rather than treating it as an error path. If the callback itself throws, the error surfaces to the caller unchanged.\n\nIntegrity events use the same shape but a separate type and callback:\n\n```typescript\nimport { app, type IntegrityEvent } from '@mradex77/google-play-scraper';\n\nconst onIntegrityEvent = (event: IntegrityEvent): void => {\n  metrics.increment('gplay.integrity', {\n    context: event.context,\n    reason: event.reason,\n  });\n};\n\nconst details = await app({\n  appId: 'com.google.android.apps.translate',\n  onIntegrityEvent,\n});\n```\n\n| Callback           | Reason                    | Meaning                                                                |\n| ------------------ | ------------------------- | ---------------------------------------------------------------------- |\n| `onDegradation`    | `cluster-page-parse`      | A cluster continuation failed to parse and collected results returned. |\n| `onIntegrityEvent` | `rpc-anchor-fallback`     | An RPC anchor used its validated absolute fallback.                    |\n| `onIntegrityEvent` | `optional-section-parse`  | A present best-effort section failed to parse and was skipped.         |\n| `onIntegrityEvent` | `pagination-token-cycle`  | A repeated token stopped pagination before a duplicate request.        |\n| `onIntegrityEvent` | `section-anchor-fallback` | A best-effort section resolved outside its declared anchor.            |\n\nTwo boundaries to know:\n\n- An empty continuation page emits nothing: that is the normal end-of-results signal and is indistinguishable from exhaustion.\n- `app` emits `optional-section-parse` with context `app comments` when the available comment roots are structurally invalid. A valid empty comment root returns an empty list without an event. Treat a rising event rate as a signal, not each event.\n- `reviews` pagination never swallows a parse failure. Malformed review pages reject with `ParseError`, while a repeated token stops safely and emits `pagination-token-cycle`.\n\nWith `memoized()`, callbacks never affect which entry a call hits. Degradation and integrity events are recorded with the entry and replayed, in order, to the callbacks of every call that hits it, so a cached degraded result is reported as degraded to each caller. Lifecycle hooks are not replayed, because a hit performs no request. See [memoized](#memoized).\n\n## Versioning and API stability\n\nThis package follows [Semantic Versioning](https://semver.org). For a scraper the contract needs one clarification: semver covers the code surface this library controls, not the data Google serves.\n\nIntegrity diagnostics use the additive API choice (Option B): the existing `DegradationEvent.reason` remains exactly `'cluster-page-parse'`, and the four new reasons live on the opt-in `onIntegrityEvent` callback. This keeps exhaustive switches over `DegradationEvent.reason` and narrowly typed `onDegradation` handlers intact while adding observability without a major release. `IntegrityReason` itself is a widening union, so a new reason is a minor release and an exhaustive switch over it needs a new arm.\n\n| Change                                                            | Release |\n| ----------------------------------------------------------------- | ------- |\n| Removing or renaming an exported function, option, or constant    | major   |\n| Removing a field from a result schema, or changing its type       | major   |\n| Raising the Node.js support floor or dropping a module format     | major   |\n| Adding a new method, option, or optional result field             | minor   |\n| Adding a reason to `IntegrityReason`                              | minor   |\n| Restoring extraction of a field after a Google Play layout change | patch   |\n\nWhat semver cannot cover is the content behind those shapes. Google Play changes its markup a few times a year, and a field can start coming back `undefined`, empty, or degraded without any release of this package. The policy for that case:\n\n- The daily live test suite fails and an issue is filed automatically, usually before user reports arrive.\n- When extraction can be restored, the fix ships as a patch.\n- When Google removes the underlying data permanently, the field stays in the schema as a documented degraded surface first (see [categories](#categories) for the worked example) and is only removed in the next major.\n\nTo watch for drift in your own production use, wire up [`onDegradation`](#monitoring-drift) and treat [`SpecError`](#error-handling) as a layout-change signal rather than an application bug.\n\n## Migrating from google-play-scraper\n\nThe method names, options, and constants are the same, so most code keeps working after swapping the import. Watch for these differences:\n\n- `reviews` always returns the `{ data, nextPaginationToken }` envelope, never a bare array.\n- Dates are ISO 8601 strings (review `date`, `replyDate`), and `updated` is a millisecond timestamp.\n- Errors are the typed classes above instead of plain `Error`.\n- The package is ESM first with a CommonJS build; the default export is the aggregate client and named exports are also available.\n\n### Deriving from the exported schemas\n\nEvery exported schema is a [zod/mini](https://zod.dev/packages/mini) schema. `.parse` and `.safeParse` behave exactly like their classic zod counterparts, but transform methods such as `.extend`, `.pick`, and `.omit` live as top-level functions on the `zod/mini` entry of the same `zod` package:\n\n```ts\nimport * as z from 'zod/mini';\nimport { appSchema } from '@mradex77/google-play-scraper';\n\nconst slimAppSchema = z.pick(appSchema, { title: true, appId: true, score: true });\n```\n\nThe zod/mini surface is what keeps the library small in your bundle. Measured with `esbuild --bundle --minify --format=esm --platform=node` against zod 4.4.3:\n\n| Bundle             | Minified | Gzip    |\n| ------------------ | -------- | ------- |\n| Classic zod (v0.4) | 414.8 kB | 85.8 kB |\n| zod/mini (current) | 115.1 kB | 29.9 kB |\n\nInstall size on disk is unchanged by design: `zod/mini` is a subpath export of the same `zod` package, so `node_modules` stays identical while the consumer bundle shrinks about 3.6×.\n\n## FAQ\n\n### Is there an official Google Play API for app data?\n\nNo. Google does not offer a public API for store listings, search results or reviews. Libraries like this one fill that gap by scraping the public web pages and internal endpoints that power play.google.com. This is the standard approach for ASO tools, market research and academic studies.\n\n### How do I scrape Google Play reviews for an app?\n\nCall [reviews](#reviews) with the app id. Use `paginate: true` and the returned `nextPaginationToken` to walk through all pages. Combine it with `throttle` to stay under the rate limits.\n\n### How do I avoid getting rate limited or blocked?\n\nSet the `throttle` option to cap requests per second, keep the default retry behavior, and reuse results through the [memoized](#memoized) client. If you run large jobs, spread them out over time or [route them through a proxy](#routing-requests-through-a-proxy). A `RateLimitError` or `BlockedError` tells you exactly when Google started pushing back, and [rotating proxies](#rotating-proxies-when-blocked) shows how to react to a block.\n\n### Does this library work in the browser?\n\nNo. It targets Node.js 22.12 and newer. Browsers cannot scrape Google Play anyway because of CORS restrictions.\n\n### Is this library affiliated with Google?\n\nNo. It is an independent open source project. All app data belongs to its respective owners, and you are responsible for using it in compliance with the Google Play Terms of Service and applicable law.\n\n## Related projects\n\n- [fa","readmeFilename":"README.md"}