Most TikTok hashtag strategies still rank tags by one number: total view count. Pick the biggest hashtag in your niche, slap it on the post, hope the algorithm notices. It almost never works that way, and the data explains why.
A hashtag with 12 billion views is, by definition, saturated. Your post lands in a feed where the median video already has six figures of engagement and a creator with a million followers. Your relative reach inside that pool is rounding error. Meanwhile a hashtag with 80 million views and a steep weekly growth curve will surface your post to a feed where the algorithm is actively probing for new entrants.
What actually correlates with reach is the shape of the hashtag, not its size: how fast it is growing, how much engagement the average post inside it gets, and whether the creators your target audience already follows are using it. None of that is visible in the TikTok app. All of it is computable from the TikLiveAPI hashtag endpoints in a small Python script you can run weekly.
This post walks through the data model, the three metrics that beat raw volume, and an end-to-end scoring pipeline you can drop into a content calendar workflow.
Internally, TikTok models every hashtag as a "challenge" object. That is the terminology you will see across the API. The relevant endpoints are documented in the documentation index, but the three you need for this workflow are:
The challenge info response is a flat snake_case object. The hashtag name field is cha_name (not name), which is a common gotcha for first-time integrators:
{
"id": "12345678",
"cha_name": "smallbusinesscheck",
"desc": "Show your small business",
"user_count": 482301,
"view_count": 1820000000,
"is_pgcshow": false,
"is_commerce": false,
"is_challenge": true,
"is_strong_music": false,
"type": 1,
"cover": "https://..."
}
Three fields matter strategically:
user_count - distinct creators who ever used the tagview_count - cumulative views across all postsis_commerce - whether TikTok flags it as a commercial/branded challenge (advertiser dominated)is_challenge - whether it is a real challenge format vs a generic descriptorThe volume numbers (user_count, view_count) are cumulative since the hashtag was born. They tell you nothing about momentum. That is where the next three metrics come in.
Velocity is the rate at which new posts are being attached to the hashtag. A hashtag whose user_count doubled in the last 14 days is in a discovery surge. A hashtag whose count has been flat for six months is dead even if the lifetime view number looks impressive.
Since the API does not expose historical user_count directly, you compute velocity by sampling recent posts via /challenge-posts/ and counting how many fall in the last N days using the create_time field (unix seconds, snake_case).
Engagement density tells you whether the hashtag is a quality pool or a dumping ground. Compute it from a sample of recent posts:
density = sum(digg_count for v in sample) / len(sample)
A 50M-view hashtag where the median recent post has 8,000 likes is far more valuable than a 2B-view hashtag where the median recent post has 200 likes. The first is a real community; the second is a graveyard of low-effort uploads.
The strongest predictor of whether a hashtag will work for you is whether creators in your target audience already use it successfully. If you sell B2B SaaS to designers and the top recent posters under a tag are all gaming streamers, the tag is irrelevant regardless of its volume.
Overlap is computed by taking the author.unique_id of every video in your /challenge-posts/ sample and intersecting it with a curated list of target creators or a set you previously scraped via /search-user/.
Start with a candidate list of hashtags. For each one, hit /challenge-info-name/ to resolve the challenge object. Auth is a single header: X-Api-Key: YOUR_API_KEY.
import os, requests
API = "https://api.tikliveapi.com"
HEADERS = {"X-Api-Key": os.environ["TIKLIVE_KEY"]}
def get_hashtag(name):
r = requests.get(
f"{API}/challenge-info-name/",
headers=HEADERS,
params={"name": name},
timeout=15,
)
r.raise_for_status()
return r.json()
candidates = ["smallbusinesscheck", "founderlife", "saasmarketing"]
meta = {h: get_hashtag(h) for h in candidates}
for name, m in meta.items():
print(name, m["cha_name"], m["user_count"], m["view_count"], m["is_commerce"])
Drop any candidate where is_commerce is true unless you are running a paid campaign - commerce challenges are advertiser auctions, not organic discovery surfaces.
For each surviving candidate, pull a sample of recent posts via /challenge-posts/. Use the id from the previous step as challenge_id. The response shape is the standard videos payload: top keys videos, cursor, hasMore (camelCase boolean). Each video item is flat snake_case with digg_count, comment_count, share_count, play_count, create_time, and an author object.
import time
def sample_posts(challenge_id, region=None, pages=3, per_page=30):
out, cursor = [], "0"
for _ in range(pages):
params = {
"challenge_id": challenge_id,
"count": per_page,
"cursor": cursor,
}
if region:
params["region"] = region
r = requests.get(
f"{API}/challenge-posts/",
headers=HEADERS,
params=params,
timeout=15,
).json()
out.extend(r.get("videos", []))
if not r.get("hasMore"):
break
cursor = r.get("cursor", "0")
return out
def density_and_velocity(videos, window_days=14):
if not videos:
return 0.0, 0
now = int(time.time())
cutoff = now - window_days * 86400
likes = [v.get("digg_count", 0) for v in videos]
density = sum(likes) / len(likes)
recent = sum(1 for v in videos if v.get("create_time", 0) >= cutoff)
velocity = recent / len(videos)
return density, velocity
Velocity here is expressed as the share of the sample that was posted in the last 14 days. A score of 0.6 means 60% of the sampled posts are recent - a healthy, growing tag. A score of 0.05 means the tag is mostly historical.
Take a set of "target creators" - either accounts your audience follows or competitors you track - and check what fraction of your hashtag sample comes from that set or its first-degree neighbors.
TARGET_CREATORS = {"acme_designs", "studio_north", "indiehackerlife"}
def overlap_score(videos, targets):
authors = {v.get("author", {}).get("unique_id", "") for v in videos}
authors.discard("")
if not authors:
return 0.0
hits = authors & targets
return len(hits) / len(authors)
You can grow the TARGET_CREATORS set programmatically by walking /userinfo-by-username/ for each seed account and then their public following list - useful if you want a dynamic target set rather than a hand-curated one.
Now combine the signals into a single 0-100 score. The weights below are a starting point - tune them to your goals (paid campaigns weight overlap higher; organic content weights velocity higher).
def normalize(values):
if not values:
return {}
lo, hi = min(values.values()), max(values.values())
if hi == lo:
return {k: 0.5 for k in values}
return {k: (v - lo) / (hi - lo) for k, v in values.items()}
def score_hashtags(candidates, targets, region=None):
raw = {}
for name in candidates:
meta = get_hashtag(name)
if meta.get("is_commerce"):
continue
videos = sample_posts(meta["id"], region=region)
density, velocity = density_and_velocity(videos)
overlap = overlap_score(videos, targets)
ad_penalty = 0.5 if meta.get("is_commerce") else 0.0
raw[name] = {
"cha_name": meta["cha_name"],
"user_count": meta["user_count"],
"view_count": meta["view_count"],
"density": density,
"velocity": velocity,
"overlap": overlap,
"ad_penalty": ad_penalty,
}
n_density = normalize({k: v["density"] for k, v in raw.items()})
n_velocity = normalize({k: v["velocity"] for k, v in raw.items()})
n_overlap = normalize({k: v["overlap"] for k, v in raw.items()})
for k, v in raw.items():
v["score"] = round(
100 * (
0.35 * n_velocity[k]
+ 0.30 * n_density[k]
+ 0.30 * n_overlap[k]
- 0.05 * v["ad_penalty"]
),
2,
)
return sorted(raw.values(), key=lambda x: x["score"], reverse=True)
The output is a ranked list where the top entries are hashtags that are growing, dense with engagement, and used by creators close to your audience - regardless of whether their lifetime view count is 50 million or 5 billion.
Run the scoring pipeline weekly. Take the top 5 tags whose velocity score crossed a threshold in the last week and brief them into the content team. These are the tags where the discovery surface is actively expanding.
For TikTok Ads spark ad campaigns, you want overlap-heavy tags - your audience needs to already live there. Re-weight the scoring function with overlap at 0.5 and velocity at 0.2.
Pull a competitor's recent posts via /user-posts/, parse hashtags out of the title field with a regex, then run each through the scoring pipeline. The competitor's tag mix tells you their distribution strategy; the scores tell you which of their tags are actually working.
/challenge-info-name/ returns an unusually low user_count for a tag you know is widely used in the app, the tag is likely shadow-limited. Skip it.is_commerce: true is a branded challenge run by an advertiser. Posting under it for organic reach is almost never effective unless you are part of the campaign.TikTok is fundamentally a regional graph. A hashtag that ranks in the US has a different velocity profile in Brazil or Indonesia, and many tags exist in language-specific variants (#smallbusinesscheck vs #petitcommerce vs #negocioscaseros).
The /challenge-posts/ endpoint accepts a region parameter using ISO-3166 alpha-2 codes. The full code list comes from /region-list/, which returns a flat object where keys are uppercase country codes and values are country names:
regions = requests.get(
f"{API}/region-list/", headers=HEADERS, timeout=15
).json()
# regions == {"AD": "Andorra", "AE": "United Arab Emirates", ...}
for code in ["US", "BR", "DE", "ID"]:
videos = sample_posts(challenge_id, region=code)
d, v = density_and_velocity(videos)
print(code, regions[code], "density:", round(d), "velocity:", round(v, 2))
The pattern: score the same hashtag across the regions you care about, then localize content production to the regions where the score is strongest. Many growth teams skip this step and lose 6-12 months to the wrong-region-wrong-tag mismatch.
The full pipeline runs in under two minutes for ~50 candidate hashtags. Wrap the scoring function in a script that:
tag)score_hashtags for each regionimport csv, pathlib, datetime
def weekly_report(tag_csv, creator_csv, region_csv, out_dir):
tags = [r["tag"] for r in csv.DictReader(open(tag_csv))]
targets = {r["unique_id"] for r in csv.DictReader(open(creator_csv))}
regions = [r["code"] for r in csv.DictReader(open(region_csv))]
out = pathlib.Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
today = datetime.date.today().isoformat()
for code in regions:
ranked = score_hashtags(tags, targets, region=code)
lines = [f"# Hashtag report {code} {today}", ""]
lines.append("| Tag | Score | Velocity | Density | Overlap |")
lines.append("|---|---|---|---|---|")
for r in ranked[:20]:
lines.append(
f"| #{r['cha_name']} | {r['score']} | "
f"{r['velocity']:.2f} | {int(r['density'])} | "
f"{r['overlap']:.2f} |"
)
(out / f"{code}-{today}.md").write_text("\n".join(lines))
if __name__ == "__main__":
weekly_report(
"tags.csv", "creators.csv", "regions.csv", "reports/"
)
Total API spend: roughly 1 call to /challenge-info-name/ + 3 calls to /challenge-posts/ per hashtag per region. For 50 hashtags x 4 regions that is 800 credits per weekly run - trivial under any pay-as-you-go credit balance. See /pricing/ for the credit math; you can prototype the script against your free starter credits, or jump into the /playground/ to run individual calls and inspect the JSON before you write any code. If you hit edge cases on specific hashtags, the team at /contact/ can help.
It is TikTok's internal terminology. Every hashtag is modeled as a challenge object with the same schema regardless of whether it is a viral dance trend or a generic descriptor like #cooking. The hashtag name lives in the cha_name field, not name.
60-90 recent posts (2-3 pages of /challenge-posts/ at count=30) is enough for stable engagement density and velocity numbers. Smaller samples are noisy; larger samples burn credits without changing the ranking much.
Weekly for content calendar planning. Daily during a campaign launch. Hashtag velocity can shift inside 48-72 hours, so anything slower than weekly will miss real surges.
Yes - the same density and velocity logic works on a creator's recent /user-posts/ output. That gives you a "creator fit score" you can use to vet influencer partnerships before you negotiate.
The API will still return the challenge object, but /challenge-posts/ will return very few or very stale videos. If the velocity score is near zero on a tag you know is widely used in the app, treat it as limited and remove it from your rotation. Track your live API health on the status page and explore more workflows on the blog.
Ready to put what you read into code? Try our endpoints live or grab the full reference.