DEV Community

Gio Rich
Gio Rich

Posted on

Send new 1-star App Store and Google Play reviews to Slack with Python

I built this actor; it's a paid tool on Apify with a free trial credit.

App Store and Google Play reviews are where users tell you a release broke something, usually before your crash reporter makes it obvious. But the two stores have different formats, the App Store's public feed stops early, and nobody wants to check both consoles every morning.

I'm an 18-year-old engineering student, and I built App Store & Google Play Reviews Scraper to pull both stores into one table with the same field names. This post covers the Python and Node calls, the output, and a daily Slack alert for new 1- and 2-star reviews.

How it works

It reads the stores' public web data over plain HTTP, with no headless browser and no login. App metadata comes from Apple's public iTunes lookup and from the data embedded in the Play app page. Reviews come from the public endpoints each store's website uses, 200 per request. In testing, 20,000 reviews (Spotify and Uber, both stores) took under 30 seconds, with 0 failed requests and 0 duplicates.

Python example

pip install apify-client
Enter fullscreen mode Exit fullscreen mode
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("rel8ble/app-reviews-scraper").call(run_input={
    "apps": [
        "https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580",
        "com.spotify.music",
    ],
    "country": "us",
    "sort": "newest",
    "maxReviewsPerApp": 200,
})

for r in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(r["store"], r["rating"], r["reviewDate"][:10], (r["text"] or "")[:60])
Enter fullscreen mode Exit fullscreen mode

You can mix App Store URLs or numeric IDs (324684580) with Play URLs or package names (com.spotify.music) in one list.

Node example

import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: "<YOUR_APIFY_TOKEN>" });

const run = await client.actor("rel8ble/app-reviews-scraper").call({
  apps: ["com.spotify.music"],
  country: "gb",
  language: "en",
  sort: "mostHelpful",
  maxReviewsPerApp: 1000,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const replied = items.filter((r) => r.developerReply).length;
console.log(`${replied} of ${items.length} reviews have a developer reply`);
Enter fullscreen mode Exit fullscreen mode

What comes back

One row per review; both stores share the same fields, and anything a store doesn't have is null. A real Google Play row, trimmed:

{
  "store": "googlePlay",
  "appId": "com.spotify.music",
  "appName": "Spotify: Music and Podcasts",
  "country": "us",
  "reviewId": "1e96015e-4577-4274-b4de-f71e0a4eae8e",
  "rating": 5,
  "title": null,
  "text": "The app has has gotten better with music suggestions, but recently podcast video episodes have trouble playing.",
  "reviewDate": "2026-09-23T05:14:13.000Z",
  "reviewerName": "Hennessy",
  "version": "9.1.82.2161",
  "thumbsUp": 4,
  "developerReply": "Hi there! Have you tried a quick reinstall? ...",
  "developerReplyDate": "2024-01-03T12:49:34.000Z",
  "appRating": 4.35,
  "appRatingCount": 36360224,
  "appInstalls": "1,000,000,000+",
  "appUpdated": "2026-09-18T07:51:07.000Z"
}
Enter fullscreen mode Exit fullscreen mode

App Store rows have a title and isEdited but no per-review version (Apple doesn't publish it). Play rows have version for about 84% of reviews, which is what lets you tie complaints to a specific release.

Use case: a daily Slack alert for new negative reviews

The idea: every morning, fetch only reviews since yesterday, only 1 and 2 stars, and post them to a Slack channel through an incoming webhook.

pip install apify-client requests
Enter fullscreen mode Exit fullscreen mode
import datetime
import requests
from apify_client import ApifyClient

SLACK_WEBHOOK = "<YOUR_SLACK_INCOMING_WEBHOOK_URL>"
APPS = ["324684580", "com.spotify.music"]  # replace with your own app IDs

since = (datetime.date.today() - datetime.timedelta(days=1)).isoformat()

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("rel8ble/app-reviews-scraper").call(run_input={
    "apps": APPS,
    "country": "us",
    "sort": "newest",
    "stars": [1, 2],
    "reviewsSince": since,
    "maxReviewsPerApp": 0,
})

lines = []
for r in client.dataset(run["defaultDatasetId"]).iterate_items():
    store = "iOS" if r["store"] == "appStore" else "Android"
    version = f' v{r["version"]}' if r.get("version") else ""
    text = (r.get("text") or "").replace("\n", " ")[:300]
    lines.append(f'*{r["rating"]}★ {store}{version}* {r.get("title") or ""}\n>{text}')

if lines:
    requests.post(SLACK_WEBHOOK, json={"text": f"{len(lines)} new 1-2★ reviews since {since}\n\n" + "\n\n".join(lines)})
Enter fullscreen mode Exit fullscreen mode

With sort: "newest" and reviewsSince, the run stops as soon as it reaches older reviews, so a daily run only pays for the new ones. If the Android rows cluster on one version, you've probably found a regression.

What it costs

$0.25 per 1,000 reviews. Reviews filtered out by stars or reviewsSince are free, and so are apps that turn out not to exist. No start fee.

  • The daily alert above for a mid-size app: often under a hundred reviews a day, a few cents a month
  • 100,000 reviews for a research dataset = $25
  • Apify's free plan gives $5 of monthly credit, about 20,000 reviews

Limits

  • App Store reviews have no app version. Apple's public data doesn't include it.
  • Google Play returns one language per run (language). Run twice for two languages.
  • Reviews are per country. us and gb have different reviews; run each country separately.
  • Star filters on Play go one star at a time: with [1, 2] you get newest 1-star first, then 2-star. Play has no "most critical" sort, so the actor walks star by star.
  • Reviewer names and avatars are personal data under GDPR/CCPA. Have a lawful reason before storing them.

Issues tab on the actor page if something's wrong.

App Store & Google Play Reviews Scraper on Apify


This article was drafted with AI and published by me.

Top comments (0)