App Review Delta Monitor — Google Play + App Store New Reviews avatar

App Review Delta Monitor — Google Play + App Store New Reviews

Pricing

from $2.00 / 1,000 new review delivereds

Go to Apify Store
App Review Delta Monitor — Google Play + App Store New Reviews

App Review Delta Monitor — Google Play + App Store New Reviews

Get only NEW Google Play + Apple App Store reviews since your last run — stateful delta monitor with app-version regression flags. Both stores in one scheduled actor. No reviewer PII. Keyless.

Pricing

from $2.00 / 1,000 new review delivereds

Rating

0.0

(0)

Developer

Heim AI

Heim AI

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Share

App Review Delta Monitor — Google Play + App Store

Scheduled delta monitor for app reviews. One actor covers Google Play and the Apple App Store. Each run emits only reviews that are new since the last run for a given monitorId, plus an optional uncharged per-app version-regression summary. No reviewer names, handles, or profile URLs — ever.

Actor idkaz_kakyo/app-review-delta
Minimal input{ "apps": ["com.whatsapp", "310633997"], "countries": ["us"] }
Cost$0.005 start + $0.002/app×country delta poll + $0.002/new review · baseline free
OutputDataset rows: review (charged), baseline / version_summary / source_degraded / error (uncharged)

Why delta beats re-pulling

Full-pull review scrapers re-download the same recent pages every hour and bill you for volume you already have. This actor keeps a per-monitorId snapshot of seen review ids and only delivers new ones.

Cost math (list prices above): 4 app×country pairs polled hourly, ~5 new reviews per pair per day:

  • Delta: 24 × ($0.005 start + 4 × $0.002 polls) = $0.31/day, plus 4 × 5 × $0.002 = $0.04/day in new reviews → ~$0.35/day ≈ $10.60/month
  • Re-pulling 200 recent reviews per pair every hour at typical per-review prices re-bills you for the same reviews hundreds of times for the same signal

First run is a free baseline (seeds the snapshot, 0 charged reviews). Prefill input succeeds keyless as-is.

Quickstart (Console)

  1. Open the actor → Input. Prefill already has WhatsApp on both stores (com.whatsapp on Play, 310633997 on Apple) for us.
  2. Run once → expect two type:"baseline" rows, 0 charged reviews, status SUCCEEDED.
  3. Schedule it (see below) with a stable monitorId. Next runs emit only new type:"review" rows.

Input contract

FieldTypeDefaultNotes
appsstringList(prefill only)Play package ids, Apple numeric/id… ids, or store URLs. Auto-detect. Schema max 100; runtime hard-cap 200.
countriesstringList["us"]Allowlisted 2-letter codes (Apple storefront table — enforced for both stores). Max 20.
monitorIdstring""defaultSnapshot + lease scope. One schedule / one run per monitorId. Truncated to 120 chars.
maxNewReviewsPerAppCountryinteger200 (1–500)Newest-first; overflow dropped permanently.
includeVersionSummarybooleantrueUncharged version_summary when ≥1 new review.

Missing/empty apps → one uncharged error row, run SUCCEEDS. Dedupe pairs; hard cap 30 app×country pairs/run (excess → uncharged skip errors). Unsupported country codes → uncharged error per pair (Play included).

Output row types

review (charged as new-review)

{
"type": "review",
"store": "play",
"appId": "com.whatsapp",
"country": "us",
"monitorId": "default",
"reviewId": "…",
"rating": 1,
"title": null,
"text": "…",
"appVersion": "2.26.27.85",
"reviewedAt": "2026-08-01T12:00:00.000Z",
"detectedAt": "2026-08-01T12:05:00.000Z",
"isEdited": null
}

Play: title/isEdited are null. Apple: appVersion is null (endpoint has no version field). PII guard: no author name, username, profile URL, or avatar in any row type.

baseline (uncharged)

Emitted once per app×country when no v2 snapshot entry exists. Seeds seen: [{i,d},…] + floorDate; no poll / new-review charges. Legacy v1 snapshots re-baseline cleanly.

version_summary (uncharged)

Per pair with ≥1 delivered new review (when enabled):

{
"type": "version_summary",
"store": "play",
"appId": "com.whatsapp",
"country": "us",
"monitorId": "default",
"newReviewCount": 12,
"overallAvgRating": 3.4,
"groups": [{"appVersion": "2.26.27.85", "count": 8, "avgRating": 2.1}],
"regressionSuspect": true,
"regressionVersions": ["2.26.27.85"],
"versionDataAvailable": true,
"deliveryConfirmed": true,
"detectedAt": "…"
}

Regression heuristic: any version group with count ≥ 5 AND avgRating ≤ overallAvgRating − 1.0. Apple pairs set versionDataAvailable: false and group under "unknown".

deliveryConfirmed: false means the review push for that batch errored, so the summary covers the reviews the poll attempted — some may be missing from this dataset and arrive on a later run. The summary is emitted either way rather than lost.

source_degraded / error (uncharged)

Fetch/parse failures and bad input. Snapshot for that pair is not advanced. Run still SUCCEEDS.

Billing

EventPriceWhen
apify-actor-start$0.005Once per run
poll$0.002Each completed delta app×country check (baseline free; empty/stale feeds are free)
new-review$0.002Each delivered new review

Worked example: 3 delta pairs, 7 new reviews → $0.005 + 3×$0.002 + 7×$0.002 = $0.025.

Charge order per delta pair (fail-closed): pending-marker recovery from any interrupted previous run → fetch → empty/stale/floor filters → lease re-check → charge pollpre-clamp to remaining new-review budget (budget 0 → uncharged skip, snapshot untouched; excess clamped newest-first and re-found next run) → persist-before-push (all clamped ids durably recorded as seen together with a pending {items, datasetId, runId, preCount} marker; if this persist fails, the reviews are withheld uncharged and retried next run) → pushData(…, 'new-review') → confirm (marker stripped, only delivered ids kept). If pushData throws, the outcome is ambiguous and the actor does no same-run shrink (same-run item counts can be stale): the marker stays and the next run settles it. The apify-actor-start charge is fail-closed too: if it cannot be confirmed on a PPE run, the run ends uncharged before any polling. Never save-then-charge in the loss direction: no review is ever charged before its id is durable. Off-PPE (local / events not registered) delivers without charging.

Interrupted-run recovery (what a marker costs you: nothing). A marker is settled by the next run before anything else, and only two outcomes exist:

SituationWhat happensBilling
The previous run is finished and its whole dataset can be readEvery marked id is checked against that dataset in full — no recorded offset or window is trusted. Ids found there were written, so they were charged and stay delivered.Nothing re-charged
Ids are absent from that complete scanProven never written, therefore never charged → re-delivered on this runCharged once, on the re-delivery
The outcome cannot be proven (dataset unreadable/expired, previous run still alive, scan too large)Ids are re-delivered anyway and flagged in an uncharged source_degraded (recovery_unverifiable) row. They may duplicate a row from the interrupted run's dataset.Free — never charged again

Because a review is only ever charged when it is written (charge-on-write), anything you were billed for is already in a dataset — a lost obligation can only ever be a review you were never charged for. Obligations are re-delivered from the store's live feed, so a review that scrolls out of reach before the next successful poll is not recoverable. Polling stops at the first page that contains nothing new, so in practice an owed review stays reachable while fewer than ~100 newer reviews have arrived since — poll often enough for that to hold.

Ambiguity always costs us the revenue, never you the data: no review is ever silently dropped and no review is ever billed twice.

Schedule recipe

Create an Apify Schedule that runs this actor hourly with a dedicated monitorId (POST https://api.apify.com/v2/schedules):

{
"name": "app-reviews-hourly",
"cronExpression": "0 * * * *",
"isEnabled": true,
"actions": [{
"type": "RUN_ACTOR",
"actorId": "<actorId of kaz_kakyo/app-review-delta>",
"runInput": {
"contentType": "application/json",
"body": "{\"apps\":[\"com.whatsapp\",\"310633997\"],\"countries\":[\"us\"],\"monitorId\":\"app-reviews-hourly\"}"
}
}]
}

API / MCP

curl -X POST "https://api.apify.com/v2/acts/kaz_kakyo~app-review-delta/runs?token=$APIFY_TOKEN" \
-H "content-type: application/json" \
-d '{"apps":["com.whatsapp","310633997"],"countries":["us"],"monitorId":"api-test"}'
{
"actor": "kaz_kakyo/app-review-delta",
"input": {
"apps": ["com.whatsapp", "310633997"],
"countries": ["us"],
"monitorId": "mcp-watch"
}
}

State semantics

  • Named KV store app-review-delta-state, key snapshot-{monitorId} (schema v2). One record holds every app×country pair that monitorId has polled, so the actor keeps the 40 most recently polled pairs (never pruning one that still owes a delivery) to stay inside the platform's record-size limit. If you rotate apps under one monitorId, a returning pair simply re-baselines — free, and it delivers nothing that run.
  • Per-pair entry: seen: [{i: reviewId, d: reviewedAt}, …] newest-first (cap 1500), newestDate, floorDate (= d of the oldest retained element). The cap trim is tie-safe: an entry is never evicted while another entry with the same timestamp is retained (whole tied groups are evicted together), so an evicted id can never tie with floorDate and slip past the floor filter. In the degenerate case where >1500 entries share one timestamp, the floor becomes exclusive (floorExclusive) — ties at the floor are suppressed rather than risk re-billing.
  • Baseline (no v2 entry): fetch newest up to cap, seed seen + floorDate, emit baseline, 0 charges. Legacy v1 seenIds is ignored → clean re-baseline. A failed baseline persist just re-baselines (free) next run.
  • Delta: paginate newest-first for ids not in seen; deliver only if also reviewedAt >= floorDate (unseen-but-older-than-floor are skipped — they predate id coverage and would risk re-billing evicted ids). Stop on all-seen page, no continuation, cap, or hard page limit (Play: 5×100; Apple: ≤10 pages × 100).
  • Persist per pair, ids-before-charge: persist-before-push with a pending marker, then a confirm persist once the delivered set is known (re-get → merge one key → set, 3 retries). An interrupted run leaves the marker; the next run settles it before doing anything else (table above). If that repair cannot be persisted, the pair is skipped uncharged for the whole run rather than continuing on unsettled state.
  • Every id a recovery releases becomes a persisted obligation (owed, cap 500 per app-country) that outlives the run that released it: it bypasses the floor filter until it is actually delivered, and ids released by an unverifiable recovery are additionally marked free so their re-delivery is never charged. If the backlog ever exceeds the cap, the oldest entries — long gone from the store's feed — are dropped with an uncharged owed_backlog_dropped row that is written before the drop is saved, so the loss can never go unreported.
  • Lease: lock-{monitorId} (15 min TTL) blocks a second concurrent run sharing the same monitor — soft-skip with an uncharged error, fail-closed (an unverifiable lease also skips uncharged) and re-checked before each pair's charges. Apify KV has no compare-and-swap, so a sub-second concurrent-acquire window remains — still run one schedule per monitorId.
  • maxNewReviewsPerAppCountry is lossy; edited reviews (same id) are not re-emitted.
  • Empty-feed: zero raw reviews on a delta poll → uncharged source_degraded (empty_feed), no poll charge.
  • Stale-feed guard: if the feed's newest review is >30 min older than what this monitor has already seen, the poll is skipped uncharged (stale_feed) — Play replicas can lag >24h.
  • Deadline: when ACTOR_TIMEOUT_AT is set, pairs that would start within 60s of timeout are skipped with one aggregated uncharged error; the deadline is also enforced inside HTTP retries and pagination (in-flight pairs degrade or deliver partially instead of overrunning). The run still SUCCEEDS.
  • A review is delivered when it first becomes visible to the monitor; reviewedAt can be older than the poll interval (store indexing/moderation lag).

Honest limits

  • Google Play reviews are language-scoped, not country-scoped: the feed is filtered by language (English), and the country field on Play rows is the requested storefront country. The same Play review can appear under multiple countries you monitor (each delivery is billed). For Play, one country per app is usually enough; Apple storefronts are genuinely country-separated.
  • Apple MZStore userReviewsRow has no per-review app version → regression grouping is Play-only (versionDataAvailable: false on Apple).
  • Play batchexecute is an internal/undocumented RPC — shape drift soft-degrades (source_degraded), never fails the run.
  • Review depth per poll is bounded (page caps above); older unseen reviews beyond the cap / below floorDate are dropped.
  • Customer Reviews RSS is dead — this actor does not use it.