# TikTok Shop Listing Health Check (`thenetaji/tiktok-shop-listing-health`) Actor

Audit a TikTok Shop product and get one 0-100 health score back, broken into review, content, keyword, competitive, creator, category and shop dimensions — each with a ranked, evidence-backed list of specific fixes. Deterministic scoring, not an AI summary; toggle off any dimension to control cost.

- **URL**: https://apify.com/thenetaji/tiktok-shop-listing-health.md
- **Developed by:** [The Netaji](https://apify.com/thenetaji) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.19 / 1,000 results

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

Learn more: https://docs.apify.com/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 Listing Health

The Actor diagnoses one or many TikTok Shop products and returns one 0-100 health score per product, built from up to seven independently-scored dimensions — review health, content completeness, keyword coverage, competitive position, creator presence, category benchmark, and shop signal — each with a ranked, evidence-backed list of specific fixes. It does not return product rows; for that, see the [TikTok Shop Product Scraper](https://apify.com/thenetaji/tiktok-shop-product-scraper).

```json
{
  "product_ids": [
    "https://shop.tiktok.com/us/pdp/1730927783781307026",
    "1729447567685489298"
  ],
  "region": "US",
  "keyword": "wireless earbuds"
}
```

### What the score is built from

`health_score` is a weighted blend of seven dimensions, each scored 0-100 on its own evidence:

| Dimension | Weight | Built from |
| --- | --- | --- |
| `review_health` | 25 | Rating average, review volume, and the dominant complaint theme, reusing the same deterministic analysis as [TikTok Shop Review Insights](https://apify.com/thenetaji/tiktok-shop-review-insights). |
| `content_completeness` | 20 | Image count, title length, whether a description is set, and whether any purchasable variant exists, from the product page. |
| `keyword_coverage` | 15 | Whether `keyword` appears in the title, and how many of TikTok's own related search terms for that keyword also appear in it. |
| `competitive_position` | 15 | Whether the product appears at all in the top results for `keyword`, and its rating percentile against the products that do. |
| `creator_presence` | 10 | How many distinct creators have posted a showcase or affiliate video for the product, their combined play count, and whether any carry a paid-partnership label. |
| `category_benchmark` | 10 | The product's rating percentile against the best-reviewed items TikTok Shop surfaces on the product's own page. |
| `shop_signal` | 5 | The seller's shop-level star rating. |

These weights and the thresholds each dimension checks against (a 4.3★ rating benchmark, 7+ images, a 40-250 character title, 20 creators and 30M combined plays for full creator credit, and so on) are a scoring model this Actor defines to make the report usable, not a number TikTok publishes anywhere. Treat `health_score` as a heuristic diagnostic, not an official TikTok Shop metric — two reasonable scoring models would rank the same two listings differently at the margin, even if they agree on the direction of a genuine problem like a missing description or zero creator videos.

**Where the thresholds come from.** They are calibrated against live listings rather than guessed. A sample of eight US listings across hair oil, wireless earbuds, lip gloss and phone chargers measured: images min 3 / median 7 / max 9; title length min 22 / median 196 / max 230; rating median 4.32; creators per listing median 19; combined creator plays median 32.3M. Benchmarks sit at or near those medians, because a benchmark below the 25th percentile never fires and one above the maximum penalises everybody. Two consequences worth knowing: long titles are normal on TikTok Shop and are not penalised until 250 characters, and a listing needs real creator traction — not merely a few videos — to score well on creator presence. The sample is small, so expect these to move as they are re-measured against more listings and more categories.

### Partial data does not fail the run

A single product's diagnosis needs up to eight separate upstream lookups. Any one of them can fail, return nothing, or not apply — a product with no `keyword` set has nothing to check keyword coverage or competitive position against, for instance. When a dimension cannot be scored it is left out of `health_score` entirely rather than counted as a 0: `health_score` is always a blend of the dimensions that genuinely produced a score, reweighted so they still sum to 100%. Every excluded dimension is listed in `dimensions_unavailable` with the reason — `fetch_failed`, `disabled` (the input option was turned off), `no_keyword_input`, `no_detail`, `no_seller_id`, `no_search_results`, `no_benchmark_data`, or `no_own_rating`.

A product is only skipped, and only then left uncharged, when **every** dimension is unavailable — nothing was fetched successfully enough to say anything about the listing. A product whose page fetch fails but whose reviews and creator videos still come back is still diagnosed and still billed, just on fewer dimensions; `dimensions_unavailable` says exactly which ones and why, so the reduced basis is never hidden.

### Cost control

Each product's full diagnosis costs up to eight upstream requests: one each for the product page and its reviews (always run — these carry `content_completeness` and `review_health`, the two largest weights), one each for creator videos and the category benchmark (per product), and one each for the keyword-coverage terms, the competitive-position search, and the shop lookup — the last three are cached and fetched only once per distinct keyword or shop across the whole run, not once per product, so a batch of 50 products that share one keyword and one shop still costs one keyword-terms request, one competitive-search request, and one shop-info request in total, not 150.

Five input options turn a dimension off to cut cost further, each shown with what it removes:

- `includeKeywordCoverage` (default on) — drops `keyword_coverage`; removes the related-search-terms request.
- `includeCompetitive` (default on) — drops `competitive_position`; removes the keyword search request.
- `includeCreators` (default on) — drops `creator_presence`; removes one request per product.
- `includeCategoryBenchmark` (default on) — drops `category_benchmark`; removes one request per product.
- `includeShopSignal` (default on) — drops `shop_signal`; removes one request per shop.

`review_health` and `content_completeness` cannot be turned off — they are what the product/reviews and product/detail lookups already fetch to build every other dimension, and together they carry 45 of the 100 weighted points.

`keyword_coverage` and `competitive_position` both need `keyword`; without it they report `no_keyword_input` regardless of their toggle. `keyword` applies to the whole run, not per product — a batch of unrelated products sharing one `keyword` will get a keyword-coverage and competitive-position check against a term that may only be relevant to some of them. Run products with different primary keywords in separate calls when that distinction matters.

### Result fields

```json
{
  "product_id": "1730927783781307026",
  "name": "Lifter Glaze Oil Balm with Hyaluronic Acid",
  "keyword": "lip oil",
  "health_score": 54,
  "dimensions": {
    "review_health": {
      "available": true,
      "score": 86,
      "weight": 25,
      "weight_applied": 27.78,
      "evidence": { "rating_average": 4.2, "reviews_analysed": 63, "top_pain_point": { "theme": "Sizing & fit", "share_of_reviews": 0.11 } }
    },
    "content_completeness": {
      "available": true,
      "score": 52,
      "weight": 20,
      "weight_applied": 22.22,
      "evidence": { "image_count": 1, "title_length": 42, "sku_count": 1, "has_description": false }
    },
    "keyword_coverage": {
      "available": true,
      "score": 0,
      "weight": 15,
      "weight_applied": 16.67,
      "evidence": { "keyword": "lip oil", "primary_keyword_present": false, "related_terms_checked": 3, "related_terms_matched": 0 }
    },
    "creator_presence": { "available": false, "weight": 10, "reason": "fetch_failed" }
  },
  "dimensions_unavailable": [{ "dimension": "creator_presence", "reason": "fetch_failed" }],
  "fix_items": [
    {
      "dimension": "category_benchmark",
      "message": "Rating trails the shop's own top-reviewed items: 0th percentile of 2 compared, which average 4.65★.",
      "observed": 0,
      "benchmark": 0.4,
      "impact": 11.11
    },
    {
      "dimension": "keyword_coverage",
      "message": "Primary keyword \"lip oil\" does not appear anywhere in the title.",
      "observed": false,
      "benchmark": true,
      "impact": 8.34
    },
    {
      "dimension": "content_completeness",
      "message": "Only 1 image on the listing, below the 5-image benchmark.",
      "observed": 1,
      "benchmark": 5,
      "impact": 6.22
    }
  ]
}
```

This is a real record produced by the scorer (from a synthetic fixture rather than a live product, shortened to fit — a full run carries all seven entries in `dimensions` and one `fix_items` entry per issue found, not three). `weight_applied` is the dimension's share of `health_score` after excluding whatever was unavailable for that product; `creator_presence`'s weight of 10 is missing here, so the other six were rescaled from summing to 90 up to 100 — `review_health`'s `weight_applied` of 27.78 is its `weight` of 25 divided by that 90, not 25 itself. `impact` on a fix item is `weight_applied` × how many of that dimension's own 100 points the issue cost, so it reads directly as "how many of the 100 `health_score` points fixing this is worth." The largest `impact` is not necessarily on the lowest-scoring dimension: `category_benchmark` scored 0 here but ranks first only because nothing else in that dimension needed fixing once it did; a dimension with several smaller issues can out-rank one with a single larger one, and a heavily-weighted dimension's issues generally rank above equivalent gaps in a lightly-weighted one.

### Charging

Each product that produces a `health_score` — meaning at least one dimension was scoreable — is billed the listing-health event once. A product where every dimension came back unavailable yields no row and is not billed; the run continues to the next product in `product_ids` rather than failing outright.

### Questions

**Why is `health_score` lower than the product's actual star rating would suggest?**
`review_health` is only 25 of the 100 weighted points. A product with a strong rating but three images, no description, and zero creator videos scores well on `review_health` and poorly on `content_completeness` and `creator_presence`, and the blended score reflects all three, not just the rating.

**Why does `competitive_position` say the product is missing from its own search results?**
That is a real finding, not an error: the top results for `keyword` at the time of the run did not include this product. Combined with a `fetch_failed` on nothing else, it means the product genuinely is not surfacing for the keyword it is being measured against — one of the more actionable signals this Actor produces, and the reason `appears_in_search` sits in `competitive_position`'s evidence rather than only in the fix list.

**Why is `creator_presence` scored at 0 instead of marked unavailable?**
Zero showcase or affiliate videos is a successful lookup that came back empty, not a failed one — a real, common state for a listing and worth reporting as a score rather than hiding as `unavailable`. `fetch_failed` is reserved for when the lookup itself could not be completed.

### Related Actors

For the underlying product page this Actor's `content_completeness` and `shop_signal` dimensions are built from, use the [TikTok Shop Product Scraper](https://apify.com/thenetaji/tiktok-shop-product-scraper). For the full ranked pain-point and praise-point analysis behind `review_health`, use [TikTok Shop Review Insights](https://apify.com/thenetaji/tiktok-shop-review-insights). For the raw creator and affiliate videos behind `creator_presence`, use the [TikTok Shop Creator Videos Scraper](https://apify.com/thenetaji/tiktok-shop-creator-videos-scraper).

# Actor input Schema

## `product_ids` (type: `array`):

One or more TikTok Shop products to fetch. Paste product page links, or product IDs — both work, and you can mix them.

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

Which TikTok Shop storefront to read. This changes the products, prices, and currency you get back — not just the display language.

## `keyword` (type: `string`):

Product keyword to search for on TikTok Shop.

## `reviewSampleSize` (type: `integer`):

How many recent reviews to pull per product before analysing them. A larger sample surfaces quieter themes but costs one larger request per product. 0 asks for the upstream ceiling of 500.

## `includeKeywordCoverage` (type: `boolean`):

Check the title against TikTok's own related search terms for the product's keyword and flag high-intent terms missing from it. Needs Search Keyword. Adds one request per distinct keyword in the run, shared across products that use it.

## `includeCompetitive` (type: `boolean`):

Compare the product's price and rating against the top search results for its keyword, and flag if it does not appear in them at all. Needs Search Keyword. Adds one request per distinct keyword in the run, shared across products that use it.

## `includeCreators` (type: `boolean`):

Check how many creators are promoting the product on TikTok Shop, their combined view count, and whether any carry a paid-partnership label. Adds one request per product.

## `includeCategoryBenchmark` (type: `boolean`):

Compare the product's rating against the best-reviewed items TikTok Shop surfaces on its own page. Adds one request per product.

## `includeShopSignal` (type: `boolean`):

Add the seller's shop-level rating to the report. Adds one request per distinct shop in the run, shared across products from the same shop.

## Actor input object example

```json
{
  "product_ids": [
    "1730927783781307026"
  ],
  "region": "US",
  "keyword": "wireless earbuds",
  "reviewSampleSize": 100,
  "includeKeywordCoverage": true,
  "includeCompetitive": true,
  "includeCreators": true,
  "includeCategoryBenchmark": true,
  "includeShopSignal": true
}
```

# Actor output Schema

## `dataset` (type: `string`):

All records scraped by this run

# 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 = {
    "product_ids": [
        "https://shop.tiktok.com/us/pdp/1730927783781307026"
    ],
    "keyword": "wireless earbuds",
    "reviewSampleSize": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("thenetaji/tiktok-shop-listing-health").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 = {
    "product_ids": ["https://shop.tiktok.com/us/pdp/1730927783781307026"],
    "keyword": "wireless earbuds",
    "reviewSampleSize": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("thenetaji/tiktok-shop-listing-health").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 '{
  "product_ids": [
    "https://shop.tiktok.com/us/pdp/1730927783781307026"
  ],
  "keyword": "wireless earbuds",
  "reviewSampleSize": 100
}' |
apify call thenetaji/tiktok-shop-listing-health --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thenetaji/tiktok-shop-listing-health"
        }
    }
}

```

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/PLRlJB0D0N8hACpn8/builds/ccJk2hOmSbdeNJasT/openapi.json
