TikTok Ad Creative Benchmark — CTR percentiles by industry
Pricing
Pay per event
TikTok Ad Creative Benchmark — CTR percentiles by industry
Turns the public TikTok Top Ads board into performance benchmarks: CTR distribution, video length, engagement and copy patterns per industry, objective and country. One row per benchmark, not another dump of ads.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Radu Furtuna
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
TikTok Ad Creative Benchmark — is your creative above or below market?
Every other TikTok ads Actor hands you a pile of ads. That answers "what is running", not the question an advertiser actually has: "my creative gets 1.2% CTR in Beauty in the US — is that good?"
This Actor turns the public TikTok Creative Center Top Ads board into benchmarks. One row is one benchmark — a country, a time window and an industry (and/or campaign objective) — with the CTR distribution, video length, engagement and copy patterns of the ads inside it.
What you get
One row per benchmark group:
| Field | Meaning |
|---|---|
countryCode, period, orderBy | which slice of the board this benchmark is built from |
industryKey, objectiveKey | what the group is (* for the dimension you did not group by) |
industryName, objectiveName | the same, as human-readable names |
adCount | how many distinct ads back it |
ctrAdCount, likesAdCount, costAdCount, durationAdCount | the denominator of each metric — an ad missing a field is counted in adCount but not in that metric |
isTruncatedSample | true if the slice hit the page cap while the source still reported more results (only possible when you opt in) |
isExploratory | true when the group holds fewer than 10 ads: readable, but too thin to quote as a market benchmark |
industryLevel, taxonomySnapshotDate | which granularity and which taxonomy snapshot produced the row |
ctrP25, ctrMedian, ctrP75, ctrP90, ctrMean | the CTR distribution — where the middle of the market is, and where the top decile starts |
likesMedian, likesMean | engagement |
costMedian | the board's cost indicator |
durationMedianSec, durationP90Sec | how long the winning videos actually are |
titleLengthMedian | how long the ad copy is |
hashtagsPerAdMean, shareWithHashtags | how much these advertisers lean on hashtags |
topHashtags | the 5 most common hashtags in that group |
runId, scrapedAt | run identity and UTC timestamp |
Percentiles, not averages. A single outlier drags a mean; the median and P75/P90 tell you where the market sits and what "good" costs. That is the whole point of the product.
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) 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, swept to its full depth — up to 100 ads (5 pages × 20), the source's own ceiling. - Ads are de-duplicated by ad id before they enter a group, so an ad on both the US and GB boards is counted once.
- Ads are grouped, groups under
minAdsPerGroupare dropped, and the rest are returned ranked by size.
Input
{"countryCodes": ["US", "GB", "DE"],"periods": [7, 30],"orderBy": "ctr","groupBy": "industry","industryLevel": "top","minAdsPerGroup": 10,"maxBenchmarks": 300}
groupBy—industry,objective, orindustryAndObjective. Country and period always split a group; this picks what else does.industryAndObjectiveis the sharpest cut but needs more ads per group to clear the minimum.industryLevel— TikTok's taxonomy has 258 detailed industries, which cuts a run into groups too small to mean anything.toprolls them up into the 21 parent categories and is the default. Measured 10.09.2026 on a live sweep of US/30d + GB/30d (140 distinct ads):detailedproduced 79 groups of which 7 cleared the minimum,topproduced 32 groups of which 11 cleared it — with the largest holding 21 ads instead of 7. Usedetailedonly when you sweep many countries and periods.minAdsPerGroup— default 10. At n=5 the 90th percentile is nearly the maximum and moves with every single ad; 10 is the smallest group we are willing to call a benchmark. Values of 5-9 are allowed but every row from such a group is flaggedisExploratory: true. Below 5 is refused outright. Groups under the threshold are dropped before they are returned and before they are billed.orderBy—ctrandimpressionare deterministic and repeatable;for_youis personalised and its window rotates between runs. Benchmarks you intend to compare over time must usectrorimpression.
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 returned parsable data and was used for benchmarks. Each slice is a separate browser session against the board, and that is where the real cost sits. A slice that failed, or that was excluded for incomplete or truncated pagination, is not charged: excluding it is our quality policy, not something you should pay for.benchmark-returned— charged per row, after the row is written to the dataset. A run where no group clears the threshold returns nothing and is charged nothing beyond the run event.
What happens if a run is interrupted. Apify can migrate a run mid-flight. This Actor keeps a durable
four-state ledger in its key-value store: intent → written → charge started → billed. On restart a
billed row is skipped; a row whose charge was started but never confirmed is not charged again and is
counted in chargeUncertainRows instead; a written-but-uncharged row is charged without being written
again; a row with only an intent is written again, because 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 — it makes the
residual uncertainty visible and small instead.
Completeness: which slices are allowed to become a benchmark
A benchmark is scoped inside one country × period slice, so a neighbouring slice failing does not
corrupt it — the good slices are still returned and billed normally.
Two kinds of slice are kept out of the benchmarks:
- a slice whose pagination broke partway — excluded always;
- a slice that hit the page cap while the source still reported more results — excluded by default.
Set
allowTruncatedSlices: trueto include it anyway; every row built from it is then flaggedisTruncatedSample: true.
The reasoning is the same in both cases. Percentiles
computed over half a window look exactly as precise as percentiles over a full one, and you would have
no way to tell them apart. Such a slice is reported in the coverage record as excluded with the reason
pagination_incomplete, and its ads enter no group at all.
A run that fails outright delivers nothing and bills nothing beyond the run event.
Honest limits
- This benchmarks the top of the board, not all TikTok advertising. The source ranks and caps at 100 ads per slice. The numbers describe high-performing ads in that slice — which is exactly what you want to compare against, but it is not a census of every ad running.
- The board's industry/objective URL filters are ignored by the source. We verified this by measurement, so this Actor does not offer filters it cannot honour — it groups after collection instead, which gives the same answer honestly.
- Ads with no industry or objective are grouped under
unknown, not silently discarded, so the numbers add up. - Industry and objective names come from a taxonomy snapshot taken from the board's own filter
endpoint on 10.09.2026. An unknown code is returned as-is with a
nullname rather than guessed at. periodis the board's window, not an ad's lifetime. TikTok does not publish an ad's real first-appearance date and this Actor does not invent one.
How much you get from one run
From the live sweep above (2 slices, 140 distinct ads), the defaults returned 11 benchmarks, the
largest backed by 21 ads. Grouping by objective instead returned 9, the largest backed by 31 ads.
Benchmarks scale with slices swept: more countries and both time windows give both more groups and
more ads behind each one.
Coverage record
Every run writes a coverage record to the key-value store: slices requested vs attempted, per-slice
status and reason, adsSeen, groupsFound, groupsQualifying, groupsSkippedByThreshold,
groupsSkippedByCap, benchmarksDelivered, benchmarksBilled, runStartCharged, completeSlices,
excludedSlices, slicesBilled, deliveryBlocked, ledgerResumed, chargeUncertainRows, pushedUnbilledRows,
allowTruncatedSlices, groupBy, industryLevel
and minAdsPerGroup. Those numbers are enough to reconcile every charge against every row.
Author: OmniCoder (https://t.me/OmniCoder)