# App Store Rank & Rating Scraper (`northbell/app-store-rank-and-review-watch`) Actor

Daily App Store keyword ranks and an alert when new ratings turn bad. Apple public endpoints only — ranks you cannot back-fill, kept as a series.

- **URL**: https://apify.com/northbell/app-store-rank-and-review-watch.md
- **Developed by:** [Northbell](https://apify.com/northbell) (community)
- **Categories:** Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## App Store Rank & Rating Scraper

Track where your app ranks for the keywords you care about, and get told when new ratings turn against you — every morning, without opening a dashboard.

Apple's public endpoints only. No HTML scraping, no private APIs, no personal data.

### Why this exists

**Ranks cannot be back-filled.** Apple does not publish yesterday's search results. If you did not record your position on a given day, that day is gone forever. This Actor records it and keeps the series, so three months from now you can answer "did that ASO change actually do anything?"

That is the whole product. Everything else is plumbing.

### What you get

Run it daily on a schedule. Each run appends to your dataset:

**`rank` rows** — one per tracked app per keyword per storefront.

| field | meaning |
|---|---|
| `rank` | position in Apple's results, or `null` if outside `searchLimit` |
| `previousRank` | where it was last run — `null` on the first run |
| `rankChange` | positive means it moved up |
| `outOfRange` | `true` when a tracked app fell out of the searched depth |
| `isNewToKeyword` | first time this app appears for this keyword |

**`reviewSummary` rows** — one per tracked app per storefront.

| field | meaning |
|---|---|
| `newRatings` | how many ratings arrived since the last run |
| `newRatingsAverage` | the average star rating of just those new ones |
| `newRatingsErrorBound` | how far off that figure could be — always reported |
| `newRatingsReliable` | `false` when the error bound is too wide to act on |
| `ratingDrop` | `true` when the new ratings are at least a full star below the app's standing average, even after allowing for the error |
| `versionChangedSinceLastRun` | you shipped since the last run — the context for any drop |
| `averageRating`, `ratingCount` | Apple's current aggregates |

**`review` rows** — individual new reviews, when review text is available (see below).

**`error` rows** — anything that failed, in the dataset where you will actually see it.

#### Reading the ratings, not the reviews

Most people who rate an app never write a word. A tool that scrapes review text is blind to all of them.

This Actor works from Apple's aggregate figures instead. Two observations are enough to recover what happened in between: total ratings × average is a running sum, so the difference between two runs divided by the number of new ratings gives **the average of just the new ones**. Twenty ratings at 1.4 stars is a bad night, and you see it whether or not anyone wrote a review.

Apple rounds the average it reports, so the recovered figure carries a margin. That margin widens as the app gets larger and the number of new ratings gets smaller — on an app with 700,000 ratings, five new ones tell you nothing. **The margin is always reported, `newRatingsReliable` says whether it is narrow enough to act on, and an alert only fires when the drop survives the margin.** A number without its error is a number that will eventually lie to you.

#### About review text

Review text usually comes through, but not always. Apple sometimes answers the customer-review feed with a valid, correctly-formed, completely empty response — indistinguishable from "this app has no reviews" unless you look closely. Measured from Apify: it failed on two consecutive runs one hour, then succeeded five out of five the next.

This Actor tells the two apart. Every `reviewSummary` row carries `reviewTextStatus`: `ok` when the feed answered, `blocked` when it came back empty. A blocked feed does not fail the run and does not affect ranks, ratings, averages, or drop alerts — those come from a different endpoint that has never gone quiet.

That is the reason the rating maths above exists. A feature that depends on someone else's goodwill needs a floor underneath it.

### Three things it gets right

**It respects Apple's rate limit, across runs.** Apple documents 20 calls per minute. The Actor uses a token bucket that persists between runs, so two overlapping schedules do not stack up and get you throttled. Default is 18/min to leave headroom.

**It fails loudly instead of quietly.** Apple returns HTTP 200 with an empty feed in at least two situations that look exactly like "no data": when `page=` is missing from a review URL, and when the request comes from a datacenter IP. The Actor asserts the response shape, writes an `error` row into the dataset, and marks the run failed if a whole requested feature produced nothing. A green run with an empty dataset is the worst possible outcome for something you check once a month.

**A tracked app that disappears still produces a row.** If your app drops out of the top 200 for a keyword, you get a row with `outOfRange: true` — not silence. Silence is indistinguishable from a broken run.

**Every derived number carries its error.** See "Reading the ratings, not the reviews" above.

### Input

```json
{
  "keywords": ["habit tracker", "focus timer"],
  "appIds": ["1438388363"],
  "countries": ["us", "gb"],
  "trackReviews": true,
  "reviewPages": 2,
  "searchLimit": 200,
  "maxCallsPerMinute": 18
}
```

App IDs are the number in the App Store URL: `apps.apple.com/us/app/x/id1438388363` → `1438388363`.

Leave `appIds` empty to record the top 20 for each keyword instead of specific apps.

#### Sizing a run

Calls per run = `keywords × countries` + `appIds × countries × (reviewPages + 1)`.

At 18 calls/minute, 10 keywords and 3 apps across 2 storefronts is about 2.5 minutes.

### What you pay for

Pay per event. You are charged for observations that produced a result:

| event | when |
|---|---|
| Actor start | once per run |
| Rank check | one keyword, one storefront, rank recorded |
| Rating check | one app, one storefront, ratings recorded |

**A failed observation is never charged.** If Apple times out, or an app ID does not exist in that storefront, you get an `error` row and no charge for it. You are paying for data, not for attempts.

Ten keywords and three apps in one storefront, run daily, is 30 starts + 300 rank checks + 90 rating checks a month.

### Limits worth knowing

- **Search results are cached by Apple for about 24 hours** per keyword and storefront. Running more than once a day returns the same ranking. This is a daily instrument.
- **Search depth caps at 200.** Below that, an app is reported as out of range rather than ranked.
- **Reviews cap at 10 pages (500) per app per storefront**, newest first.
- The rate-limit bucket is shared through a key-value store. Two runs starting at the same instant can each read the same balance, so the default leaves headroom rather than spending all 20.

### On data and privacy

Reviewer names are never fetched or stored. They are not needed to decide anything about an app, and holding them would mean holding personal data.

Review titles and bodies, when available, pass through to your dataset but are **never written to the Actor's own history**. The persistent store holds numbers only: ranks, rating counts, and averages.

Icons and screenshots are not redistributed.

### Storage

History lives in a named key-value store, `aso-history`, so it survives between runs. Deleting that store resets the baselines — ranks start over at `previousRank: null`, and spike detection goes quiet until it has enough samples again.

### Running locally

```bash
npm install
npm test          # unit tests, no network
npm run smoke     # end-to-end against Apple, twice, to prove the diff works
```

### For AI agents

This Actor works well as an agent tool: the input schema is small and fully described, every run returns structured rows, and failures come back as data rather than silent gaps. Use it when you need to:

- track App Store keyword rankings for an app over time
- monitor iOS app rating changes and detect rating drops
- get the average rating of new App Store reviews since a previous run

***

### More no-login scrapers by northbell

Every one of these reads only public pages — **no login, no cookies** — and most of them record the numbers that cannot be back-filled if you don't capture them today.

**LinkedIn jobs**

- [LinkedIn Jobs Scraper with Applicant Counts](https://apify.com/northbell/linkedin-jobs-applicants-scraper) — jobs plus how fast applicants are arriving
- [LinkedIn Jobs Scraper — Filters That Actually Work](https://apify.com/northbell/linkedin-jobs-filter-scraper) — the experience/workplace filters LinkedIn silently ignores, applied for real
- [LinkedIn Jobs Salary Data — Filter by Pay](https://apify.com/northbell/linkedin-jobs-salary-scraper) — salary parsed into numbers so you can filter by yearly pay
- [Fast LinkedIn Jobs Scraper](https://apify.com/northbell/linkedin-jobs-fast-scraper) — bulk job listings, cheap and quick
- [LinkedIn Company Jobs Scraper](https://apify.com/northbell/linkedin-company-jobs-scraper) — every open role at a company you name

**LinkedIn companies**

- [LinkedIn Company Scraper with Headcount Growth](https://apify.com/northbell/linkedin-company-growth-scraper) — the real headcount and how fast it's growing
- [LinkedIn Company Posts + Engagement](https://apify.com/northbell/linkedin-company-posts-scraper) — a company's posts with exact reaction and comment counts

**App stores**

- [App Store Rank & Rating Scraper](https://apify.com/northbell/app-store-rank-and-review-watch) — iOS keyword rank and rating changes over time
- [Shopify App Reviews Scraper — Filter & Sort by Rating](https://apify.com/northbell/shopify-app-reviews-scraper) — exact per-star review counts, filter and sort
- [Google Play Rating & Review Tracker](https://apify.com/northbell/google-play-rating-tracker) — an Android app's rating tracked day by day

# Actor input Schema

## `keywords` (type: `array`):

Search terms to check your rank for. Ranks are only recorded on the day you run — they cannot be back-filled, so schedule this daily.

## `appIds` (type: `array`):

Numeric App Store IDs — yours and your competitors'. Found in the App Store URL: apps.apple.com/us/app/x/id1438388363 → 1438388363

## `countries` (type: `array`):

Two-letter storefront codes (us, gb, jp, de ...). Each one multiplies the number of API calls.

## `trackReviews` (type: `boolean`):

Fetch new reviews and flag unusual spikes in 1–2 star ratings. Requires App IDs.

## `reviewPages` (type: `integer`):

50 reviews per page. Apple caps this at 10 pages (500 reviews). 2 is enough for a daily run.

## `searchLimit` (type: `integer`):

How deep to look for your app in the results. Apple's maximum is 200. Apps below this are reported as out of range.

## `maxCallsPerMinute` (type: `integer`):

Apple documents a limit of 20 calls/minute. The default of 18 leaves headroom for overlapping runs. Raise it only if you run nothing else against Apple.

## Actor input object example

```json
{
  "keywords": [
    "habit tracker",
    "focus timer"
  ],
  "appIds": [
    "1438388363"
  ],
  "countries": [
    "us"
  ],
  "trackReviews": true,
  "reviewPages": 2,
  "searchLimit": 200,
  "maxCallsPerMinute": 18
}
```

# Actor output Schema

## `all` (type: `string`):

Every row: rank, reviewSummary, review, and error.

## `ranks` (type: `string`):

Where each tracked app sat for each keyword, and how far it moved since the last run.

## `ratings` (type: `string`):

New ratings since the last run, their implied average, and whether that is a drop worth acting on.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "habit tracker",
        "focus timer"
    ],
    "appIds": [
        "1438388363"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/app-store-rank-and-review-watch").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keywords": [
        "habit tracker",
        "focus timer",
    ],
    "appIds": ["1438388363"],
}

# Run the Actor and wait for it to finish
run = client.actor("northbell/app-store-rank-and-review-watch").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "habit tracker",
    "focus timer"
  ],
  "appIds": [
    "1438388363"
  ]
}' |
apify call northbell/app-store-rank-and-review-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,northbell/app-store-rank-and-review-watch"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/zme47joSahi9zSugd/builds/OmgEOkkmWeWXjRBMA/openapi.json
