Changelog
All notable changes to app-reviews are recorded here. Release details for
recent versions are also kept in
.github/release-notes.
Unreleased
Documentation
- Correct outdated reply support, client inventory, authentication setup, token refresh, pagination budgets, filtering, and connection-pooling descriptions.
- Include replies and version history in the feature overviews and align model reference fields with the public API.
1.2.0 - 2026-09-28
Reply to reviews on both stores, list App Store versions from App Store Connect, and pass credentials without a key file. Backward compatible.
Added
AppStoreAuth(key_id, issuer_id, key_path=None, private_key=None)takes the.p8either as a path or as PEM text, andGooglePlayAuth( service_account_path=None, service_account_info=None)takes the service account either as a path or as the parsed JSON mapping. Each needs exactly one of its two sources (ValueErrorotherwise); neither shows the key inrepror in the frames of a validation error.AppStoreReplies(auth)withreply(),get_reply(), anddelete_reply(), andGooglePlayReplies(auth)withreply(..., package_name=)andget_reply(..., package_name=), each with an async twin. They create or replace, read, and (App Store only) delete the developer reply through App Store ConnectcustomerReviewResponsesand the Play Developer APIreviews.reply/reviews.get.ReviewReply(review_id, reply_id, text, state, updated_at)andReplyState = Literal["published", "pending"]. Apple can keep a reply"pending"for up to 24 hours.ReplyRejectedError(reason), aRequestError: the store refused the reply, or a Play reply was over 350 characters (reason="too_long", raised before sending).ReplyOutcomeUnknownError, anHttpError: a write timed out, lost its connection, or got a 5xx, so it may or may not have taken effect.RateLimitError.retry_after, the wait in seconds a 429 asked for, on reads and writes alike.AppStoreVersions(auth).versions(app_id)/aversions(), returninglist[AppStoreVersion]from App Store ConnectappStoreVersions, withwhatsNewper locale inrelease_notes. The official API has no release date:created_at(createdDate) andearliest_release_date(earliestReleaseDate) are named for what they are,stateisappVersionState, andAppStoreSearch.version_history()remains the source for release dates.HttpClient.send_once(method, url, body=, headers=)/asend_once(): one attempt, never retried and never redirected, for a request that must not be applied twice.HttpResponse.retry_aftercarries the final attempt'sRetry-After.
Changed
- Reply writes are never retried, whatever
RetryConfigsays, and never follow a redirect, since following a 307/308 re-sends the write; a 3xx raisesReplyOutcomeUnknownError. Reads, token exchanges, and every other request keep the normal retry policy. - A 429 from the Google token exchange now carries
RateLimitError.retry_after. GoogleAuthalso takesservice_account_info=;service_account_path(by position or keyword) works as before, and exactly one of the two is required.
Fixed
- A service-account file that is truncated or not UTF-8 no longer leaves its
contents, private key included, in the frames of the raised
AuthError. A.p8or service-account file that is not UTF-8 raisesAuthErrorinstead ofUnicodeDecodeError.
See the v1.2.0 release notes.
1.1.0 - 2026-09-28
A shared rate limit for processes that fetch many apps from one address, App
Store release history with exact dates, and App Store reviews read from the XML
feed when the JSON feed comes back empty. Backward compatible except for one
reclassification: an App Store RSS 403 is now a retryable rate_limited
failure instead of request (see Changed). The XML fallback also applies
without opting in. Otherwise, omitting the new parameters keeps 1.0.0 behavior.
Added
RateLimiter(rate, burst=1, *, initial_penalty=30.0, max_penalty=900.0), a thread-safe and asyncio-safe token bucket one process can share across every client, thread, and task.penalize(seconds)pauses every holder.rate_limiter=onHttpClient,AppStoreReviews,GooglePlayReviews,AppStoreSearch, andGooglePlaySearch. Every attempt, retries included, takes a token. A 429, or a 403 from a credential-free request, pauses the limiter forRetry-After, else for 30 seconds doubling per consecutive throttled answer, capped atmax_penalty; a success resets the doubling. Passing it alongsidehttp=raisesTypeError, likeproxy=andretry=.RequestLimiter, the protocolrate_limiter=accepts:acquire(),aacquire(), andrecord(status, retry_after), whichHttpClientcalls once per response (not for a transport failure, nor for a 403 on a credentialed request).RateLimiterimplements it; any other object with those methods, such as a limiter shared across processes, can be passed instead.AppStoreSearch.version_history(app_id, *, country="us")andaversion_history(), returning the app's App Store "Version History" aslist[AppVersionEntry], newest first. Read from the public product page through the client'sHttpClient, soproxy=,retry=, andrate_limiter=apply. An unknown app (HTTP 404) or a page without a history returns[]; a history that cannot be read raisesParseError. This is a scraped source: best-effort, 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.AppVersionEntry(version, released_at, release_notes), a frozen dataclass exported fromapp_reviews.released_atis timezone-aware UTC, andrelease_notesis named likeAppMetadata.release_notes.AppMetadata.release_notes, the current version's "What's New" text: from iTunesreleaseNoteson every App Store result, and from the Google Play detail page onlookup()and the featured search hit.Nonewhen absent.PageResult.feed_formatandCountryOutcome.feed_format("json","xml", orNone), also in bothto_dict()envelopes, and theFeedFormattype. They say which App Store RSS feed answered;Nonefor other sources.
Fixed
- App Store RSS pages the JSON feed answers 200 with no entries, or with an
unreadable body, are asked again from the same page's XML (Atom) feed, and
its entries are used when it has any. Duolingo, Spotify, Facebook, and
Instagram storefronts have been seen answering an empty JSON page 1 while the
XML page held 50 reviews, which 1.0.0 reported as an exhausted storefront.
The XML request goes through the same
HttpClient, so retry, proxy, and rate limiter apply. It is made on page 1, and on a later page unless the previous page was short. Both feeds empty is still a normal"exhausted"; an unreadable XML body leaves the JSON answer standing; a failed XML request is reported as the page'sFetchError.
Changed
- An App Store RSS 403 is now
FetchError(kind="rate_limited", retryable=True, status=403)instead of a non-retryablerequestfailure. The feed answers 403 while it throttles an address. Credentialed endpoints keep 403 asauth, and the 403 is not retried inside the package.
See the v1.1.0 release notes.
1.0.0 - 2026-09-22
The first stable API release.
Added
- Complete
FetchResult.to_dict()envelopes with reviews, outcomes, errors, retryability, stop reasons, skipped-record counts, and optional raw payloads. - Explicit
max_pagesrequest budgets across buffered and streaming review fetches. - Context management plus public protocol and transport types for typing, lower-level use, and direct built-in provider integration. Custom providers cannot be injected into the high-level clients or paging engine in v1.
- Root exports for
HttpResponse,ReviewProvider,TokenSource, and the new non-retryableRequestErrorclassification. - Reviewer language and legacy-title mapping for Google Play's official API.
Changed
- Review IDs are required. Naive timestamps get UTC attached; already-aware offsets are preserved.
- Date-only
untilfilters include the complete UTC day. - The default multi-storefront concurrency is capped at eight.
- Negative limits raise; zero limits and explicit empty or all-blank country collections make no requests.
- Google Play rejects any nonblank review-country selection before network I/O; its search and metadata storefront selector remains supported.
- Permanent HTTP client errors are non-retryable and RSS access blocks receive a source-appropriate classification.
- Google Play prices retain the storefront's formatted currency.
- Provider pages report malformed records instead of silently losing them.
Security
- App Store Connect pagination cursors are bound to the expected review endpoint and app before a credential-bearing request is sent.
See the v1.0.0 release notes for migration instructions.
0.6.0 - 2026-08-02
Introduced the page/cursor ladder, native async entry points, typed outcomes, pooled connections, bounded retry behavior, and a redesigned public review API. This was a breaking prerelease. See the v0.6.0 release notes.
0.5.0 - 2026-07-29
Removed the terminal UI and CLI, made the package library-focused, and corrected review identifiers. See the v0.5.0 release notes.
0.4.0 - 2026-04-09
Added the earlier unified review/search API and expanded tests and packaging.
0.3.1 - 2026-04-08
Corrected prerelease packaging and provider behavior.
0.3.0 - 2026-04-08
Expanded store search and review fetching capabilities.
0.2.1 - 2026-04-07
Corrected metadata and distribution details.
0.2.0 - 2026-04-07
Added the first cross-store public API.
0.1.1 - 2026-04-04
Corrected initial package behavior and metadata.
0.1.0 - 2026-04-04
Initial release.