# TikTok Shop Keyword Rank Tracker (`marielise.dev/tiktok-shop-rank-tracker`) Actor

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.

- **URL**: https://apify.com/marielise.dev/tiktok-shop-rank-tracker.md
- **Developed by:** [Marielise](https://apify.com/marielise.dev) (community)
- **Categories:** E-commerce, SEO tools, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## TikTok Shop Keyword Rank Tracker

**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:

| Field | What it tells you |
| --- | --- |
| **Your rank** | Your exact position in TikTok Shop search results |
| **What happened** | One sentence: *Climbed 4 to #6*, *Vanished (was #3)*, *Too noisy to compare* |
| **Do this** | The next action, in the imperative: *Check the listing is live and in stock* |
| **Usual rank** | Your 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 spot** | The exact listing that took your slot, with its price, rating and sold count |
| **New competitors above** | Listings that appeared ahead of you since last check, with their price |
| **Your price gap** | How far above or below the median price of the listings beating you |
| **Keyword in your title** | Whether your title even contains the phrase you are ranking for |
| **Shelf space** | How many of the slots read belong to you |
| **Competitor listings** | Every 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.

| Event | Price | Charged when |
| --- | --- | --- |
| Rank check | $0.05 per run | Once, on every run that reaches the search source |
| Keyword tracked | $0.012 per keyword | That 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 shape | Rows | Source cost | This Actor | **All-in (Free/Bronze)** |
| --- | --- | --- | --- | --- |
| 10 keywords × depth 20, weekly | 200 | ~$0.40 | $0.17 | **~$0.57/run, ~$2.50/mo** |
| 30 keywords × depth 20, daily | 600 | ~$1.20 | $0.41 | **~$1.61/run, ~$48/mo** |
| 30 keywords × depth 50, daily | 1,500 | ~$3.00 | $0.41 | **~$3.41/run, ~$102/mo** |
| 200 keywords × depth 100 | 20,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:

```json
{
  "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:

```json
{
  "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.

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `keywords` | string\[] | demo sweep | The 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. |
| `trackShopName` | string | none | Match rankings owned by this shop, case-insensitively. The easiest way to track yourself. |
| `trackProductIds` | string\[] | `[]` | 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. |
| `region` | string | `US` | Which marketplace to search. United States only, because it is the only region the sources were measured serving correctly. |
| `topN` | integer | `20` | How many results to read per keyword, 10 to 100. Multiplies your source bill one-for-one. |
| `trackingId` | string | `default` | Names 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. |
| `compareWithPreviousRun` | boolean | `true` | Compute movement, new entrants and displacement against the last snapshot under this tracking ID. |
| `includeCompetitors` | boolean | `true` | Publish the ranked listings around you as their own rows, and use them for price-gap analysis. |
| `maxKeywords` | integer | `50` | Hard ceiling on keywords processed, applied after de-duplication. A safety net for scheduled runs. |
| `upstreamProvider` | string | `trakk` | Which Store Actor supplies the ranked results. Setting it explicitly pins it. |
| `allowProviderFallback` | boolean | `true` | Try the other source when the chosen one fails. This is what keeps a scheduled tracker alive. |
| `proxy` | object | Apify residential | Passed 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:

```bash
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:

```javascript
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();
```

# Actor input Schema

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

The search terms your buyers actually type. One rank check is run per keyword, so this is the main driver of run cost and runtime. Start with 10-30 keywords that matter, not every keyword you can think of.

## `trackShopName` (type: `string`):

Match rankings owned by this shop, case-insensitively. This is the easiest way to track yourself: every listing whose seller matches counts toward your rank and your share of search. Leave empty if you would rather match on exact product IDs.

## `trackProductIds` (type: `array`):

Exact TikTok Shop product IDs to track (the long numeric ID in a product URL). More precise than shop name when you only care about specific SKUs. Can be combined with the shop name - a listing matching either one counts as yours.

## `region` (type: `string`):

Which TikTok Shop marketplace to search. United States only for now: the search data sources advertise other regions but 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.

## `topN` (type: `integer`):

How many search results to read per keyword. 20 covers the first screen most buyers ever see. Raise it only if you rank deep and need to watch yourself climb - cost scales directly with this number multiplied by your keyword count, and the maximum settings (100 results x 200 keywords) ask the source for 20,000 rows in a single run.

## `trackingId` (type: `string`):

Names the history this run reads from and writes to. Runs sharing a tracking ID form one continuous time series, which is what makes rank movement work. Use one ID per shop or per client, and keep it stable - changing it starts a fresh history with no comparison. Change it away from 'default' before your first real run: 'default' is shared with the platform's own daily health check. Note that history is keyed by this ID, the region AND what you track - changing your shop name or product IDs deliberately starts a fresh history, because comparing one shop's ranks against another's would be meaningless.

## `compareWithPreviousRun` (type: `boolean`):

Read the last snapshot stored under this tracking ID and compute rank movement, new entrants and who displaced you. The first run under a new tracking ID has nothing to compare against and reports movement as null rather than zero.

## `includeCompetitors` (type: `boolean`):

Return the ranked listings around you - title, price, sold count, rating and shop - as their own rows, and use them to work out your price gap against the listings beating you. This is what turns a rank number into something you can act on. Turn off for a smaller dataset; you keep your rank and movement, but lose the price and competitor analysis. Note that `isPaidPlacement` is reported as null: neither search data source publishes an ad marker, and this Actor will not guess one.

## `alertWebhookUrl` (type: `string`):

Post this run's urgent findings to a URL when something needs your attention, so you do not have to open the dataset to find out. Works with a Slack or Discord incoming webhook, or with Make, Zapier, n8n or your own endpoint: the body is JSON and includes a ready-made `text` field that Slack and Discord render directly. Nothing is sent when nothing meets the threshold, because a daily 'all clear' is the fastest way to get a channel muted. Leave empty to disable.

## `alertLevel` (type: `string`):

How urgent a finding has to be before it is sent. 'Critical only' covers drop-outs and losing a visibility band, which are the ones that cost money today. 'Critical and warning' adds big declines and new competitors above you, and will fire most days on a volatile keyword set.

## `maxKeywords` (type: `integer`):

Hard ceiling on keywords processed, applied after de-duplication. A safety net against a runaway keyword list on a scheduled run.

## `upstreamProvider` (type: `string`):

Which Store Actor supplies the ranked search results. TikTok Shop's own search cannot be read directly - it is CAPTCHA-gated and its API requires a browser-minted signature - so this Actor buys ranked results from a source that has solved that, and spends its effort on the rank history instead. Both sources publish TikTok's own result position. Setting this explicitly PINS the source: it is asked first for every keyword, overriding the per-keyword stickiness that otherwise keeps a keyword on whichever source measured it last. Leave on the default unless it starts failing.

## `allowProviderFallback` (type: `boolean`):

If the chosen source fails, returns nothing, or stops publishing rank positions, automatically try the other one before giving up. Recommended: it is what keeps a scheduled tracker alive when one source has a bad day.

## `proxy` (type: `object`):

Passed through to the search data source. This Actor makes no outbound requests of its own, so proxy spend here belongs to the source Actor, not to this one. TikTok refuses datacenter IP addresses outright - a US datacenter IP asking for the US storefront still gets served a Security Check page - so residential proxies are required. Changing this away from RESIDENTIAL will almost certainly return an empty run.

## Actor input object example

```json
{
  "keywords": [
    "wireless earbuds",
    "hair growth serum",
    "led strip lights"
  ],
  "trackProductIds": [],
  "region": "US",
  "topN": 20,
  "trackingId": "default",
  "compareWithPreviousRun": true,
  "includeCompetitors": true,
  "alertLevel": "critical",
  "maxKeywords": 50,
  "upstreamProvider": "trakk",
  "allowProviderFallback": true,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `needsAction` (type: `string`):

Start here: what happened on each keyword and what to do about it

## `rankPositions` (type: `string`):

Where you rank for every tracked keyword, with the usual position to steer by

## `biggestMovers` (type: `string`):

Keywords whose position changed, who took the slot, at what price, and how much of the page turned over

## `competitors` (type: `string`):

Every ranked listing that was read, one row each, with price and sold count

## `market` (type: `string`):

How your price and sold count compare with the listings beating you

## `runSummary` (type: `string`):

Headline numbers: keywords needing action, positions held, gained and lost

## `keyValueStore` (type: `string`):

Run outputs including RANK\_REPORT.md, rank\_positions.csv and RUN\_DIAGNOSTICS.json

# 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": [
        "wireless earbuds",
        "hair growth serum",
        "led strip lights"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("marielise.dev/tiktok-shop-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": [
        "wireless earbuds",
        "hair growth serum",
        "led strip lights",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("marielise.dev/tiktok-shop-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": [
    "wireless earbuds",
    "hair growth serum",
    "led strip lights"
  ]
}' |
apify call marielise.dev/tiktok-shop-rank-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,marielise.dev/tiktok-shop-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/rFm8crXDlqokDxi9P/builds/HoxpEqVtq7hnExSur/openapi.json
