# App Store Keyword Rank Tracker — iOS & Google Play (`insight.solutions/app-store-keyword-rank-tracker`) Actor

Where any app ranks for any keyword on the App Store and Google Play, in 54 storefronts. Reads Apple's own store search — not the iTunes Search API. Rank, rank change vs your last run, competitors above you, autocomplete suggestions and a free competition readout, in one dataset. No API key.

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

## Pricing

from $0.18 / 1,000 ranking rows

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 — iOS & Google Play

Where any app ranks for any keyword, on the **App Store** and **Google Play**, in
any storefront — with the rank change since your last run, the apps above you,
the store's own autocomplete suggestions, and a free competition readout for
every keyword. One flat table, one JSON object in, no API key, no browser.

The App Store half reads **Apple's own store search** — the endpoint the App
Store app itself queries — not the public iTunes Search API that the rest of this
category calls "App Store rankings". They are different lists, the difference is
measured below, and you can pick either one.

> **What this is.** A reading of a public store's search results at one instant,
> from one storefront, through one exit IP. Apple and Google personalise and A/B
> test; two readings minutes apart can differ. Nothing here is an estimate, a
> score or a prediction — every number in the output is something a store said,
> or arithmetic over the rows this run returned.

***

### What you get

One row per ranked position. Same columns on every row type:

```json
{
  "rowType": "rank",
  "keyword": "notes",
  "country": "us",
  "platform": "ios",
  "storefrontId": "143441",
  "storefrontCc": "US",
  "language": "en-us",
  "rankSource": "native",
  "detailSource": "itunes-lookup",
  "rank": 1,
  "rankStatus": "ranked",
  "checkedDepth": 20,
  "totalResults": 247,
  "isTargetApp": false,
  "appId": "1110145109",
  "bundleId": "com.apple.mobilenotes",
  "appName": "Notes",
  "subtitle": "Take note of almost anything",
  "developer": "Apple",
  "developerId": "284417353",
  "price": 0,
  "priceFormatted": "$0.00",
  "free": true,
  "rating": 4.84119,
  "ratingCount": 640785,
  "genres": ["Productivity"],
  "primaryGenre": "Productivity",
  "releaseDate": "2016-05-23T00:00:00.000Z",
  "version": "2.6",
  "minimumOs": "15.0",
  "contentRating": "4+",
  "hasInAppPurchases": false,
  "deviceFamilies": ["watch", "iphone", "realityDevice", "ipad", "ipod"],
  "iconUrl": "https://is1-ssl.mzstatic.com/image/thumb/…/1024x1024bb.png",
  "appUrl": "https://apps.apple.com/us/app/notes/id1110145109",
  "source": "apple-native-search",
  "sourceUrl": "https://search.itunes.apple.com/WebObjects/MZStore.woa/wa/search?clientApplication=Software&media=software&term=notes"
}
```

Four questions, in any combination, chosen by what you fill in:

| You want | Set | You get |
|---|---|---|
| The ranked list for a keyword | `keywords` | one `rank` row per position, to `depth` |
| Where **your** app sits | `keywords` + `apps` | one `rank-check` row per app × keyword × country × platform, with `rank`, the `totalResults` it was measured against, and `top3` naming who outranks it |
| What moved since last time | `monitor: true` | `rank-change` rows with `previousRank`, `rankChange`, `trend`, `bestRank`, `worstRank`, `daysTracked` |
| Keyword ideas | `includeSuggestions: true` | one `suggestion` row per autocomplete term the store's own search box offers |

Plus, always and free: one `keyword-summary` row per keyword × country ×
platform, and a typed `diagnostic` row for anything that could not be read.

***

### Try it in 30 seconds

**Input** — this is the Store prefill; paste it and run:

```json
{
  "keywords": ["meditation"],
  "countries": ["us"],
  "platforms": ["ios"],
  "depth": 20,
  "includeSuggestions": true,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

Three requests, well under 30 seconds: 20 ranking rows, 10 suggestion rows and
one free competition readout.

**Output** — one row per ranked position; the fields you will use most are
`keyword`, `country`, `platform`, `rank` and `appName` (full list under *Output
reference*). Anything that could not be read comes back as a free diagnostic row
(`ok: false`, `errorType`, `error`) instead of a charge.

**Price** — $0.30 per 1,000 ranking rows on the FREE tier (+ $0.001 per run,
charged only after the run returned a paid row); volume tiers are SILVER $0.24
and GOLD $0.18 per 1,000, autocomplete suggestions are $0.20 per 1,000, and the
competition readout, every diagnostic row, the detail lookups and a run that
returns no paid row at all are free. Pay-per-event, no API key, no browser,
limited permissions — works over the Apify MCP server (`mcp.apify.com`) and with
agentic (x402) payments.

**From code** —
`client.actor("insight.solutions/app-store-keyword-rank-tracker").call(run_input={…})`
with `apify-client`, or
`POST https://api.apify.com/v2/acts/insight.solutions~app-store-keyword-rank-tracker/run-sync-get-dataset-items`.

***

### Which endpoint this reads, and why it matters

Most "App Store rank" tools read `itunes.apple.com/search`. It is a public JSON
API, it is easy, and **it is not the store's ranking**.

This Actor's default reads
`search.itunes.apple.com/WebObjects/MZStore.woa/wa/search` — the endpoint the App
Store app on an iPhone queries, keyed on the `X-Apple-Store-Front` header. Here
is the difference, measured on the term `notes` in the US storefront, both lists
captured within seconds of each other:

| Measure | Value |
|---|---|
| Positions in Apple's own search | **247** |
| Positions in the iTunes Search API | 185 |
| Of Apple's top 50, present in the iTunes list | 35 |
| Of Apple's top 50, **absent from the iTunes list entirely** | **15** |
| Spearman ρ (over the 35 matched) | **0.880** |
| Kendall τ (over the 35 matched) | **0.738** |
| Same rank in both, out of 50 | 5 |
| Mean absolute rank difference | 22.9 positions |
| Top-10 overlap | 9 / 10 |
| Top-20 overlap | 16 / 20 |
| Top-50 overlap | 29 / 50 |

The two lists agree almost perfectly at the very top and then come apart. If your
app is at position 30, which endpoint you read decides whether you are at
position 30, position 53, or not on the list at all.

Reproduce it yourself — both of these are public and take no key:

```bash
curl -s 'https://search.itunes.apple.com/WebObjects/MZStore.woa/wa/search?clientApplication=Software&media=software&term=notes' \
  -H 'X-Apple-Store-Front: 143441,24 t:native' -H 'Accept: application/json' \
  | python3 -c 'import json,sys; d=json.load(sys.stdin); print([r["id"] for r in d["bubbles"][0]["results"]][:10])'

curl -s 'https://itunes.apple.com/search?term=notes&country=us&entity=software&limit=200' \
  | python3 -c 'import json,sys; d=json.load(sys.stdin); print([str(r["trackId"]) for r in d["results"]][:10])'
```

Apple's own search is also **32× cheaper per ranked position** — 12.5 KB of wire
for 247 positions against 396 KB for 185 — which is why this Actor can charge
$0.30 per 1,000 rankings and still read the better list.

Set `rankSource: "itunes-search-api"` if you want the other one. The row says
which you got, in `rankSource` and `source`.

***

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `keywords` | string\[] | `[]` | Up to 200, 200 characters each. Trimmed and de-duplicated case-insensitively; the row still shows what you typed, in `input`. |
| `apps` | string\[] | `[]` | Your apps. An App Store id (`1110145109`), a bundle id (`com.apple.mobilenotes`), an Android package (`com.google.android.keep`), an `apps.apple.com/…/id…` link or a `play.google.com/store/apps/details?id=…` link. Up to 50. Non-empty turns on the rank check. |
| `countries` | string\[] | `["us"]` | Two-letter ISO 3166-1. **Apple ranks: 54 verified storefronts** — see "Storefronts" below. Google Play accepts any code. Up to 25. |
| `platforms` | enum\[] | `["ios","android"]` | `ios`, `android`. |
| `depth` | integer | `50` | 1–250. iOS: one request returns the whole list, so depth is free. **Android: clamped to one search page** — see limits. |
| `rankSource` | enum | `"native"` | `native` (Apple's own store search) or `itunes-search-api`. iOS only. |
| `language` | string | `null` | Two letters. Apple: sent as `Accept-Language`; leave empty and Apple's own storefront default is used and reported back in `language`. Play: `hl=`; leave empty and it is derived from the country. |
| `includeTopApps` | boolean | `true` unless `apps` is set | Emit the ranked list as well as your own positions. |
| `includeSuggestions` | boolean | `false` | Autocomplete terms for each keyword used as a prefix. |
| `includeAppDetails` | boolean | `true` | Free. Fills name, developer, price, rating and genres on every position, from one public lookup per 100 positions — which is what keeps the `rating` column at one precision. Off means fewer requests and the same price. |
| `includeSummary` | boolean | `true` | The free `keyword-summary` readout. |
| `monitor` | boolean | `false` | Compare against this Actor's previous run and emit `rank-change` rows. |
| `emitUnchanged` | boolean | `true` | Monitor mode. `false` returns only what moved — the cheap way to run a daily alert. |
| `firstRunBehavior` | enum | `"emit-all"` | `emit-all` returns the first snapshot as rows; `baseline-only` records it silently and bills nothing. |
| `stateStoreName` | string | `"app-store-keyword-rank-state"` | The named key-value store the history lives in. |
| `residentialFallback` | boolean | `true` | Retry a refused Google Play page once through a residential exit. |
| `maxConcurrency` | integer | `3` | 1–10. Each slot keeps its own proxy session. |
| `maxRunSecs` | integer | `240` | 30–3600. |
| `maxRows` | integer | `5000` | Ceiling on **charged** rows. `0` means no ceiling. Free rows do not count. |
| `proxyConfiguration` | object | `{"useApifyProxy": true}` | Apify datacenter. See "Proxy". |

***

### Output reference

Every row carries all **74 columns**, `null` where they do not apply, so the
dataset exports as one rectangular table whatever mix of modes and platforms a
run produced.

**Envelope and query (14)** — `ok`, `rowType`, `input`, `keyword`, `prefix`,
`country`, `storefrontId`, `storefrontCc`, `platform`, `device`, `language`,
`rankSource`, `detailSource`, `checkedAt`

**Rank (6)** — `rank`, `rankStatus`, `checkedDepth`, `totalResults`,
`isTargetApp`, `lowRelevance`

**App identity and public detail (22)** — `appId`, `bundleId`, `appName`,
`subtitle`, `developer`, `developerId`, `developerUrl`, `price`, `currency`,
`priceFormatted`, `free`, `rating`, `ratingCount`, `genres`, `primaryGenre`,
`releaseDate`, `version`, `minimumOs`, `contentRating`, `hasInAppPurchases`,
`installs`, `deviceFamilies`

**Links (2)** — `iconUrl`, `appUrl`

**Change / monitor (9)** — `previousRank`, `previousCheckedAt`, `rankChange`,
`trend`, `firstSeenAt`, `lastSeenAt`, `bestRank`, `worstRank`, `daysTracked`

**Rank-check extra (1)** — `top3`

**Suggestion (3)** — `suggestion`, `position`, `suggestionUrl`

**Competition readout, free (11)** — `resultCount`, `top10AvgRating`,
`top10AvgRatingCount`, `top10MedianRatingCount`, `top10FreeShare`,
`top10Genres`, `titleMatchShare`, `subtitleMatchShare`, `trackedAppsRanked`,
`trackedAppsMissing`, `fuzzyFallbackDetected`

**Diagnostics and provenance (6)** — `error`, `errorType`, `scrapedAt`,
`source`, `sourceUrl`, `depthClamped`

Which row type fills which:

| Column group | `rank` | `rank-check` | `rank-change` | `suggestion` | `keyword-summary` | `diagnostic` |
|---|---|---|---|---|---|---|
| Envelope + query | all | all | all | the query half | all but `prefix`, `detailSource` | as far as known |
| Rank | all | all (`rank` may be `null`) | all | — | — | — |
| App identity + detail | all | all | all | — | — | — |
| Change / monitor | monitor mode only | monitor mode only | **all** | — | — | — |
| `top3` | — | **yes** | when the row is a tracked app | — | — | — |
| Suggestion | — | — | — | **all** | — | — |
| Competition readout | — | — | — | — | **all** | — |
| `error`, `errorType` | null | null | null | null | null | **set** |

`checkedAt` is when the search was made; `scrapedAt` when the row was built. They
differ when detail was filled from a batched lookup afterwards.

Six dataset views ship with it: **Rankings**, **Movers**, **My apps**,
**Suggestions**, **Competition** and **Problems**.

#### The free competition readout

One `keyword-summary` row per keyword × country × platform. It is a **readout,
not a score**: no 0–100 difficulty index, no popularity number, nothing you
cannot check by opening the store. `resultCount`, `totalResults`, the top-10
average rating and rating count, the free share, the genre mix, and what share of
the top 10 have every word of the keyword in their title or subtitle.

***

### Storefronts — 54 countries, every one verified

Apple identifies a storefront by a numeric id in a request header, and **an id it
does not recognise is not an error** — Apple silently serves the US storefront
with HTTP 200 and a full ranked list. A guessed id therefore does not fail
loudly; it fails as US ranks labelled with somebody else's country.

So every country below has been observed echoing back **its own country code and
its own id** in a captured response:

| | | | |
|---|---|---|---|
| `ae` 143481 | `ar` 143505 | `at` 143445 | `au` 143460 |
| `bd` 143490 | `be` 143446 | `br` 143503 | `ca` 143455 |
| `ch` 143459 | `cl` 143483 | `cn` 143465 | `co` 143501 |
| `cz` 143489 | `de` 143443 | `dk` 143458 | `eg` 143516 |
| `es` 143454 | `fi` 143447 | `fr` 143442 | `gb` 143444 |
| `gr` 143448 | `hk` 143463 | `hu` 143482 | `id` 143476 |
| `ie` 143449 | `il` 143491 | `in` 143467 | `it` 143450 |
| `jp` 143462 | `ke` 143529 | `kr` 143466 | `mx` 143468 |
| `my` 143473 | `ng` 143561 | `nl` 143452 | `no` 143457 |
| `nz` 143461 | `pe` 143507 | `ph` 143474 | `pk` 143477 |
| `pl` 143478 | `pt` 143453 | `ro` 143487 | `ru` 143469 |
| `sa` 143479 | `se` 143456 | `sg` 143464 | `th` 143475 |
| `tr` 143480 | `tw` 143470 | `ua` 143492 | `us` 143441 |
| `vn` 143471 | `za` 143472 | | |

Any other country gets a free `unknown-storefront` row and no charge. At runtime
the check runs again on every response: if the storefront Apple answered for is
not the one that was asked for, the rows are suppressed and the diagnostic says
so.

One footnote, because it is the only row that is not identical to the others:
**`bd` (Bangladesh) returns no apps at all** for the terms tried so far. Apple
answers, names the storefront it served, and returns an empty result set — so
that row is confirmed by the storefront id rather than by a country code, and
there are no results to mislabel either way. You get a free `no-results` row.

**Google Play needs none of this** and accepts any country code you send.

More Apple storefronts are added as they are verified — it is a one-line change
plus a captured response, and `test/verify-storefronts.mjs` prints the list the
fixtures support. The ones still missing are the small European and Gulf
storefronts (`lu`, `hr`, `sk`, `si`, `qa`, `kw`, `ee`, `lv`, `lt`, `bg`, `cy`,
`is`, `kz`) that no capture has reached yet.

***

### Rank history and monitoring

Set `monitor: true` and every position is compared against the one this Actor
stored on its previous run.

- **One data point per UTC day.** Run the same keyword twice in a day and the
  second run reports the *same* delta as the first, against yesterday — not a
  flat zero against an hour ago.
- **A failed check is never reported as a rank drop.** A blocked request, a
  timeout or an unrecognised storefront produces a free diagnostic and leaves the
  stored history untouched: no `dropped`, no `rank-change`, no bumped dates. An
  app that genuinely left the results is a different thing and *is* reported,
  with `rankStatus: "not-in-results"` and `trend: "dropped"`.
- **`emitUnchanged: false`** returns only what moved. Twenty keywords across two
  countries and two platforms with a 15 % churn is about twelve rows a day.
- **A run lock** stops two runs sharing one state store from diffing against the
  same snapshot and double-reporting every move.

#### Where the state lives

In a **named** key-value store (`stateStoreName`, default
`app-store-keyword-rank-state`), because an Actor's default store is created
fresh for every run and would never be seen by tomorrow's run.

Apify's permissions model lets a limited-permissions Actor "create any additional
storage, and write to that storage" and "read and write to storages created in
previous runs". The store this Actor opens is created by the Actor itself on its
first run and re-opened by its own later runs, which is exactly the permitted
case — so it ships with **limited permissions** and stays eligible for x402
agentic payments. If the store cannot be opened or read, the run degrades to
first-run behaviour and emits a free `state-unavailable` row; it never degrades
to "everything dropped".

Storage cost is one key-value write per keyword × country × platform per run —
about $0.00005 — not one per tracked app.

***

### Proxy

| Leg | Default | Why |
|---|---|---|
| Apple store search, hints, `itunes.apple.com` | Apify **datacenter** | Every capture answered HTTP 200 through it, and the no-proxy capture is byte-identical |
| Google Play search | Apify **datacenter**, with a one-page **residential** retry | Residential transfer costs 40× more per gigabyte, and a Play search page is 228 KB gzipped |

A refused Play page is retried from a fresh datacenter exit first; only if that
is refused too does the one page go out through residential. Set
`residentialFallback: false` to skip that and take a free `blocked` row instead,
or set `proxyConfiguration` to
`{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` to use residential
everywhere — it works, and on Android it costs about 40× more per row.

***

### Pricing

Pay per event. **$0.30 per 1,000 ranking rows.**

| Event | What it is | FREE | BRONZE | SILVER | GOLD |
|---|---|---|---|---|---|
| `actor-start` | Charged once per run, only after the run returned a paid row | $0.001 | $0.001 | $0.001 | $0.001 |
| **`rank`** | One app's position for one keyword, country and platform — a ranked-list row, a tracked-app check or a rank change | **$0.0003** | **$0.0003** | **$0.00024** | **$0.00018** |
| `suggestion` | One autocomplete term for one prefix, country and platform | $0.0002 | $0.0002 | $0.0002 | $0.0002 |

`rank` covers all three ranked row types. All three are one position read off one
search response; charging them differently would be pricing the label, not the
work.

| Run | Rows | Cost |
|---|---|---|
| The quick start above | 20 `rank` + 10 `suggestion` + 1 free readout | **$0.0090** |
| Daily monitor: 20 keywords × 2 countries × 2 platforms, `emitUnchanged: false` | ~12 `rank-change` | **$0.0046/day ≈ $0.14/month** |
| Sweep: 50 keywords × 5 countries × 2 platforms, depth 50 / ~30 | ~20,000 `rank` | **$6.00** |

#### What you are never charged for

- The `keyword-summary` competition readout — free on every run.
- Every diagnostic row: no results, unknown storefront, blocked, app not found,
  invalid input, timeout, upstream unavailable, baseline, state unavailable.
- The extra lookup that fills the detail on every ranked position, and the
  lookup that resolves a bundle id to an app id.
- Positions suppressed by `emitUnchanged: false`.
- The bookkeeping of a `baseline-only` first run.
- **A run that returns no paid row at all.** It finishes FAILED with a clear
  status message and bills nothing, the start fee included.

`ACTOR_MAX_TOTAL_CHARGE_USD` is honoured: when the budget can no longer cover the
next batch the run stops reading keywords, finishes SUCCEEDED, and says so in the
status message.

***

### Limits, and the ones that might bite

- **iOS depth is whatever Apple's search returns** — 247 results for `notes` in
  the US, 248 in the UK. There is no paging: `&page=2` returns the identical
  list, which is why this Actor never sends it.
- **Android depth is one search page: 30 results for `notes` in the US, 14 for
  `notizen` in Germany.** Google Play ships no continuation token with the page,
  so a higher `depth` is clamped to what the page gave and the summary row says
  `depthClamped: true`.
- **Apple's own search carries result detail for the top 8 positions only**, and
  it rounds ratings to the half star (a flat `5`, a flat `4.5`). So every ranked
  position is filled from the public `lookup` endpoint instead — one request per
  100 positions, free to you — and the whole `rating` column comes out at one
  precision (`4.84119`, not `5`). Apple's own record is kept for the three
  fields the lookup has no column for — `subtitle`, `hasInAppPurchases` and
  `deviceFamilies` — and as the fallback for an app the lookup does not return.
  `detailSource` on every row says which of the two its numbers came from.
  Setting `includeAppDetails: false` skips the lookup entirely: positions 1–8
  then carry Apple's rounded values and positions 9+ an id and a store link
  only.
- **Apple returns 9 low-relevance apps instead of nothing** for a query that
  matches nothing. This Actor detects that — 12 or fewer results, none of whose
  name or subtitle contains any part of the keyword — reports no results, and
  charges nothing, rather than publishing them as ranks 1 to 9. A thin but
  genuine result set is delivered and flagged `lowRelevance: true` instead.
- **Android search cards carry no rating count, no subtitle, no version, no
  release date and no content rating.** Those columns are null on Android rows,
  and the competition readout leaves its rating-count and subtitle fields null
  rather than filling them with a guess.
- **iPad and Mac ranks are not in this version.** Apple's native endpoint ignores
  `platform=ipad` — the capture is identical in order to the iPhone one — so an
  iPad rank would have to come from a different, differently-ordered endpoint.
  Every iOS row here is an iPhone storefront rank and says so in `device`.
- **Google Play top charts are not available.** Play's chart RPC answers
  `PlayDataError` and the collection pages ship no app data. This Actor does
  keyword search only.
- **Apple ranks are available for 54 storefronts**, every one verified against a
  captured response. See "Storefronts" for the list and for why a country is
  refused rather than guessed.
- Ranks are a reading at one instant, from one storefront, through one exit IP.
  Two readings minutes apart can differ.
- The upstream format may change. Every parser here is pinned by tests against
  captured pages, so a change shows up as a failing test rather than a silent
  wrong number.

***

### FAQ

**Is this Apple's Search Popularity score?**
No, and nothing here claims to be. That figure lives behind an authenticated
Apple Search Ads account and is not public. The competition readout in this Actor
is arithmetic over the ranked list it just returned, it is free, and every number
in it can be checked by opening the store.

**Why is there no keyword difficulty score?**
Because it would be a number we made up. Every "difficulty" column in this
category is a vendor's private weighting of the same public inputs. You get the
inputs — result count, top-10 ratings, rating counts, free share, genre mix,
title-match share — and you can weight them however your business actually works.

**Why 54 countries and not every one Apple lists?**
Because a storefront id that Apple does not recognise is served the US
storefront, silently. Shipping a table of plausible ids would mean shipping US
ranks labelled `de`. The 54 here are the ones a captured response confirmed, id
and country code both. The list grows as more are confirmed.

**Which rank source should I use?**
`native` unless you have a reason. It is the list the store shows, it is cheaper
to read, and it goes deeper. Use `itunes-search-api` if you need to reconcile
against a tool that reads that endpoint.

**Does it work without a proxy?**
Apple does. Google Play is much likelier to refuse an unproxied address. The
default datacenter proxy is the right setting for almost everyone.

**Can I run it every day?**
Yes — that is what `monitor: true` and `emitUnchanged: false` are for. Schedule
it, and each run returns only what moved.

***

### Use it from an AI agent, or from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor
runs with **limited permissions**, uses **pay-per-event** pricing and never
enters Standby, so it works over the Apify MCP server and with x402 agentic
payments. The **Integrations** tab pushes results to Slack, a webhook, Zapier,
Make, Google Sheets, Snowflake or BigQuery.

```bash
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~app-store-keyword-rank-tracker/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords":["meditation","sleep tracker"],"countries":["us"],"platforms":["ios","android"],"depth":30}'
```

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/app-store-keyword-rank-tracker").call(run_input={
    "keywords": ["meditation", "sleep tracker", "breathing exercises"],
    "apps": ["com.apple.mobilenotes", "com.google.android.keep"],
    "countries": ["us", "gb"],
    "platforms": ["ios", "android"],
    "depth": 50,
    "monitor": True,
    "emitUnchanged": False,
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row.get("ok") and row["rowType"] in ("rank", "rank-check", "rank-change"):
        print(row["keyword"], row["country"], row["platform"],
              row["rank"], row["rankChange"], row["appName"], sep=" | ")
```

Set `monitor: true` on a schedule and the run becomes a rank tracker: each run
compares against the last and tells you what moved.

***

### Legal and data-protection notes

- Public store listings only. No login, no account, no cookie that identifies
  anyone, no API key of anyone's.
- No personal data is collected. Developer names are the publisher names the
  stores print on a public product page.
- Every row carries the exact public URL it was read from, in `sourceUrl`.
- App names, icons and store content belong to their publishers. This Actor
  returns references to public listings; what you do with them is your call and
  your responsibility.

***

### Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

**Video, audio & social**

- [YouTube Transcript API](https://apify.com/insight.solutions/youtube-transcript-api) — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- [YouTube Comments API](https://apify.com/insight.solutions/youtube-comments-api) — comments and replies with likes, pinned and hearted flags, newest or top sort.
- [YouTube Channel API](https://apify.com/insight.solutions/youtube-channel-api) — a channel's videos, Shorts and live streams, plus YouTube search.
- [Podcast Search, Episodes & Charts API](https://apify.com/insight.solutions/podcast-api) — Apple Podcasts search, charts and full episode feeds.
- [Bluesky Scraper](https://apify.com/insight.solutions/bluesky-scraper) — profiles, posts, followers and follows from the public AT Protocol API.
- [Telegram Channel Scraper](https://apify.com/insight.solutions/telegram-channel-scraper) — posts, views and channel stats from public Telegram channels.
- [Substack Scraper](https://apify.com/insight.solutions/substack-scraper) — posts with full free text, comments and publication profiles.
- [Hacker News API](https://apify.com/insight.solutions/hacker-news-api) — stories, comments, users, front page and a structured "Who is hiring?" parser from the official HN APIs.

**News, documents & the web**

- [Google News Search, Topics & Real Article URLs](https://apify.com/insight.solutions/google-news-api) — news search and topic feeds with the publisher's real URL decoded.
- [Website to Markdown — Content Extractor for LLMs & RAG](https://apify.com/insight.solutions/website-content-extractor) — any site as clean Markdown, text and heading-aware chunks.
- [Internet Archive API](https://apify.com/insight.solutions/internet-archive-api) — archive.org search, item metadata, files and reviews.
- [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) — archived URL inventories, snapshots and text diffs between dates.
- [Website Technology Detector](https://apify.com/insight.solutions/website-tech-detector) — the tech stack behind any site, with the evidence for each detection.
- [Domain Intelligence API](https://apify.com/insight.solutions/domain-intelligence-api) — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- [SEO Page Audit](https://apify.com/insight.solutions/seo-page-audit) — sitemap crawl with on-page checks, structured data and broken-link reports.
- [Keyword Suggestions API](https://apify.com/insight.solutions/keyword-suggestions-api) — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) — emails, phone numbers and social profiles from any list of websites.
- [Web Search Results API](https://apify.com/insight.solutions/web-search-api) — Bing and DuckDuckGo organic results with snippets, no key, no browser.
- [Company Enrichment API](https://apify.com/insight.solutions/company-enrichment-api) — a domain in, a company profile out: firmographics, contacts, tech stack, DNS and hiring signal.

**Business, finance & jobs**

- [Congress & Insider Trades API](https://apify.com/insight.solutions/congress-insider-trades-api) — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- [Federal Contracts, Grants & Lobbying API](https://apify.com/insight.solutions/federal-contracts-grants-api) — SAM.gov opportunities, USAspending awards, Grants.gov notices and Senate lobbying filings in one schema.
- [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) — filings, XBRL financials and full-text search by ticker or CIK.
- [Clinical Trials & FDA API](https://apify.com/insight.solutions/clinical-trials-fda-api) — ClinicalTrials.gov studies plus openFDA recalls, labels, approvals, 510(k)s and adverse-event reports.
- [Product & Vehicle Recalls API](https://apify.com/insight.solutions/product-recalls-api) — CPSC, NHTSA, FDA and USDA recalls, vehicle complaints and ratings, plus a VIN decoder.
- [Y Combinator Companies, Batches & Founders](https://apify.com/insight.solutions/yc-companies-directory) — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor) — new, closed and changed postings on the career sites you watch.
- [Remote Jobs API](https://apify.com/insight.solutions/remote-jobs-api) — RemoteOK, Remotive, We Work Remotely, Himalayas, Jobicy and more in one schema, deduplicated.
- [Shopify Products API](https://apify.com/insight.solutions/shopify-products-api) — any Shopify store's catalogue, variants, prices and stock signals.
- [Shopify Store Monitor](https://apify.com/insight.solutions/shopify-store-monitor) — price drops, sales, restocks, sell-outs and new products on any Shopify store, one row per change.

**Apps & games**

- [App Store & Google Play Reviews API](https://apify.com/insight.solutions/app-reviews-api) — reviews from both stores with ratings, versions and developer replies.
- [App Store Top Charts & App Search API](https://apify.com/insight.solutions/app-charts-api) — Apple top charts by country and genre, plus app search and details.
- [Steam Reviews API](https://apify.com/insight.solutions/steam-reviews-api) — Steam reviews with playtime, helpfulness and game details.
- [Steam Game Data API](https://apify.com/insight.solutions/steam-store-stats-api) — prices, tags, review scores, live player counts and top charts.

# Actor input Schema

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

The search terms to read rankings for, e.g. `["meditation", "sleep tracker"]`. Each one is looked up in every country and on every platform you selected. Trimmed and de-duplicated case-insensitively; up to 200 per run, 200 characters each.

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

Two-letter ISO 3166-1 country codes. **Apple ranks are available for 54 storefronts** — every one whose id this build has verified against a captured response, including us, gb, de, fr, it, es, jp, kr, cn, in, br, mx, ca, au and 40 more. A country outside that list gets a free `unknown-storefront` row rather than another country's ranks by mistake. Google Play accepts any country code. Up to 25.

## `platforms` (type: `array`):

Which stores to read. Both produce the same columns, so a run over both is one table.

## `depth` (type: `integer`):

How many ranked positions to read per keyword. Apple's own search returns the whole list in one request (247 results for `notes` in the US), so depth on iOS costs nothing extra. **Google Play ships one search page — about 30 results in the US, 14 in Germany** — so a higher depth is clamped to what the page gave and the summary row says `depthClamped: true`.

## `rankSource` (type: `string`):

`native` is Apple's own store search — the endpoint the App Store app itself queries, and the default. `itunes-search-api` is the public iTunes Search API that most of this category calls "App Store rankings"; on the term `notes` in the US the two agree at ρ = 0.880, and 15 of the native top 50 are absent from the iTunes list entirely. Ignored on Android.

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

Apps whose position you want for every keyword. Any of: an App Store id (`1110145109`), a bundle id (`com.apple.mobilenotes`), an Android package (`com.google.android.keep`), an `apps.apple.com/…/id…` link or a `play.google.com/store/apps/details?id=…` link. Leave empty to get the ranked list instead. Up to 50.

## `language` (type: `string`):

Two-letter language code. On Apple it is sent as `Accept-Language`; leave it empty and Apple resolves the storefront's own default, which the row reports in `language`. On Google Play it is `hl=`; leave it empty and it is derived from the country (`gl=DE` → `hl=de`).

## `includeTopApps` (type: `boolean`):

Emit one row per ranked position, not just your own apps' positions. On by default when `apps` is empty; turn it on alongside `apps` to get the competitive field and your positions in one run.

## `includeSuggestions` (type: `boolean`):

Ask each store's own search box what it suggests for each keyword used as a prefix. Apple returns 10 terms per prefix; Google Play returns as many as it has. Charged as `suggestion` rows.

## `includeAppDetails` (type: `boolean`):

Fill name, developer, price, rating, genres and links on every ranked row. Free. Apple's own search carries detail for the top 8 results only and rounds ratings to the half star, so every ranked position is filled from a public lookup instead — one request per 100 positions, inside the same row price — which is what keeps the rating column at one precision. Turn it off for fewer requests and the same price.

## `includeSummary` (type: `boolean`):

One free `keyword-summary` row per keyword, country and platform: result count, top-10 average rating, free share, genre mix and how many of the top 10 have the keyword in their title. Arithmetic over this run's own rows — no difficulty score, no popularity index.

## `monitor` (type: `boolean`):

Compare every position against the one this Actor stored on its previous run and emit `rank-change` rows carrying `previousRank`, `rankChange`, `trend`, `bestRank`, `worstRank` and `daysTracked`. State lives in a named key-value store this Actor creates and re-opens. A check that failed is never reported as a rank drop.

## `emitUnchanged` (type: `boolean`):

Monitor mode only. Leave on for a full picture every run. Turn it off and only positions that moved, entered or dropped out are emitted — the cheap way to run a daily alert.

## `firstRunBehavior` (type: `string`):

Monitor mode only. `emit-all` returns the first snapshot as ordinary rows, because a first rank reading is the product. `baseline-only` writes the state and bills nothing — which means a run with nothing else in it finishes FAILED and costs you nothing at all.

## `maxRows` (type: `integer`):

Hard ceiling on charged rows. 0 means no ceiling. Free rows — the competition readout and every diagnostic — do not count against it.

## `maxRunSecs` (type: `integer`):

Wall-clock budget for the whole run. Keywords not reached in time get a free `timeout` row and everything already returned is kept.

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

How many keyword × country × platform reads run at once. Each slot keeps its own proxy session, so one blocked exit does not burn the others.

## `residentialFallback` (type: `boolean`):

When a Google Play page is refused from a datacenter exit even after a new IP is taken, retry that one page through a residential exit. Residential transfer costs 40× more per gigabyte, so it is used for the page that was refused and nothing else. Turn it off to go straight to a free `blocked` row.

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

Apify datacenter proxy by default. Apple answers through it, and through no proxy at all; Google Play is read through it in production and falls back to a residential exit for a page that is refused. Set `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` to use residential everywhere — it works, and it costs 40× more per gigabyte on the Play leg.

## `stateStoreName` (type: `string`):

The named key-value store the rank history lives in. Change it to run two independent trackers under one account.

## Actor input object example

```json
{
  "keywords": [
    "meditation"
  ],
  "countries": [
    "us"
  ],
  "platforms": [
    "ios"
  ],
  "depth": 20,
  "rankSource": "native",
  "apps": [],
  "includeTopApps": true,
  "includeSuggestions": true,
  "includeAppDetails": true,
  "includeSummary": true,
  "monitor": false,
  "emitUnchanged": true,
  "firstRunBehavior": "emit-all",
  "maxRows": 5000,
  "maxRunSecs": 240,
  "maxConcurrency": 3,
  "residentialFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "stateStoreName": "app-store-keyword-rank-state"
}
```

# Actor output Schema

## `results` (type: `string`):

Ranked positions, tracked-app checks, rank changes, autocomplete suggestions, free per-keyword competition readouts and free diagnostics, in one flat table. Delivered as JSON items in the default dataset.

# 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": [
        "meditation"
    ],
    "countries": [
        "us"
    ],
    "platforms": [
        "ios"
    ],
    "depth": 20,
    "rankSource": "native",
    "apps": [],
    "includeTopApps": true,
    "includeSuggestions": true,
    "includeAppDetails": true,
    "includeSummary": true,
    "monitor": false,
    "firstRunBehavior": "emit-all",
    "maxRows": 5000,
    "maxRunSecs": 240,
    "maxConcurrency": 3,
    "residentialFallback": true,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("insight.solutions/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": ["meditation"],
    "countries": ["us"],
    "platforms": ["ios"],
    "depth": 20,
    "rankSource": "native",
    "apps": [],
    "includeTopApps": True,
    "includeSuggestions": True,
    "includeAppDetails": True,
    "includeSummary": True,
    "monitor": False,
    "firstRunBehavior": "emit-all",
    "maxRows": 5000,
    "maxRunSecs": 240,
    "maxConcurrency": 3,
    "residentialFallback": True,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("insight.solutions/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": [
    "meditation"
  ],
  "countries": [
    "us"
  ],
  "platforms": [
    "ios"
  ],
  "depth": 20,
  "rankSource": "native",
  "apps": [],
  "includeTopApps": true,
  "includeSuggestions": true,
  "includeAppDetails": true,
  "includeSummary": true,
  "monitor": false,
  "firstRunBehavior": "emit-all",
  "maxRows": 5000,
  "maxRunSecs": 240,
  "maxConcurrency": 3,
  "residentialFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call insight.solutions/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,insight.solutions/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/QbGvfWAppFydZKAvC/builds/y8cDPhFKcVelOffAB/openapi.json
