TikTok Ad Hashtag Miner — hashtags that top ads actually use
Pricing
Pay per event
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
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:
| Field | Meaning |
|---|---|
hashtag | the hashtag as it is written in the ads (first spelling seen) |
hashtagKey | lower-cased key used for grouping |
adCount | how many distinct ads used it |
avgCtr, medianCtr, maxCtr | click-through rate across those ads |
ctrAdCount, likesAdCount, durationAdCount | how many ads actually carried each metric — the denominator, which can be lower than adCount |
avgLikes, totalLikes | engagement across those ads |
avgDurationSec | average video length of the ads using it |
topIndustries, topObjectives | which industries and campaign objectives it shows up in (raw codes) |
topIndustryNames, topObjectiveNames | the same, as human-readable names |
countries | which country boards it was found on |
sampleAds | up to 3 example ads: id, copy, CTR, likes, adUrl (its card in TikTok Creative Center) and videoId |
isPartial | true if the run's coverage was incomplete (see below) |
isTruncatedSample | true if any slice hit the page cap while the source still reported more results |
taxonomySnapshotDate | which taxonomy snapshot resolved the industry/objective names |
runId, scrapedAt | run identity and UTC timestamp |
How it works
- 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. - You pick countries and periods. Every
country × periodpair is one slice of the board. - Each slice is swept to its full depth — up to 100 ads (5 pages × 20), the source's own hard ceiling per slice.
- 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
1to a hashtag'sadCount, not2. (The overlap is real: measured US/30d ∩ GB/30d = 38 of ~100.) - 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}
orderBy—ctrandimpressionare deterministic and repeatable between runs;for_youis 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 of2drops 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 percountry × periodslice 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
chargeUncertainRowsfor 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.
About the ad links
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
adsSeenbut to no hashtag. Those 140 ads produced 288 distinct hashtags, of which 17 cleared the default threshold of 2 ads. The run'scoveragerecord reportsadsSeen,adsWithHashtagsandhashtagsFoundso 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_youranking is personalised. Two runs will not return the same window. Usectrorimpressionfor 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)