TikTok Ad Creative Benchmark — CTR percentiles by industry avatar

TikTok Ad Creative Benchmark — CTR percentiles by industry

Pricing

Pay per event

Go to Apify Store
TikTok Ad Creative Benchmark — CTR percentiles by industry

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

Radu Furtuna

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

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:

FieldMeaning
countryCode, period, orderBywhich slice of the board this benchmark is built from
industryKey, objectiveKeywhat the group is (* for the dimension you did not group by)
industryName, objectiveNamethe same, as human-readable names
adCounthow many distinct ads back it
ctrAdCount, likesAdCount, costAdCount, durationAdCountthe denominator of each metric — an ad missing a field is counted in adCount but not in that metric
isTruncatedSampletrue if the slice hit the page cap while the source still reported more results (only possible when you opt in)
isExploratorytrue when the group holds fewer than 10 ads: readable, but too thin to quote as a market benchmark
industryLevel, taxonomySnapshotDatewhich granularity and which taxonomy snapshot produced the row
ctrP25, ctrMedian, ctrP75, ctrP90, ctrMeanthe CTR distribution — where the middle of the market is, and where the top decile starts
likesMedian, likesMeanengagement
costMedianthe board's cost indicator
durationMedianSec, durationP90Sechow long the winning videos actually are
titleLengthMedianhow long the ad copy is
hashtagsPerAdMean, shareWithHashtagshow much these advertisers lean on hashtags
topHashtagsthe 5 most common hashtags in that group
runId, scrapedAtrun 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

  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) 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, swept to its full depth — up to 100 ads (5 pages × 20), the source's own ceiling.
  3. 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.
  4. Ads are grouped, groups under minAdsPerGroup are 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
}
  • groupByindustry, objective, or industryAndObjective. Country and period always split a group; this picks what else does. industryAndObjective is 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. top rolls 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): detailed produced 79 groups of which 7 cleared the minimum, top produced 32 groups of which 11 cleared it — with the largest holding 21 ads instead of 7. Use detailed only 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 flagged isExploratory: true. Below 5 is refused outright. Groups under the threshold are dropped before they are returned and before they are billed.
  • orderByctr and impression are deterministic and repeatable; for_you is personalised and its window rotates between runs. Benchmarks you intend to compare over time must use ctr or impression.

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 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: true to include it anyway; every row built from it is then flagged isTruncatedSample: 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 null name rather than guessed at.
  • period is 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)