Skip to main content

app-reviews

Scrape App Store and Google Play reviews in Python with typed sync and async clients for review ingestion, app search, metadata lookup, and resumable cursors. Use public sources without credentials, or supply store credentials for the official review APIs.

PyPI Python CI E2E License

Documentation · Comparison · FAQ · Changelog

from app_reviews import AppStoreReviews, Country

with AppStoreReviews() as client:
    result = client.fetch(
        "324684580",  # numeric Apple app ID from its App Store URL
        countries=[Country.US, Country.GB],
        limit=20,
        max_pages=2,
    )

for review in result:
    print(review.country, review.rating, review.body[:80])

if result.errors:
    print(result.errors)

Why use it?

  • One typed model for Apple App Store and Google Play reviews.
  • Sync and native async methods for fetch, paging, search, and lookup.
  • Resumable cursors, streaming iterators, request budgets, retry policy, and connection pooling.
  • Partial failures remain visible through typed errors and per-source outcomes.
  • A complete JSON-safe result envelope for automation.
  • Search and metadata lookup for both stores.
  • Python 3.11+ with py.typed; runtime dependencies are httpx and cryptography.

The public endpoints are unofficial and can be throttled or changed by the stores. Authenticated sources require an Apple Developer or Google Play Developer account. See source capabilities before choosing a source.

Install

pip install app-reviews

Or:

uv add app-reviews

Fetch reviews

Apple App Store

The credential-free client uses Apple's public RSS feed. Reviews are partitioned by storefront, so countries= controls which storefronts are fetched.

from app_reviews import AppStoreReviews, Country

with AppStoreReviews() as client:
    result = client.fetch(
        "324684580",
        countries=[Country.US, Country.DE],
        limit=100,
        max_pages=5,
    )

Use the numeric Apple app ID—the number after /id in an App Store URL. Search and lookup also accept the identifiers documented in the Python API guide.

Google Play

Google Play reviews form one global corpus. A review response does not include a reviewer country, so Review.country is None; a store presentation country is not a review filter.

An explicit empty or all-blank countries collection is a no-op and makes no requests on any review client. Otherwise, Google Play review clients reject any nonblank country or countries selection before network I/O because Play reviews are global. Google Play search and metadata still accept country to select the storefront used for availability, presentation, and price.

from app_reviews import GooglePlayReviews

with GooglePlayReviews() as client:
    result = client.fetch(
        "com.spotify.music",
        limit=100,
        max_pages=5,
    )

Use the package name from the id query parameter in a Google Play URL.

Source behavior

Client configuration Source Review country behavior Credentials
AppStoreReviews() Apple RSS One corpus per requested storefront No
AppStoreReviews(auth=...) App Store Connect Global request; territory may be present on each review Yes
GooglePlayReviews() Google Play web Global; country=None No
GooglePlayReviews(auth=...) Google Play Developer API Global; country=None Yes

Review.language is populated only when a provider reports a language. The Google Play Developer API can report reviewerLanguage; do not interpret that as reviewer location. Google Play web reviews have no title, while the Developer API may expose a legacy title embedded in its review text.

Search and app metadata

Search and lookup are credential-free. Here, country selects the storefront used for availability, presentation, and price; it still does not identify a reviewer's country.

from app_reviews import AppStoreSearch, Country, GooglePlaySearch

with AppStoreSearch() as apple:
    ios_apps = apple.search("fitness tracker", country=Country.US, limit=5)

with GooglePlaySearch() as play:
    android_apps = play.search("fitness tracker", country=Country.US, limit=5)

print([app.name for app in ios_apps + android_apps])

Search returns list[AppMetadata]; lookup returns AppMetadata | None. AppMetadata.release_notes carries the current version's "What's New" text: on every App Store result, and from Google Play lookup().

App Store version history

The iTunes APIs report only the current version. version_history() reads the "Version History" from the public App Store product page instead, newest first:

from app_reviews import AppStoreSearch

with AppStoreSearch() as apple:
    for entry in apple.version_history("324684580"):
        print(entry.version, entry.released_at.isoformat(), entry.release_notes)

Each AppVersionEntry has version, released_at (timezone-aware UTC), and release_notes. It takes the numeric app ID and goes through the client's proxy=, retry=, and rate_limiter= settings. An app the store does not have, or a page with no history, returns []; a history that cannot be read raises ParseError. aversion_history() is the async twin.

Scraped source. version_history() reads the public product page, not an API. It is best-effort and App Store only, and it may break when Apple changes the page. An official source from App Store Connect is planned for 1.2.0.

Results and errors

fetch() returns FetchResult, which is iterable over Review objects. It also records one CountryOutcome for each source walk. A fetch may contain reviews and errors at the same time.

for outcome in result.outcomes:
    print(
        outcome.country,
        outcome.pages,
        outcome.reviews_fetched,
        outcome.skipped_reviews,
        outcome.stopped_because,
    )

for error in result.errors:
    if error.retryable:
        schedule_retry(error)

stopped_because="exhausted" means the source reported no next page. Reasons such as limit, since, and max_pages mean the client stopped while more data may exist. Malformed records that can be isolated are counted as skipped_reviews; malformed page envelopes are parse errors.

Apple's RSS JSON feed sometimes answers with no entries while its XML feed for the same page has them. The package then reads the XML feed, through the same client and settings, and records it: outcome.feed_format is "xml" (else "json", and None for other sources). Both empty is a normal "exhausted".

fetch() retains partial failures as data. Search and lookup are single-request operations and raise typed exceptions such as RateLimitError, RequestError, NotFoundError, and ParseError.

JSON, JSONL, and CSV

Use result.to_dict() when diagnostics matter. It returns the full JSON-safe envelope: reviews, outcomes, errors, and skipped_reviews. Use result.to_dicts() only when review rows are all you need. Provider raw payloads are omitted unless include_raw=True.

import json

payload = result.to_dict()
print(json.dumps(payload, indent=2))

Guard an empty result before deriving CSV headers:

import csv

rows = result.to_dicts()
if rows:
    with open("reviews.csv", "w", newline="", encoding="utf-8") as stream:
        writer = csv.DictWriter(stream, fieldnames=list(rows[0].keys()))
        writer.writeheader()
        writer.writerows(rows)

Agents and automation

Agents that can call Python functions can use the library directly; an extra package interface is not required. Keep the function bounded and return the complete envelope so the caller can distinguish an empty success, a partial result, and a provider failure.

from typing import Any

from app_reviews import AppStoreReviews, Country


def fetch_app_store_reviews(app_id: str, country: str = "us") -> dict[str, Any]:
    """Fetch at most 50 reviews across at most two provider pages."""
    with AppStoreReviews() as client:
        result = client.fetch(
            app_id,
            countries=[Country(country.lower())],
            limit=50,
            max_pages=2,
        )
    return result.to_dict()

Register that function with the agent framework used by your application. Keep authentication, allowlists, timeouts, retry decisions, and user authorization in the application layer. The package itself is a Python library and does not run a network service or accept prompts.

Async and streaming

Every network entry point has an async twin. Use iter_reviews() / aiter_reviews() to avoid buffering a multi-storefront fetch, or iter_pages() / aiter_pages() when you need to persist next_cursor.

import asyncio

from app_reviews import GooglePlayReviews


async def main() -> None:
    async with GooglePlayReviews() as client:
        result = await client.afetch(
            "com.spotify.music",
            limit=100,
            max_pages=3,
        )
    print(len(result), result.errors)


asyncio.run(main())

For Apple RSS, multi-country fetch() caps default fan-out at eight workers. Pass concurrency= to choose a smaller or larger explicit limit.

Sharing a rate limit across fetches

concurrency= paces one fetch. When one process fetches many apps, pass the same RateLimiter to every client instead, so they share one request budget. It is thread-safe and asyncio-safe, and every attempt, retries included, takes a token.

from concurrent.futures import ThreadPoolExecutor

from app_reviews import AppStoreReviews, RateLimiter

limiter = RateLimiter(rate=2.0, burst=4)  # two requests per second, bursts of four


def fetch(app_id: str):
    with AppStoreReviews(rate_limiter=limiter) as client:
        result = client.fetch(app_id, countries=["us", "gb", "de"], max_pages=2)
    throttled = [error.country for error in result.errors if error.kind == "rate_limited"]
    return result, throttled


with ThreadPoolExecutor(max_workers=16) as pool:
    results = list(pool.map(fetch, ["324684580", "310633997"]))

When a store throttles (HTTP 429, or the HTTP 403 Apple's RSS feed answers while it blocks an address), the limiter pauses every client sharing it: for the server's Retry-After if it sent one, else 30 seconds, doubling on each consecutive throttled answer up to 15 minutes. A successful answer resets the doubling. Tune it with RateLimiter(rate, burst, initial_penalty=30.0, max_penalty=900.0), or pause it yourself with limiter.penalize(seconds).

Any object with acquire(), aacquire(), and record(status, retry_after) satisfies the RequestLimiter protocol and can be passed instead, for a budget shared across processes. record runs once per response.

Throttled storefronts fail alone: other countries keep their reviews, and each throttled country carries FetchError(kind="rate_limited", retryable=True) in its CountryOutcome. The package does not retry those 403s itself, because requests made during a block extend it; fetch the throttled countries again later through the same limiter.

Official APIs

Pass AppStoreAuth to use App Store Connect or GooglePlayAuth to use the Google Play Developer API. The authenticated APIs only expose apps owned by the credential holder and have different history, ordering, and field behavior.

from app_reviews import AppStoreAuth, AppStoreReviews

auth = AppStoreAuth(
    key_id="ABC123DEF4",
    issuer_id="12345678-1234-1234-1234-123456789012",
    key_path="/path/to/AuthKey.p8",
)

with AppStoreReviews(auth=auth) as client:
    result = client.fetch("324684580", limit=100, max_pages=3)

See authentication for Google credentials and setup details.

Limits worth knowing

  • Apple RSS exposes roughly 500 recent reviews per storefront and an empty feed cannot distinguish no reviews, an unknown app, and some upstream throttling.
  • The Google Play Developer API returns reviews created or modified within the last seven days; this package does not read Play Console CSV exports.
  • Google Play's public web endpoint is undocumented and can change without notice.
  • Store terms and applicable law depend on how and where you use the data. Review them for your use case.

Read source capabilities, the comparison, and the FAQ for details.

Development

git clone https://github.com/0xfirattamur/app-reviews.git
cd app-reviews
make install
make all

See CONTRIBUTING.md and SECURITY.md.

Acknowledgements

The Google Play web parsing is based on field-path knowledge from google-play-scraper, reworked on this project's HTTP, retry, paging, and model layers.

License

MIT

Metadata

Release files for app-reviews 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for app-reviews 1.1.0
File Size Uploaded
app_reviews-1.1.0.tar.gz 340.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for app-reviews 1.1.0
File Interpreter ABI Platform
app_reviews-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 435.1 kB

Release files / app_reviews-1.1.0.tar.gz

Download URL app_reviews-1.1.0.tar.gz
Size 340.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5567f0e46ed98dc9359d2b5486a1481c246858bb6f7d100a10f3199bda37b528
BLAKE2b-256 checksum
How to use checksums
6e3002c72afbde9c26e12c71e23a0e30f3ebc09bd9796d0a802dca7047b0a5aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / app_reviews-1.1.0-py3-none-any.whl

Download URL app_reviews-1.1.0-py3-none-any.whl
Size 95.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c20bbcbed469081521ef5d779b298f36650f94c7d63c4ad7d5f4d906f7c8ab4e
BLAKE2b-256 checksum
How to use checksums
f060d00aee67fca7d57388583b9a0b7f6c309ff314a23bc3c4afdf7ec631eb39
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

1.2.0

2 release files

This release

1.1.0 This release

2 release files

1.0.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page