App Review Delta Monitor — Google Play + App Store New Reviews
Pricing
from $2.00 / 1,000 new review delivereds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
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 id | kaz_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 |
| Output | Dataset 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)
- Open the actor → Input. Prefill already has WhatsApp on both stores (
com.whatsappon Play,310633997on Apple) forus. - Run once → expect two
type:"baseline"rows, 0 charged reviews, status SUCCEEDED. - Schedule it (see below) with a stable
monitorId. Next runs emit only newtype:"review"rows.
Input contract
| Field | Type | Default | Notes |
|---|---|---|---|
apps | stringList | (prefill only) | Play package ids, Apple numeric/id… ids, or store URLs. Auto-detect. Schema max 100; runtime hard-cap 200. |
countries | stringList | ["us"] | Allowlisted 2-letter codes (Apple storefront table — enforced for both stores). Max 20. |
monitorId | string | "" → default | Snapshot + lease scope. One schedule / one run per monitorId. Truncated to 120 chars. |
maxNewReviewsPerAppCountry | integer | 200 (1–500) | Newest-first; overflow dropped permanently. |
includeVersionSummary | boolean | true | Uncharged 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
| Event | Price | When |
|---|---|---|
apify-actor-start | $0.005 | Once per run |
poll | $0.002 | Each completed delta app×country check (baseline free; empty/stale feeds are free) |
new-review | $0.002 | Each 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 poll → pre-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:
| Situation | What happens | Billing |
|---|---|---|
| The previous run is finished and its whole dataset can be read | Every 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 scan | Proven never written, therefore never charged → re-delivered on this run | Charged 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, keysnapshot-{monitorId}(schema v2). One record holds every app×country pair thatmonitorIdhas 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 onemonitorId, 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(=dof 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 withfloorDateand 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, emitbaseline, 0 charges. Legacy v1seenIdsis 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 alsoreviewedAt >= 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
pendingmarker, 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 unchargedowed_backlog_droppedrow 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. maxNewReviewsPerAppCountryis 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_ATis 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;
reviewedAtcan 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
countryfield 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
userReviewsRowhas no per-review app version → regression grouping is Play-only (versionDataAvailable: falseon 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
floorDateare dropped. - Customer Reviews RSS is dead — this actor does not use it.