# App Store Keyword Rank Tracker (`scrapyx/app-store-keyword-rank-tracker`) Actor

Tracks iOS App Store search-ranking positions for any keyword, via Apple's own public Search API. Returns the full ranked result list per keyword/country plus, optionally, the exact rank of specific apps you track by bundle ID.

- **URL**: https://apify.com/scrapyx/app-store-keyword-rank-tracker.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Keyword Rank Tracker (ASO)

Tracks iOS App Store search-ranking positions for any keyword, via
**Apple's own public Search API**. Public data only. No login, no cookies,
no browser, no reverse engineering — this is the one actor in this
portfolio built entirely on a vendor-documented endpoint.

### The one thing you need to know before using this

There is no WAF, no rate limit hit, and no bypass work here: `search.itunes.apple.com`
is meant for third-party consumption
([Apple's own docs](https://performance-partners.apple.com/search-api)).
The product this actor sells is **ASO (App Store Optimization) rank
tracking** — turning Apple's search-result *order* into a reportable
ranking signal for a keyword, not the difficulty of reaching the data.
Android/Google Play is intentionally **not** covered — see
[`CRAWLING_METHOD.md`](CRAWLING_METHOD.md) §4 for why.

### What you get

Four record types share one dataset, told apart by `recordType`.

#### `APP` — one row per ranked result

`rank` is the result's 1-based position in Apple's own returned order —
that ordinal position **is** the ranking signal, not a score Apple exposes
as a field. `app` carries Apple's full result object verbatim (price,
rating, rating count, icon, genres, seller, screenshots, release notes,
…).

#### `RANK_TRACK` — one row per (keyword, tracked app), only when **Track specific apps' rank** is set

Identify an app by bundle ID (most precise), numeric track ID, or name
(least precise — case-insensitive substring match). Reports the app's
exact rank if it's within the fetched results, or `foundInResults: false`
if it isn't — a real, reportable ASO outcome, not a failure.

#### `SEARCH_SUMMARY` — one row per (keyword, country, entity) query

Apple's own `resultCount` for that query (capped at 200 server-side,
confirmed live — not a client-side guess).

#### `ERROR` — one row per input that failed

So every entry in **Keywords** maps to at least one output row.

### Input

| Field | What it does |
| --- | --- |
| **Keywords** | free-text App Store search terms — one search per entry |
| **App Store country** | two-letter storefront code (`us`, `gb`, `id`, …), applies to every keyword |
| **App type** | iPhone/universal, iPad-only, or Mac — each a genuinely different result set (verified live) |
| **Track specific apps' rank** | bundle ID / track ID / name — adds a `RANK_TRACK` row per keyword |
| **Results per keyword** | up to 200 (Apple's own hard cap) — also the depth searched for tracked-app matches |

#### Example

```json
{
  "keywords": ["todo list", "habit tracker"],
  "country": "us",
  "trackedApps": ["com.todoist.ios"],
  "limit": 100
}
```

### Notes on reliability

- **A bogus country code is refused by Apple with a clean HTTP 400** —
  reported as an `ERROR` row, never silently substituted with a default
  storefront.
- **A nonsense keyword is NOT a hard zero** — Apple's search does its own
  fuzzy/relevance matching and can return a small number of loosely
  related apps even for made-up terms. This is documented Apple behaviour,
  not a bug this actor works around.
- **A wrong bundle-ID guess for a tracked app is honestly reported as
  `foundInResults: false`**, never a false-positive match — matching only
  happens on an exact bundle ID, exact numeric track ID, or a genuine
  substring of the real app name.
- **Apple caps results at 200 regardless of what's requested** — verified
  live (`limit=201` returns the identical set as `limit=200`).

### Output envelope

Every record carries `_input`, `_source` and `_scrapedAt`. Upstream field
names pass through **verbatim** under `app` (and `matchedApp` on
`RANK_TRACK` rows) — no renaming.

See [`CRAWLING_METHOD.md`](CRAWLING_METHOD.md) for the recon trail,
including why Google Play was left out of v0.1.

# Actor input Schema

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

One App Store search per entry (e.g. 'todo list', 'meditation app'), each with its own SEARCH\_SUMMARY row. Apple's search is a real relevance ranking -- the returned order IS the rank signal this actor reports.

## `country` (type: `string`):

Two-letter App Store storefront code (e.g. 'us', 'gb', 'de', 'id', 'jp'). Applies to every keyword in this run. An unrecognised code is refused by Apple with a clean error, not silently substituted.

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

Which App Store catalogue to search. 'software' covers iPhone/universal apps (the common case) and is a genuinely different result set from the iPad- and Mac-specific catalogues -- verified live.

## `trackedApps` (type: `array`):

Optional. Identify apps to track by bundle ID (most precise, e.g. 'com.todoist.Todoist'), numeric App Store track ID, or app name (least precise -- matched as a case-insensitive substring of the result's name). For each keyword, adds one RANK\_TRACK row per tracked app reporting its exact rank, or foundInResults=false if it isn't within the fetched results.

## `limit` (type: `integer`):

How many ranked results to fetch per keyword. Apple caps this at 200 server-side regardless of what's requested here (verified live) -- this is also the depth searched for trackedApps matches.

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

Upper bound on requests in flight at once. This is Apple's own public API with no bot-mitigation gate observed, so this can be raised freely -- kept moderate by default purely as a courtesy to a shared public endpoint.

## `minRequestInterval` (type: `integer`):

Paces request starts (not held inside a concurrency slot) rather than raw concurrency. Kept at 1s by default as a courtesy to a shared public endpoint, not because rate-limiting was observed.

## `proxyConfiguration` (type: `object`):

Not needed by default -- this is Apple's own public Search API with no WAF or fingerprint gate observed. Exposed for users who specifically want to test from a given egress region.

## Actor input object example

```json
{
  "keywords": [
    "todo list"
  ],
  "country": "us",
  "entity": "software",
  "trackedApps": [],
  "limit": 50,
  "maxConcurrency": 5,
  "minRequestInterval": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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": [
        "todo list"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/app-store-keyword-rank-tracker").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": ["todo list"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/app-store-keyword-rank-tracker").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": [
    "todo list"
  ]
}' |
apify call scrapyx/app-store-keyword-rank-tracker --silent --output-dataset

```

## MCP server setup

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

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/hekbOh7NA8vk4Vtb0/builds/XjK2SW9HuAOHy7ecb/openapi.json
