App Store Keyword Rank Tracker — iOS & Google Play avatar

App Store Keyword Rank Tracker — iOS & Google Play

Pricing

from $0.18 / 1,000 ranking rows

Go to Apify Store
App Store Keyword Rank Tracker — iOS & Google Play

App Store Keyword Rank Tracker — iOS & Google Play

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.

Pricing

from $0.18 / 1,000 ranking rows

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

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:

{
"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 wantSetYou get
The ranked list for a keywordkeywordsone rank row per position, to depth
Where your app sitskeywords + appsone 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 timemonitor: truerank-change rows with previousRank, rankChange, trend, bestRank, worstRank, daysTracked
Keyword ideasincludeSuggestions: trueone 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:

{
"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 codeclient.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:

MeasureValue
Positions in Apple's own search247
Positions in the iTunes Search API185
Of Apple's top 50, present in the iTunes list35
Of Apple's top 50, absent from the iTunes list entirely15
Spearman ρ (over the 35 matched)0.880
Kendall τ (over the 35 matched)0.738
Same rank in both, out of 505
Mean absolute rank difference22.9 positions
Top-10 overlap9 / 10
Top-20 overlap16 / 20
Top-50 overlap29 / 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:

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

FieldTypeDefaultNotes
keywordsstring[][]Up to 200, 200 characters each. Trimmed and de-duplicated case-insensitively; the row still shows what you typed, in input.
appsstring[][]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.
countriesstring[]["us"]Two-letter ISO 3166-1. Apple ranks: 54 verified storefronts — see "Storefronts" below. Google Play accepts any code. Up to 25.
platformsenum[]["ios","android"]ios, android.
depthinteger501–250. iOS: one request returns the whole list, so depth is free. Android: clamped to one search page — see limits.
rankSourceenum"native"native (Apple's own store search) or itunes-search-api. iOS only.
languagestringnullTwo 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.
includeTopAppsbooleantrue unless apps is setEmit the ranked list as well as your own positions.
includeSuggestionsbooleanfalseAutocomplete terms for each keyword used as a prefix.
includeAppDetailsbooleantrueFree. 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.
includeSummarybooleantrueThe free keyword-summary readout.
monitorbooleanfalseCompare against this Actor's previous run and emit rank-change rows.
emitUnchangedbooleantrueMonitor mode. false returns only what moved — the cheap way to run a daily alert.
firstRunBehaviorenum"emit-all"emit-all returns the first snapshot as rows; baseline-only records it silently and bills nothing.
stateStoreNamestring"app-store-keyword-rank-state"The named key-value store the history lives in.
residentialFallbackbooleantrueRetry a refused Google Play page once through a residential exit.
maxConcurrencyinteger31–10. Each slot keeps its own proxy session.
maxRunSecsinteger24030–3600.
maxRowsinteger5000Ceiling on charged rows. 0 means no ceiling. Free rows do not count.
proxyConfigurationobject{"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 grouprankrank-checkrank-changesuggestionkeyword-summarydiagnostic
Envelope + queryallallallthe query halfall but prefix, detailSourceas far as known
Rankallall (rank may be null)all
App identity + detailallallall
Change / monitormonitor mode onlymonitor mode onlyall
top3yeswhen the row is a tracked app
Suggestionall
Competition readoutall
error, errorTypenullnullnullnullnullset

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

LegDefaultWhy
Apple store search, hints, itunes.apple.comApify datacenterEvery capture answered HTTP 200 through it, and the no-proxy capture is byte-identical
Google Play searchApify datacenter, with a one-page residential retryResidential 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.

EventWhat it isFREEBRONZESILVERGOLD
actor-startCharged once per run, only after the run returned a paid row$0.001$0.001$0.001$0.001
rankOne 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
suggestionOne 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.

RunRowsCost
The quick start above20 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.

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


  • 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

News, documents & the web

Business, finance & jobs

Apps & games