TikTok Ad Hashtag Miner — hashtags that top ads actually use avatar

TikTok Ad Hashtag Miner — hashtags that top ads actually use

Pricing

Pay per event

Go to Apify Store
TikTok Ad Hashtag Miner — hashtags that top ads actually use

TikTok Ad Hashtag Miner — hashtags that top ads actually use

TikTok's own Trending Hashtags page shows three rows and then asks you to log in. This Actor mines hashtags straight out of the ad copy on the public Top Ads board and ranks them by how many high-performing ads use them, with CTR and likes attached.

Pricing

Pay per event

Rating

0.0

(0)

Developer

Radu Furtuna

Radu Furtuna

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

TikTok Ad Hashtag Miner — the hashtags that top-performing ads actually use

TikTok's own Trending Hashtags page moved into TikTok One and now shows three rows before it asks you to log in. This Actor gets at hashtag data from the other side: it reads the public Top Ads board in TikTok Creative Center, pulls the hashtags out of the ad copy, and ranks them by how many high-performing ads use them — with CTR, likes, industry and country attached to every hashtag.

Be clear about what this is. These are hashtags used in paid ads that performed well, not organic TikTok trends. That is the point: an organic trend tells you what people are watching, this tells you what advertisers are actually spending money on and how those creatives performed. Both are useful; they are not the same thing, and this Actor does not pretend otherwise.

What you get

One row per hashtag:

FieldMeaning
hashtagthe hashtag as it is written in the ads (first spelling seen)
hashtagKeylower-cased key used for grouping
adCounthow many distinct ads used it
avgCtr, medianCtr, maxCtrclick-through rate across those ads
ctrAdCount, likesAdCount, durationAdCounthow many ads actually carried each metric — the denominator, which can be lower than adCount
avgLikes, totalLikesengagement across those ads
avgDurationSecaverage video length of the ads using it
topIndustries, topObjectiveswhich industries and campaign objectives it shows up in (raw codes)
topIndustryNames, topObjectiveNamesthe same, as human-readable names
countrieswhich country boards it was found on
sampleAdsup to 3 example ads: id, copy, CTR, likes, adUrl (its card in TikTok Creative Center) and videoId
isPartialtrue if the run's coverage was incomplete (see below)
isTruncatedSampletrue if any slice hit the page cap while the source still reported more results
taxonomySnapshotDatewhich taxonomy snapshot resolved the industry/objective names
runId, scrapedAtrun identity and UTC timestamp

How it works

  1. No third-party account needed. Creative Center renders client-side and refuses requests made outside a real browser session, so a real anti-detect browser is required — this Actor uses a built-in stealth browser by default. You can point it at your own remote browser (cdpUrl, e.g. a Bright Data Scraping Browser) instead; the backend is never switched automatically and is reported in the run's coverage. Either way this Actor only reads what the public board loads for an ordinary visitor.
  2. You pick countries and periods. Every country × period pair is one slice of the board.
  3. Each slice is swept to its full depth — up to 100 ads (5 pages × 20), the source's own hard ceiling per slice.
  4. Ads are de-duplicated by ad id across slices before they are counted, so an ad that appears on both the US and GB boards adds 1 to a hashtag's adCount, not 2. (The overlap is real: measured US/30d ∩ GB/30d = 38 of ~100.)
  5. Hashtags are aggregated across the whole run and returned ranked by adCount.

Input

{
"countryCodes": ["US", "GB", "DE"],
"periods": [7, 30],
"orderBy": "ctr",
"maxPagesPerSlice": 5,
"minAdsPerHashtag": 2,
"maxHashtags": 500
}
  • orderByctr and impression are deterministic and repeatable between runs; for_you is personalised and its window rotates, so use it only if you want a moving sample.
  • minAdsPerHashtag — a hashtag seen in a single ad is noise. The default of 2 drops it. Hashtags below the threshold are removed before they are returned and before they are billed.
  • maxHashtags — hard stop, applied after ranking.

Pricing

Pay per event:

  • run-started — charged once per run, and only after the board has actually answered with parsable data. A blocked or empty run is not billed at all.
  • slice-swept — charged per country × period slice that actually returned parsable data. Each slice is a separate browser session against the board, and that is where the real cost of this Actor sits. A slice that failed, was blocked or returned nothing is not charged.
  • hashtag-returned — charged per row, after the row is written to the dataset.

You are never charged for a hashtag you did not receive, and never twice for the same hashtag in one run.

What happens if a run is interrupted. Apify can migrate a run to another machine mid-flight. This Actor keeps a durable four-state ledger in its key-value store:

intent → write the row → **written** → **charge started** → charge → **billed**

On restart:

  • a billed row is skipped entirely;
  • a row with charge started but no confirmation is not charged again — we cannot know whether the payment went through, and undercharging ourselves beats charging you twice. It is counted in chargeUncertainRows for reconciliation;
  • a written row that was never charged is charged, without being written again;
  • a row with only an intent is written again. Its previous write may have failed outright, and a duplicate row costs you nothing while billing for a row that never reached the dataset costs you money.

Apify's pay-per-event call takes no idempotency key, so exactly-once is not achievable and this Actor does not claim it. What it does instead is make the residual uncertainty visible and small: the only unresolved case is a crash inside the charge call itself, and every such row is counted, never silently re-charged.

Completeness: when this Actor returns nothing on purpose

The ranking is computed across every slice. If one slice fails, or its pagination breaks partway, the ranking is no longer a ranking of the whole window you asked for — some ads never entered the count.

So by default an incomplete run returns nothing and charges nothing per row. It is not a silent half-answer sold as a whole one. The coverage record tells you exactly which slice broke and why.

Set allowPartial: true if you want the incomplete ranking anyway. Then every row carries isPartial: true and no per-row charge is made at all — you pay only the run event.

A run that fails outright (run_wall_timeout, repeated slice errors) delivers nothing and bills nothing beyond the run event.

sampleAds[].adUrl points at the ad's own card in TikTok Creative Center, which is where this data comes from and which we verified opens and shows the same ad. We deliberately do not hand you a tiktok.com/@/video/... link: the board exposes an internal video id, not the public post id, and such a link does not open. videoId is given raw for your own matching, without pretending it is a URL.

Honest limits

  • Coverage of hashtags depends on ad copy. Measured 10.09.2026 on a live sweep of US/30d and GB/30d (140 distinct ads): 63 of 140 ads — 45% — carry hashtags in their copy; the rest contribute to adsSeen but to no hashtag. Those 140 ads produced 288 distinct hashtags, of which 17 cleared the default threshold of 2 ads. The run's coverage record reports adsSeen, adsWithHashtags and hashtagsFound so you can see exactly this ratio for your own run.
  • Volume comes from slices. Two slices give a couple of dozen ranked hashtags. If you want a broad table, sweep several countries and both the 7- and 30-day windows.
  • Industry and objective names come from a taxonomy snapshot taken from the board's own filter endpoint on 10.09.2026 (258 industries, 7 objectives). If TikTok adds a code we do not know, the raw code is still returned and the name field is null — the drift is visible in the data, never papered over.
  • 100 ads per slice is the source's ceiling, not a setting. Volume comes from sweeping more countries and periods.
  • The board's industry/objective URL filters are ignored by the source. We verified this (requesting a specific industry returned the same distribution as no filter), so this Actor does not offer filters it cannot honour. Industry and objective are reported per hashtag instead.
  • for_you ranking is personalised. Two runs will not return the same window. Use ctr or impression for anything you intend to compare over time.

Coverage record

Every run writes a coverage record to the key-value store: slices requested vs attempted, per-slice status and reason, adsSeen, adsWithHashtags, hashtagsFound, hashtagsQualifying, hashtagsSkippedByThreshold, hashtagsSkippedByCap, hashtagsDelivered, hashtagsBilled, runStartCharged, completeSlices, incompleteSlices, slicesBilled, coverageComplete, deliveryBlocked, truncatedSample, ledgerResumed, chargeUncertainRows, intentionallyUnbilledRows, pushedUnbilledRows. Those numbers are enough to reconcile every charge against every row. If part of the run failed, the record says which part and why — the dataset is never silently short.

Author: OmniCoder (https://t.me/OmniCoder)