# App Store Keyword Rank Monitor (`mmdincer/app-store-keyword-rank-monitor`) Actor

Reports an app's position in Apple's public iTunes Search API results for one keyword and storefront. That number is a proxy and can differ from the App Store app. Apple's terms restrict automated rank measurement. Not affiliated with Apple.

- **URL**: https://apify.com/mmdincer/app-store-keyword-rank-monitor.md
- **Developed by:** [Muhammet Mustafa Dincer](https://apify.com/mmdincer) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What is App Store Keyword Rank Monitor?

**App Store Keyword Rank Monitor** tells you where an app sits in Apple's public iTunes Search API results for a keyword and a storefront. For each keyword, country and app it calls `https://itunes.apple.com/search` with `entity=software` (or iPad / Mac software) and reads the app's **1-based position in the `results` array**. If the app is not in that window, the rank is null.

That number is a commonly used proxy. **It can differ from the order a person sees in the App Store app.** Apple's search page, the App Store app, and this API are not the same list. Do not treat the number as the official chart.

A bundle id is resolved to a numeric track id with `https://itunes.apple.com/lookup` before the search. There is no App Store HTML page, no private Apple endpoint, no Apple developer token, and no proxy. Google Play is not part of this Actor.

Apple's Media Services terms generally forbid using an automated process to measure or monitor the services, including ranking. This Actor calls the public JSON API anyway. Whether you may run it is your decision. This README is not legal advice. The Actor is not affiliated with Apple.

### Quick answer: when should you (or an AI agent) pick this Actor?

**Pick this Actor when** you want a scheduled check of where your app, or a competitor, sits in the iTunes Search API for a list of keywords and storefronts such as `us`, `tr` and `gb`. A typical input is 10 to 40 keywords, one to three storefronts, and one app id.

**Pick something else when** you need the rank a person sees inside the App Store app, a keyword's search popularity, install estimates, or Google Play. This Actor does not have those. It also does not keep a chart: the dataset of each scheduled run is the changelog, and the saved memory is only the previous rank.

**Defaults that work:** keyword `podcast`, Apple id `324684580`, storefront `us`, limit 200. One Start is one Search API call.

### At a glance

| | |
|---|---|
| **Does** | Calls the public iTunes Search API and records the 1-based position of each app in `results`. Resolves a bundle id with Lookup. |
| **Input** | Keywords, app ids and/or bundle ids, storefronts, result limit (max 200), optional app kind and attribute, concurrency and delay. |
| **Output** | One dataset row per keyword × storefront × app: rank or null, the apps around it, the top of the window, and the previous rank when one was saved. |
| **Needs** | Nothing. No Apple key and no proxy. |
| **Depth** | At most 200 results per search, which is the Search API maximum. |
| **Pricing** | Pay per event. Free plan: $0.0005 per run and $0.008 per completed rank row, including a miss. |
| **Does not** | Read the App Store app's own ranking, scrape App Store HTML, call a private API, use a proxy, or read Google Play. |

### Key terms used in this README

- **Search API rank:** the 1-based index of the app in the `results` array from `itunes.apple.com/search` for that keyword, storefront, entity and limit. This is the only rank this Actor reports.
- **Miss:** `rank` is null and `status` is `not_found`. The app was not inside the returned window. A miss is a result and is charged. It does not mean the app is unpublished.
- **Window:** the `resultLimit` you set, from 1 to 200. `hitResultLimit` is true when Apple returned a full window, so a miss may be past the cut rather than absent from a short list.
- **Track id:** the numeric App Store id, the same digits as `id324684580` in an `apps.apple.com` URL. The field is `trackId`.
- **Lookup:** `itunes.apple.com/lookup`, used only to turn a bundle id into a track id and to fill the app name. Several numeric ids share one Lookup call.
- **Previous rank:** the last completed check for the same watchlist name, keyword, storefront, app, entity, attribute and limit. `rankChange` is the previous rank minus the new rank when both were numbers. Positive means the app moved up. This is not a history.
- **Blocked:** Apple answered 403 or 429 after the backoff. That keyword's rows are not charged. Other keywords continue.

### What can you use it for?

#### A weekly keyword watch

Schedule the same input once a week. Twenty keywords, two storefronts and one app are 40 rows. On the Free plan that is 40 × $0.008 = $0.32 plus the start fee. The dataset from each run is the record of what changed. `rankChange` only compares with the previous saved check.

#### A storefront check

Pass `countries: ["us", "gb", "tr"]`. Each storefront is its own search. The same app can be present in one and missing in another.

#### Your app and two competitors

Pass three ids. One Search API call still covers the keyword and storefront. You get three rows, and you pay for three completed checks, because each rank is a separate answer. The neighbor list shows the apps immediately above and below yours.

### How does it work?

1. App ids are checked as digits, bundle ids are checked as `com.example.app`, and an `apps.apple.com` or `itunes.apple.com` URL is reduced to its id. Anything else is skipped. The Actor never requests a host you typed. The only host is `itunes.apple.com`.
2. Numeric ids are sent together to Lookup. Each bundle id is looked up on its own, then on each requested storefront if the default store returns nothing. A lookup miss does not drop the app: the search still matches the id you gave.
3. For each keyword and storefront the Actor calls Search with `media=software`, your entity, your limit, and no attribute unless you chose developer-name search. Every app is then located in that one `results` array.
4. Request starts are at least `requestDelayMs` apart (default 500, minimum 400). At most `maxConcurrency` requests are in flight (default 2, maximum 3). Connections are reused. A 403 or 429 waits 1s, then 2s, then 4s, and is tried up to four times in all. After that, every app on that keyword is a `blocked` row and the run moves on. A 5xx and a dropped connection use the same backoff. The Actor does not change IP, does not open a proxy, and does not call another Apple host.
5. The previous rank is read from the named key-value store `app-store-keyword-rank-monitor-state`. After a completed row is stored and charged, that one rank replaces the saved value. A failed request does not replace it. There is no list of older ranks.
6. If the run hits its maximum charge, rows that did not fit are left unsaved, so the next run can deliver them. Rows already refused by Apple are still written and are not charged.

### How to check a keyword (step by step)

1. Open the Actor. Keep the prefilled keyword and app, or replace them.
2. Add storefronts if you need more than the US. `US`, `TR` and `GB` are the usual codes. `uk` is read as `gb`.
3. Leave the limit at 200 unless you only care about a shorter window.
4. Leave **Search attribute** on **All attributes**. Developer name only is a different search.
5. Click **Start**. Each app is one row. `status: ranked` means the app was in the window. `status: not_found` means it was not.
6. Schedule the same input. The next run fills `previousRank` and `rankChange` when both checks found the app inside the window.

### Input

| Field | Type | Default | Meaning |
|---|---|---|---|
| `keywords` | list | prefilled `podcast` | Up to 40. One Search API call per keyword and storefront. |
| `apps` | list | prefilled `324684580` | Up to 8 track ids, bundle ids or App Store URLs. |
| `countries` | list | `us` | Up to 5 storefronts. `uk` is read as `gb`. |
| `resultLimit` | integer | 200 | 1 to 200. A null rank is outside this window. |
| `entity` | string | `software` | `software`, `iPadSoftware` or `desktopSoftware`. |
| `attribute` | string | `all` | `all` is the keyword rank. `softwareDeveloper` searches the developer name only. |
| `stateName` | string | `default` | Letters, digits, `-` and `_`. Same name, same previous-rank memory. |
| `maxConcurrency` | integer | 2 | In-flight requests, 1 to 3. |
| `requestDelayMs` | integer | 500 | Gap between request starts. Minimum 400. |

There is no proxy field.

### Output

One dataset row per keyword, storefront and app, including failures. The view is **Ranks**. `assets/sample-run-data.json` is two rows from a local Search API call on 5 November 2026: keyword `spotify`, US store, limit 10. Spotify was position 1, and the next check a moment later reported `rankChange: 0`. Apple Podcasts was not in that short list. Ranks move. The file shows the shape, not today's order.

| Field | Meaning |
|---|---|
| `keyword`, `country` | What was searched. `country` is lowercase. |
| `trackId`, `bundleId`, `appName` | The app. `trackId` is the App Store id. |
| `rank` | 1-based Search API position, or null. |
| `rankChange` | Previous rank minus this rank. Positive means moved up. Null when either side was outside the window or there was no previous check. |
| `previousRank`, `comparedAt` | The last saved check. `comparedAt` is null the first time. |
| `resultCountReturned`, `resultLimit`, `hitResultLimit` | How many results came back, what you asked for, and whether the window was full. |
| `searchedAt`, `searchUrl` | When, and the Search API URL. |
| `entity`, `attribute` | App kind, and `softwareDeveloper` or null. |
| `source` | Always `itunes-search-api`. |
| `neighbors` | Up to three apps above and three below: `rank`, `trackId`, `bundleId`, `appName`. |
| `topResults` | The first results, up to 10, with the same fields. |
| `status` | `ranked`, `not_found`, `blocked` or `search_failed`. |
| `message` | Why the rank is null, or why the request failed. |

#### Key-value store

| Key | Where | Meaning |
|---|---|---|
| `RUN_SUMMARY` | default store | Counts, warnings, and why the run stopped. |
| `STATE` | default store | Internal resume marker. |
| `rank--{stateName}--{hash}` | named store `app-store-keyword-rank-monitor-state` | The last rank only. Not a chart. |

`finishedReason` in the summary is `completed`, `charge_limit` or `aborted`. A run where every keyword fails ends as a failed run. One failed keyword does not.

### Pricing

Pay per event. Prices below are US dollars on the public tier list in `pricing-ppe.json`. Apify's margin is 20%. This file is the proposal. It is not applied on the platform by this change.

| Event | Free | Bronze | Silver | Gold and above | When |
|---|---|---|---|---|---|
| Actor start | $0.0005 | $0.0005 | $0.0005 | $0.0005 | Once per run (one event per GB of memory, minimum one) |
| Rank reported | $0.008 | $0.007 | $0.006 | $0.005 | One per completed row |

The charge in the code is `Actor.charge({ eventName: 'rank-reported', count })` after the rows are stored. `pushData` is called **without** an event name, so a row is not billed twice.

Charged: `ranked` and `not_found`. A miss is the answer "not in this window".

Not charged: `blocked` and `search_failed` rows, Lookup calls, neighbor and top-result lists, `rankChange`, and the run summary. There is no proxy traffic.

A charge result of zero events is a spending limit only when `eventChargeLimitReached` is true. A run that is not on pay-per-event still stores the rows.

Worked examples on the Free plan, plus the start fee: 20 keywords × 2 storefronts × 1 app, weekly, is about 160 rows a month, about $1.28. The same set daily is about $9.60. Forty keywords × 3 storefronts × 1 app, daily, is 3,600 rows, about $28.80. A heavier daily matrix costs more than a flat ASO subscription; use a weekly schedule if that is the job you want.

The band sits above those public per-row prices and near a small ASO subscription for a working keyword set. See the comparison. The platform cost of a run was not measured here, because this change does not publish the Actor or set a live price.

### Comparison

| | This Actor | Public Search-API rank actors | An ASO subscription |
|---|---|---|---|
| What the number is | Position in iTunes Search API `results` | The same family of signal | Their own rank product |
| Google Play | No | Varies | Often included |
| Previous rank | Last check only | Varies | A chart inside the product |
| Price signal | $0.008 per completed row on Free, $0.005 on Gold | About $0.002 and about $0.001 per row on public list prices | About $19/month on annual billing, or $29/month on monthly billing, for a suite that does more than this |

The $0.002 figure is the public list price cited for `petersutarik/aso-keyword-intel` (about 95 users in 30 days). The $0.001 figure is the public list price cited for `slothtechlabs/aso-keyword-rank-tracker`. The subscription figures are from AppFollow's October 2026 article on building versus buying an ASO tool. Those products are named only so the price has a reference. Their text and their implementation are not reused here.

Every paid tier of this Actor is at least $0.005, which is 2.5 times the $0.002 row. The price is for the rank answer. The HTTP call is shared by every app on that keyword, and that cost was not measured on Apify in this change.

### Legal and privacy

**Apple.** The Search and Lookup URLs are public and need no key. They are documented by Apple as the iTunes Search API. `itunes.apple.com/robots.txt` (fetched 5 November 2026) contains `Disallow: /search*` and `Disallow: /*/lookup?`. The Search call this Actor makes is under `/search`, so the robots file disallows it. The Lookup call is `/lookup` on the same host, and the robots file has a lookup rule. Apple's Media Services terms say you may not use an automated process to scrape, copy, or perform measurement, analysis, or monitoring of the services. Rank checking is that kind of measurement. This Actor calls the API anyway. It does not have Apple's permission.

It does not rotate addresses, open a proxy, raise the delay above your setting except for the 403/429 backoff, or switch to a private API when Apple refuses. If Apple asks for this Actor to be removed, it comes off the Store. The place to say so is an issue on `https://github.com/mmdincer/apify`. This README is not legal advice. The Actor is not affiliated with Apple.

**Personal data.** Rows store the app name, the numeric id, the bundle id, and the same fields for neighboring apps. Those are the public store listing. Review text, user names and device data are not requested.

**No chart.** Do not expect this Actor to draw a 90-day graph. Schedule it and keep the datasets. The key-value store holds one previous rank so the next run can show the move.

### FAQ

**Is this the rank I see in the App Store?**
No. It is the position in the iTunes Search API results for that keyword and storefront. The App Store app can show a different order. The row's `source` is `itunes-search-api` for that reason.

**Why is the rank null for a famous app?**
It was not inside the first `resultLimit` results. The default limit is 200, which is as far as this API goes. Raise the limit if you set it lower. A null rank with `hitResultLimit: false` means the whole response was shorter than the limit and the app was still not in it.

**What if Apple returns 403 or 429?**
That keyword is retried with a wait, then marked `blocked` and not charged. The run does not switch IP or turn on a proxy. Other keywords still run. If every keyword is blocked, the run fails.

**Does the first run cost money?**
Yes. The rank itself is the product, including the first time you see it. You are not charged for a request Apple refused.

**What does rankChange mean?**
Previous rank minus the new rank. From 12 to 5, `rankChange` is 7 (the app moved up). From 5 to 12, it is -7. If either check missed the window, `rankChange` is null and `previousRank` still shows the number when the earlier check had one.

**Can I track Google Play?**
No. This Actor does not call Google. A public Play search that would require scraping the Play page is out of scope.

### Local run

From `actors/app-store-keyword-rank-monitor` after `npm install`:

```bash
mkdir -p storage/key_value_stores/default
```

Write `storage/key_value_stores/default/INPUT.json`, then `npm start`. `npm test` runs the unit tests and does not call Apple.

# Changelog

This Actor's version history is a separate document: https://apify.com/mmdincer/app-store-keyword-rank-monitor/changelog.md

# Actor input Schema

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

Words or phrases to look up, one per line. The rank on each output row is that app's position in the iTunes Search API <code>results</code> array for this keyword and storefront. It is a proxy for App Store search rank and can differ from the App Store app. Matching is not case sensitive. At most 40. Each keyword and storefront is one Search API call, shared by every app.

## `apps` (type: `array`):

Apps to find in those results. Use a numeric App Store id (<code>324684580</code> in <code>apps.apple.com/.../id324684580</code>), a bundle id such as <code>com.spotify.client</code>, or an App Store URL. A bundle id is resolved with the public Lookup API. At most 8. Each app is its own output row.

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

Two-letter storefront codes, for example <code>US</code>, <code>TR</code>, <code>GB</code>. <code>uk</code> is read as <code>gb</code>. Case does not matter. Results differ by storefront. At most 5. Empty means <code>us</code>.

## `resultLimit` (type: `integer`):

How many Search API results to request (1 to 200). 200 is the API maximum. A null rank means the app was not inside this window. It does not mean the app is absent from the App Store, and it is not the rank shown in the App Store app. A shorter limit makes a miss more likely.

## `entity` (type: `string`):

Which software results to ask for. <code>software</code> is iPhone. <code>iPadSoftware</code> is iPad. <code>desktopSoftware</code> is Mac. The rank is the position inside that result list only.

## `attribute` (type: `string`):

Leave this on <b>All attributes</b> for a keyword rank. <b>Developer name only</b> searches the developer field and is a different question, not a keyword rank.

## `stateName` (type: `string`):

Name of the saved last rank (letters, digits, - and \_). Runs with the same name, keyword, storefront, app, app kind, attribute and result limit share one memory. The memory is only the previous rank, so a scheduled run's dataset is the changelog. Stored in the key-value store <code>app-store-keyword-rank-monitor-state</code>.

## `maxConcurrency` (type: `integer`):

How many iTunes requests may be in flight at once (1 to 3). Starts are still spaced by the pause below. The default is 2.

## `requestDelayMs` (type: `integer`):

Minimum gap between iTunes request starts. The default is 500. The minimum is 400. A 403 or 429 adds a further wait (1s, then 2s, then 4s) and then that keyword fails. The Actor does not retry on another address and does not use a proxy.

## Actor input object example

```json
{
  "keywords": [
    "podcast"
  ],
  "apps": [
    "324684580",
    "com.spotify.client"
  ],
  "countries": [
    "us"
  ],
  "resultLimit": 200,
  "entity": "software",
  "attribute": "all",
  "stateName": "default",
  "maxConcurrency": 2,
  "requestDelayMs": 500
}
```

# Actor output Schema

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

One row per keyword, storefront and app. rank is the 1-based Search API position, or null.

## `summary` (type: `string`):

Searches, rows delivered, failures, warnings and why the run stopped.

# 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": [
        "podcast"
    ],
    "apps": [
        "324684580"
    ],
    "countries": [
        "us"
    ],
    "resultLimit": 200,
    "entity": "software",
    "attribute": "all",
    "stateName": "default",
    "maxConcurrency": 2,
    "requestDelayMs": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("mmdincer/app-store-keyword-rank-monitor").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": ["podcast"],
    "apps": ["324684580"],
    "countries": ["us"],
    "resultLimit": 200,
    "entity": "software",
    "attribute": "all",
    "stateName": "default",
    "maxConcurrency": 2,
    "requestDelayMs": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("mmdincer/app-store-keyword-rank-monitor").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": [
    "podcast"
  ],
  "apps": [
    "324684580"
  ],
  "countries": [
    "us"
  ],
  "resultLimit": 200,
  "entity": "software",
  "attribute": "all",
  "stateName": "default",
  "maxConcurrency": 2,
  "requestDelayMs": 500
}' |
apify call mmdincer/app-store-keyword-rank-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mmdincer/app-store-keyword-rank-monitor"
        }
    }
}
```

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/4asGFmtJGynl6YE51/builds/EJMxBmR4t26i2sfql/openapi.json
