App reviews are free user research. People write them unprompted, they mention the app version, and on Google Play the developer can answer in public. The hard part is getting them out of the store in a shape you can sort and count.
This walkthrough uses the App Store & Google Play Reviews Scraper on Apify. I ran it on Duolingo, US store, only 1 and 2 star reviews with at least 40 characters of text, capped at 25 reviews. Everything below comes from that one run.
What came back from one small run
The run took about 19 seconds. The status line said: 25 reviews, 2 app records, 159 filtered out. So the scraper read 184 recent reviews and kept the 25 that matched my rules. You only pay for the 25.
A few things I noticed in those 25 reviews:
- 13 were 1 star, 12 were 2 star. The sentiment label said "negative" for all 25.
- They were all posted on November 5, 2026, within roughly five hours. Duolingo gets a lot of reviews.
- 11 reviews came from version 6.103.6 and 9 from 6.104.5. Three had no version at all.
- None had a developer reply yet. That makes sense for reviews a day old.
- 7 of 25 mentioned streaks, 5 mentioned AI, 3 mentioned ads, 3 mentioned price. One person described a refund problem after upgrading to Duolingo Max.
- Median review length was 351 characters. These are not one-word rants.
The app records are useful context. Google Play listed Duolingo at 4.75 stars from 51,650,385 ratings, 500M+ installs, last updated 2026-11-03. The App Store record showed 4.72 stars from 5,565,332 ratings and version 7.147.1.
That contrast is the reason to filter. A 4.75 average tells you nothing about what annoys people this week. Twenty-five recent low ratings, grouped by version, do.
Fields you get
Each review is one row. App details, if you turn them on, are separate rows with recordType: "app".
| Field | What it holds |
|---|---|
recordType |
review or app
|
store |
google_play or app_store
|
appId, appName
|
Package name or Apple ID, plus the store title |
country, language
|
Store country and review language |
rating |
1 to 5 stars |
sentiment |
negative, neutral or positive
|
title, text
|
Review title (App Store) and body |
date |
When the review was posted (ISO timestamp) |
appVersion |
Version the reviewer had installed |
thumbsUp |
"Helpful" votes |
developerReply, developerReplyDate
|
The public answer, Google Play only |
url |
Direct link to the review |
userName |
Reviewer display name. Treat as personal data (see the last section) |
App rows carry developer, rating, ratingsCount, installs (Google Play), price, category, contentRating, updated, icon and url.
Input options that matter
You can paste store links, an app ID like com.duolingo, or just a name like Duolingo. A name is looked up in both stores. There is also searchTerms, which takes a keyword such as meditation and scrapes the top N apps for it (maxAppsPerSearch, default 5).
The rest is filtering:
-
countries: two-letter codes. Reviews are collected per country, so["us", "gb", "de"]gives three separate sets. -
maxReviewsPerApp: per app and per country, counted after filters. 0 means everything available. -
sort:newest,most_relevantorrating. -
minRating/maxRating: setmaxRating: 2for complaints only. -
reviewsSince: a date, handy for weekly reports. -
keywords: keep reviews that mention any of these words, likecrashorlogin. -
appVersion: version prefix such as6.104. This is the one I use most for release checks. -
developerReply:with,withoutorany.withoutgives you a support queue. -
minThumbsUpandminTextLength: drop noise. -
onlyNewReviewsplusmonitorId: for scheduled runs, return only reviews you have not seen before.
Two limits from the README. Apple publishes only the latest 500 reviews per app and country, so add countries if you need more. Developer replies exist only on Google Play.
Option 1: no code
- Open the actor page and click Try for free.
- Under Apps, type
Duolingoor paste a Play Store link. - Pick the US in Countries and set Max. reviews per app and country to 25.
- Set Max. rating to 2 and Min. review length to 40.
- Leave Include app details on if you want the rating and install numbers.
- Click Start. When it finishes, open the dataset and export to CSV, Excel or JSON.
On a free Apify account this is enough to test a hypothesis in a couple of minutes.
Option 2: Python with apify-client and pandas
Install the two packages:
pip install apify-client pandas
Then run the actor and load the reviews into a DataFrame. Put your Apify token in an environment variable, not in the script.
import os
from apify_client import ApifyClient
import pandas as pd
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"apps": ["com.duolingo"],
"countries": ["us"],
"language": "en",
"maxReviewsPerApp": 25,
"sort": "newest",
"maxRating": 2,
"minTextLength": 40,
"includeAppDetails": True,
}
run = client.actor("liveclaude/app-reviews-scraper").call(run_input=run_input)
items = client.dataset(run["defaultDatasetId"]).list_items(clean=True).items
df = pd.DataFrame(items)
reviews = df[df["recordType"] == "review"].drop(columns=["userName", "userImage"], errors="ignore")
apps = df[df["recordType"] == "app"]
print(apps[["store", "appName", "rating", "ratingsCount", "installs", "updated"]])
# Low ratings per app version
print(reviews.groupby("appVersion", dropna=False)["rating"].agg(["count", "mean"]))
# Which topics come up (also matches plurals)
topics = ["streak", "ai", "ads", "price", "subscription", "refund"]
text = reviews["text"].str.lower()
counts = {t: int(text.str.contains(rf"\b{t}s?\b", regex=True).sum()) for t in topics}
print(counts)
reviews.to_csv("duolingo_negative_reviews.csv", index=False)
On my run the version table showed 11 rows for 6.103.6 and 9 for 6.104.5, and the topic counter gave streak 7, ai 5, ads 3, price 3. Note that I drop userName and userImage right after loading. You rarely need them for analysis.
If you want to compare competitors, put several apps in apps and add appName to the groupby. If you want to compare markets, add countries and group by country.
Scheduling and integrations
For ongoing monitoring, save the input as a task and add a schedule, for example daily at 07:00. Turn on onlyNewReviews and give the monitor a name like duolingo-negative. Each run then returns only new reviews, and the README says reviews already returned are not charged again.
From there you can send the dataset to Google Sheets, post a Slack message through a webhook, or hook it into Make, Zapier or n8n. A setup I like: daily run, maxRating: 2, developerReply: "without", results posted to a support channel.
Pricing
The actor uses pay-per-event pricing, platform usage included. Current prices:
| Event | Free | Starter | Scale | Business |
|---|---|---|---|---|
| 1,000 reviews | $0.20 | $0.18 | $0.165 | $0.15 |
| App details (per app and country) | $0.002 | $0.002 | $0.002 | $0.002 |
There is also a start fee of $0.00005 per GB of memory. My run produced 25 reviews and 2 app records, so on the Free tier that is about $0.005 for reviews plus $0.004 for app details. Reviews dropped by filters are not charged.
Responsible use
Reviews are public, but the people who wrote them are private individuals. Their display names and avatars count as personal data under GDPR. Drop those columns unless you have a clear reason to keep them, and never publish them. Quote review text without names, as I did above. Keep request volumes reasonable and follow the stores' terms.
This tutorial was written with AI assistance and checked by the author. All numbers come from a real run of the scraper.


Top comments (0)