TikTok Shop Keyword Rank Tracker avatar

TikTok Shop Keyword Rank Tracker

Pricing

Pay per event

Go to Apify Store
TikTok Shop Keyword Rank Tracker

TikTok Shop Keyword Rank Tracker

Track where your TikTok Shop products rank in search for the keywords that matter, every day. Exact rank positions, movement vs the previous run, share of search, and the competitor listings that outrank you. Filters out TikTok's search volatility so you only see real movement.

Pricing

Pay per event

Rating

0.0

(0)

Developer

Marielise

Marielise

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Find out where your products actually rank in TikTok Shop search - and watch that position move every day.

You know your sales dropped last week. You do not know that you fell from position 3 to position 14 for "hair growth serum", or that a $9 competitor listing now sits in the slot you used to own. This Actor tells you both, every morning, for every keyword you care about.

Every other TikTok Shop tool hands you a pile of search results, or a leaderboard of whatever is selling well globally. This one answers the only question a seller actually asks: where am I, and is it getting better or worse?


What you get

Give it your keywords and your shop name. It returns, per keyword:

FieldWhat it tells you
Your rankYour exact position in TikTok Shop search results
What happenedOne sentence: Climbed 4 to #6, Vanished (was #3), Too noisy to compare
Do thisThe next action, in the imperative: Check the listing is live and in stock
Usual rankYour median position across recent checks - the number to actually steer by
Shows up %How often you appeared at all. Low means unstable visibility, not a stable rank
Change+5 means you climbed five places, -3 means you slipped
Who took your spotThe exact listing that took your slot, with its price, rating and sold count
New competitors aboveListings that appeared ahead of you since last check, with their price
Your price gapHow far above or below the median price of the listings beating you
Keyword in your titleWhether your title even contains the phrase you are ranking for
Shelf spaceHow many of the slots read belong to you
Competitor listingsEvery ranked listing, as its own row - sort by price or sold count

Plus a ready-to-read RANK_REPORT.md that leads with what needs attention, a spreadsheet-friendly rank_positions.csv, and a full JSON export.

Six Console views ship with it: Needs Action (start here), Rank Positions, Biggest Movers, Competitor Listings, Price & Market Position, and Run Summary.

Alerts, so you do not have to read a dataset

A scheduled tracker is only useful if it tells you when something happened. Each run produces a short, ranked list - most urgent first:

  • "hair growth serum: Dropped out of the top 20 entirely (was #3). Buyers searching this keyword can no longer find you. First check: is the listing still active and in stock?"
  • "wireless earbuds: Fell out of the top 5, from #4 to #15. The new #4 is Bluetooth Pro Buds (SkyAudio) at $22.99, and you are $7 above the median price of the listings above you."
  • "led strip lights: New competitor ahead of you at #2: Neon Rope Light 5m (GlowLab Official) at $9.99."
  • "tws earbuds: Absent this check, and showing up in only 33% of the last 9 checks (typically #9 when you do appear). Coming and going costs more than a stable lower position."
  • "GlowLab Official is ahead of you on 6 of your keywords. One rival across several keywords is a positioning problem, not a keyword problem."

Every alert says what happened, why it matters, and the first thing to check. Where the run knows the cause it names it: a rival who undercut you on price and a rival who simply outranked you at the same price call for different responses, and the alert carries the price gap so you can tell which one you are looking at.

Crucially, alerts do not fire on noise. TikTok Shop reshuffles its results constantly, and an alert that cries wolf trains you to ignore it. If the result page largely turned over, or a different data source served that keyword, the movement is marked untrustworthy and stays silent.

They are also written to the strength of the evidence behind them. An absence on a keyword the rolling history has seen coming and going does not claim buyers can no longer find you - it says the absence is within that keyword's normal range and to watch for a second one. A competitor is only called new if no recent run has seen it, and is only called your displacer if it climbed past you rather than merely landing on your old slot number.


Why rank tracking beats another product scraper

TikTok Shop search is where discovery happens, and position decides whether a buyer ever sees you. Amazon sellers have tracked keyword rank for a decade with tools like Helium 10 and Jungle Scout. TikTok Shop sellers have had nothing equivalent - the existing tools show you global bestseller leaderboards, not your position for your keywords.

A snapshot tells you nothing. Movement is the entire product. Ranking #7 is neither good nor bad; falling from #2 to #7 in three days while a new competitor climbs is an emergency, and climbing from #30 to #7 means the change you shipped last week worked.

That is why this Actor keeps history between runs. Give it a Tracking ID, schedule it daily, and each run compares itself against the last one automatically.


Who this is for

Built for whoever is responsible for TikTok Shop visibility across one or more shops, and comfortable scheduling an Apify run and reading a dataset. If that is not you yet, ask whoever runs your automations to set it up - it takes five minutes.

  • Ecommerce agencies: report keyword visibility to clients with real position data, one tracking ID per client, and open one view each morning to see which accounts need you today.
  • In-house growth and marketing leads: prove whether your listing changes, price moves or ad spend actually improved visibility, instead of guessing from revenue.
  • Brand marketplace managers: watch resellers and dupes climb on your own brand keywords before they eat your category.
  • Product researchers: leave the shop name empty and measure how contested a keyword really is before committing inventory.

How to use it

  1. Add the keywords your buyers type - start with 10 to 30 that matter, not everything you can think of.
  2. Add your shop name (easiest) or specific product IDs (most precise).
  3. Set a Tracking ID and keep it stable. This is what links runs into a history.
  4. Schedule it daily. The first run establishes your baseline; every run after reports movement.

United States only. The search data sources advertise other marketplaces, but they were measured returning the US catalogue under a foreign label, or nothing at all. A rank measured against the wrong market is worse than no rank, so only the verified region is offered.

Leave the shop name empty and you still get the full ranked leaderboard for each keyword - useful for sizing up a keyword before you enter it.


How the data is sourced, and what it costs

This Actor does not scrape TikTok itself, and that is a deliberate design decision.

TikTok Shop's search page cannot be read directly. Its only reachable search page server-renders no products at all, the API behind it requires a signature minted by TikTok's own in-browser anti-crawler code, and a real browser on residential proxies still hits a slider CAPTCHA. Anyone claiming otherwise is either using a signed mobile endpoint or paying to solve CAPTCHAs.

So this Actor buys ranked search results from an established TikTok Shop search Actor that has already solved that problem, and spends all of its own effort on the part nobody else has built: rank history, movement, displacement and volatility filtering. The scrape was never the differentiator.

Two consequences you should know before you run it:

  • You are billed twice. The upstream search Actor runs on your account and charges you separately, on top of this Actor's own charge. Budget for both. Note that Apify's run-level Max total charge caps this Actor only - it does not cap the child runs it starts. This Actor therefore bounds the upstream itself: it sizes one spend ceiling for the whole fetch and shares it across every call, including retries and the fallback source, and publishes the total it authorised as meta.upstreamChargeAuthorizedUsd alongside the child run IDs.
  • Two sources are supported. If the default one fails, returns nothing, or stops publishing rank positions, the other is tried automatically. A rank tracker that dies because one upstream had a bad day is not a tracker.

What you pay for

This Actor bills one flat charge per completed rank check, plus one event per keyword it actually tracked: nothing else.

EventPriceCharged when
Rank check$0.05 per runOnce, on every run that reaches the search source
Keyword tracked$0.012 per keywordThat keyword was successfully checked
You are not charged for
Keywords that could not be checked
The run summary row
Competitor listing rows
A run rejected before it searched anything, such as unedited template input

A run the source blocks costs you the $0.05 rank check and nothing else. Every keyword on it is free. This is deliberate and it is a change from earlier versions, which made blocked runs entirely free: a run that collects nothing still occupies a container for as long as a successful one, and a price that collapses to zero exactly when the source is having a bad day is not a price. If you want a hard ceiling on what a scheduled run can ever cost you, set Max total charge on the run or the schedule.

So this Actor charges $0.41 per run for a 30-keyword daily tracker, and $0.17 for a 10-keyword weekly check. That is not your whole bill - the search source charges you separately, and it is the bigger number. See the table below before you schedule anything.

Charging per keyword rather than per row is what makes a scheduled run predictable: asking for 30 keywords costs the same whether each one returns 10 competitor listings or 100. It also means the number on your invoice matches the number you put in the input.

The source Actor's bill, in numbers

This is the larger half of your cost, so here it is explicitly. The source charges per product row returned, and rows = keywords × scan depth.

The row price depends on your Apify plan. trakk is tiered: $2.00 per 1,000 rows on Free and $1.60 on Bronze, falling to $1.22 on Gold and above. pro100chok is a flat $2.00 per 1,000 on every plan. The table below quotes $2.00 per 1,000, which is what a Free or Bronze account actually pays. On Gold or above, the source column drops by about 39%.

Run shapeRowsSource costThis ActorAll-in (Free/Bronze)
10 keywords × depth 20, weekly200~$0.40$0.17~$0.57/run, ~$2.50/mo
30 keywords × depth 20, daily600~$1.20$0.41~$1.61/run, ~$48/mo
30 keywords × depth 50, daily1,500~$3.00$0.41~$3.41/run, ~$102/mo
200 keywords × depth 10020,000~$40.00$2.45~$42.45/run

Scan depth is the lever most people miss: it multiplies the source bill one-for-one while this Actor's price stays flat. Depth 20 covers the first screen almost every buyer ever sees. Going from depth 20 to depth 50 on 30 keywords costs you an extra $54/month and tells you nothing new unless you actually rank below 20.

Prices move; check both source Actors' Store pages before committing to a schedule. Every run publishes meta.upstreamChargeAuthorizedUsd - the ceiling this Actor authorised for the source, which is deliberately above the expected cost, not the amount spent. The child runs' own pages show what they actually billed.

Your upstream cost scales with keywords × scan depth, since that decides how many product rows the source Actor returns. Scan depth 20 covers the first screen almost every buyer ever sees; raise it only if you rank deep. Max keywords per run is a hard ceiling that protects a scheduled run from a runaway keyword list. Apify's run-level Max total charge caps what this Actor bills - if it is reached, this Actor tells you exactly how many keywords were billed and saved rather than silently truncating, and the per-run base fee is taken last so a tight cap costs you the fee rather than your results. That setting does not reach the child runs, so the upstream is capped separately by this Actor.


Honest notes on accuracy

TikTok Shop search is genuinely volatile. This was measured, not assumed: two runs minutes apart on the same keyword returned a completely different top 10, while another pair returned an identical one. TikTok personalises and reshuffles results per session.

A naive rank tracker would scream "YOU DROPPED OUT" every time this happened. This one deals with it in two ways.

First, it measures how much of the page turned over and marks the movement as untrustworthy when the result set was effectively replaced - those keywords are separated into a "read these with caution" section, excluded from the improved/declined headline counts, given no "displaced by" villain, and fire no alerts. The position is always reported; only the movement is qualified.

Second, and more useful day to day, it keeps a rolling history and reports your median rank across recent runs plus a direction derived from comparing the recent half of that window against the older half. One noisy day cannot move a median, and no direction is claimed at all until there are enough runs to support one. This is the number to build decisions on.

Some runs come back partial, and the Actor says so rather than hiding it. TikTok pushes back on the search sources, so a run occasionally returns nothing for a keyword, or fewer results than asked for. Keywords with no data are reported with measurement: "no_results" and are not billed; a run that collects nothing at all publishes no keyword rows and bills no keyword events, only the $0.05 rank check. Confidence and the quick take always reflect how much was actually measured, so a bad day at the source never reads as a change in your rankings.

Scan depth is requested, not guaranteed. The source returns a variable number of results per keyword - asking for 20 sometimes yields 10, and this varies run to run for reasons on TikTok's side. Every record reports scan.resultsRead (rows that were usable) and scan.deepestRankSeen (how far down the page the source actually reached) alongside your requested depth, so you always know which claim the data supports.

"Not ranking" and "not in the results we read" are different claims. If the source returns fewer results than you asked for, that is reported (scan.wasShallow) and you are told so explicitly, rather than being informed you do not rank when you might simply sit below the depth that was actually read.

If your shop name matches nothing, it says so and helps. The most common setup mistake is a shop name that does not match the seller string TikTok displays. Rather than reporting a silent wall of nulls, the Actor surfaces the closest seller names it actually saw in the results, so you can copy the right one.

A failed keyword is never reported as a drop-out. If a keyword could not be checked, it is excluded from the results entirely rather than published as rank null, because a false drop-out alarm is worse than no data.

A blocked run charges you nothing. If the source returns nothing at all, this Actor charges no events and publishes a single run_status row explaining what happened and what to try, instead of empty keyword rows that a dashboard would render as "you dropped out". That one row is deliberate - a run finishing with an empty dataset is indistinguishable from a broken Actor to Apify's daily health check.

Fields are null when the data is absent, never 0. A listing with no rating is null, not 0.0 - TikTok's scale starts at 1 once anyone rates, so 0 means "unrated", and publishing it as a score would describe a brand-new listing as terrible.

When a check cannot be compared, it says why. comparison.reason names the actual cause - first_check, source_changed, page_reshuffled, scan_too_shallow, last_check_too_old - instead of collapsing every one of them into a single "not comparable" flag you cannot act on.

The rolling history spans data sources; the run-to-run comparison does not. That is deliberate, and now visible: historyCoversSources lists which sources are behind the median, so a usual rank of #2 across eleven checks sitting beside an empty lastRank reads as two different populations rather than as a contradiction.

It runs with limited permissions, and so must the sources it calls. This Actor uses Apify's default LIMITED_PERMISSIONS scope, which is enough for what it does: start another Actor and read its results, and read and write its own named storage. A limited-permission Actor can only call other limited-permission Actors, so if a search source is ever republished with full permissions the call fails - and that failure is reported as a source problem rather than mislabelled as a block by TikTok.

Every run writes RUN_DIAGNOSTICS.json recording which source was requested, which ones answered, what each returned, how many rows were dropped and why, and the spend ceiling authorised for the child runs.


Output example

Three kinds of row, told apart by itemType.

A keyword row:

{
"schemaVersion": 2,
"itemType": "keyword_rank",
"keyword": "hair growth serum",
"yourRank": 14,
"whatHappened": "Slipped 11 to #14",
"nextStep": "Review your ad bid on this keyword",
"usualRank": 4,
"seenInPercentOfChecks": 90,
"checksCompared": 10,
"trendOverTime": "declining",
"lastRank": 3,
"lastRankKnown": true,
"placesChanged": -11,
"comparison": {
"status": "compared",
"reason": "ok",
"comparedWith": "2026-07-31T06:00:00.000Z",
"daysSinceLastCheck": 1,
"pageTurnoverPercent": 20
},
"whoTookYourSpot": {
"title": "Rosemary Oil Scalp Serum 100ml",
"shopName": "GlowLab Official",
"rank": 3,
"isPaidPlacement": null,
"price": 9.99
},
"shelfSpace": { "slotsOwned": 1, "slotsRead": 20, "percentOfSlotsRead": 5 },
"market": {
"medianPriceAbove": 12.5,
"yourPriceGapVsAbove": 7.49,
"medianSoldCountAbove": 3400,
"yourSoldCount": 210,
"paidPlacementsAbove": null,
"keywordInYourTitle": true
},
"alerts": ["Fell out of the top 5, from #3 to #14. The new #3 is Rosemary Oil Scalp Serum 100ml (GlowLab Official) at $9.99, and you are $7.49 above the median price of the listings above you."],
"checkedAt": "2026-08-01T06:00:00.000Z"
}

A listing row, one per ranked competitor:

{
"schemaVersion": 2,
"itemType": "listing",
"keyword": "hair growth serum",
"rank": 3,
"productId": "1729502836410000123",
"title": "Rosemary Oil Scalp Serum 100ml",
"shopName": "GlowLab Official",
"price": 9.99,
"soldCount": 4210,
"rating": 4.6,
"isPaidPlacement": null,
"isYours": false
}

And one run_summary row per run, carrying the headline, the action count, the verdict and recurringRivals.

Upgrading from v0.1? The output shape changed. Field names are now what a seller would call them (yourRank, usualRank, whoTookYourSpot), competitor listings are their own rows instead of a nested array, and internal QA fields (source, sourceChanged, baselineAgeDays, scanDepthShort, movementReliable) have moved to RUN_DIAGNOSTICS.json or been folded into whatHappened. Rows carry schemaVersion: 2. Your stored rank history carries over automatically.


Tips

  • Keep your Tracking ID stable. Changing it starts a brand new history with nothing to compare against.
  • Use one Tracking ID per shop or per client. Histories are namespaced by tracking ID and region.
  • Schedule for the same time each day. Given how much TikTok reshuffles, a consistent hour makes movement mean something.
  • Trust the trend line, not a single run. One day of movement on a volatile keyword is noise; a week of it is a signal.
  • Open the Needs Action view first. It is ordered by what costs you money today. placesChanged is the noisiest number in the output; usualRank and trendOverTime are the ones worth acting on.
  • Give it a week before judging a keyword. trendOverTime stays not_enough_checks until there are enough runs to say something honest, by design.
  • Read an absence against seenInPercentOfChecks, not on its own. A keyword the last ten checks saw eight times has crossed the edge of the scanned window before and will again; one that has been solidly present and is now gone is the thing to drop everything for. The alert tells you which case you are in.
  • A low seenInPercentOfChecks with a good rank is a warning in its own right. Appearing sometimes and vanishing other times costs more sales than a stable mid-table position, so it raises its own alert even on runs where nothing else moved.
  • Check market.yourPriceGapVsAbove before you touch the listing. Being outranked by cheaper listings and being outranked at the same price need opposite responses: one is a pricing decision, the other is a listing or ad-bid decision.
  • Set the Tracking ID before your first real run. Leaving it as default shares a history record with the platform's own daily health check.

Input parameters

Every field is optional. Running with no input at all produces a short demo sweep so you can see the output shape before committing to a schedule.

FieldTypeDefaultWhat it does
keywordsstring[]demo sweepThe search terms your buyers type. One rank check per keyword, so this is the main driver of cost and runtime. Start with 10 to 30 that matter.
trackShopNamestringnoneMatch rankings owned by this shop, case-insensitively. The easiest way to track yourself.
trackProductIdsstring[][]Exact TikTok Shop product IDs. More precise than a shop name when you only care about specific SKUs. A listing matching either one counts as yours.
regionstringUSWhich marketplace to search. United States only, because it is the only region the sources were measured serving correctly.
topNinteger20How many results to read per keyword, 10 to 100. Multiplies your source bill one-for-one.
trackingIdstringdefaultNames the history this run reads and writes. Runs sharing an ID form one time series. Change it away from default before your first real run.
compareWithPreviousRunbooleantrueCompute movement, new entrants and displacement against the last snapshot under this tracking ID.
includeCompetitorsbooleantruePublish the ranked listings around you as their own rows, and use them for price-gap analysis.
maxKeywordsinteger50Hard ceiling on keywords processed, applied after de-duplication. A safety net for scheduled runs.
upstreamProviderstringtrakkWhich Store Actor supplies the ranked results. Setting it explicitly pins it.
allowProviderFallbackbooleantrueTry the other source when the chosen one fails. This is what keeps a scheduled tracker alive.
proxyobjectApify residentialPassed through to the search source. Residential is required; datacenter IPs get a Security Check page.

History is keyed by trackingId + region + what you track. Changing trackShopName or trackProductIds deliberately starts a fresh history, because comparing one shop's ranks against another's would be meaningless.


FAQ

Does this scrape TikTok Shop directly? No, and that is deliberate. TikTok Shop's search API requires a signature minted by its own in-browser anti-crawler code, and a real browser on residential proxies still hits a CAPTCHA. This Actor buys ranked results from an established TikTok Shop search Actor and spends its effort on the part nobody else has built: rank history, movement, displacement and volatility filtering.

So I am billed twice? Yes, and the source is the larger half. This Actor charges $0.05 per run plus $0.012 per keyword tracked. The search source charges you separately, per product row returned. See the cost table above for worked examples.

Why does my first run show no movement? Movement needs two runs under the same tracking ID. The first establishes the baseline and reports movement as null rather than as zero, because "unchanged" and "nothing to compare against" are different facts.

Why is isPaidPlacement always null? Neither search data source publishes an ad marker for a search slot, so the Actor does not know. It reports null rather than guessing. It will not print false, because asserting every competitor is organic on no evidence is worse than admitting the gap.

Can I track more than one shop? Yes, with one tracking ID per shop or client. They stay completely separate: history is keyed by tracking ID, region and what you track.

How often should I run it? Daily, at the same hour. TikTok Shop reshuffles enough that a consistent cadence is what makes movement mean anything. Twice a day works and will not corrupt anything, but it mostly adds noise.

What happens if a data source goes down? Two sources are wired up with automatic failover, and per-keyword comparisons are never made across sources, so a fallback never manufactures fake movement. If both are down the run publishes a status row explaining which one failed and why, rather than reporting your rankings as lost.

Which regions are supported? United States only. The sources advertise others, but were measured returning the US catalogue under a foreign label. A rank measured against the wrong market is worse than no rank.

Does it work with more than 200 keywords? maxKeywords caps a single run at 200. Split a larger set across several tasks with different tracking IDs, or across several scheduled runs.


Run it from your own tools

Everything below returns the same dataset you see in Console.

API. Start a run and wait for the dataset:

curl -X POST "https://api.apify.com/v2/acts/marielise.dev~tiktok-shop-rank-tracker/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"keywords": ["hair growth serum", "led strip lights"],
"trackShopName": "Your Shop",
"trackingId": "my-shop",
"topN": 20
}'

Scheduling. Create a Task with your keyword list, then attach a daily Schedule to it. This is the intended way to run it: the tracking ID stays fixed, so every run compares against the last.

Webhooks. Attach a webhook on ACTOR.RUN.SUCCEEDED to push each run into Slack, a Google Sheet, or your own endpoint. The run summary row carries keywordsNeedingAction and alerts, which is usually all a notification needs.

Integration platforms. The Actor works with Apify's Make, Zapier and n8n integrations, and with the Apify MCP server if you want an AI agent to query your rankings directly.

Client libraries. apify-client for JavaScript and Python both work as normal:

const { ApifyClient } = require('apify-client');
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('marielise.dev/tiktok-shop-rank-tracker').call({
keywords: ['hair growth serum'],
trackShopName: 'Your Shop',
trackingId: 'my-shop',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();