None of the three endpoints in TikTok's Display API searches by hashtag; they read a user's own profile and videos. The one official route that filters videos by hashtag, the Research API, is for approved researchers. ToolzerHub lists a hashtag's public videos in two calls: the first turns the name into TikTok's ID, the second pages through that ID's videos.
What TikTok's APIs do with hashtags
The Display API's three endpoints read a user's profile, a user's videos and videos by ID, each with that user's authorization. The Research API does filter by hashtag_name, but it needs a date window of 30 days at most and an approved researcher account. Our Research API post covers who can apply. If you qualify, use it: it adds the date window and returns more videos a call.
| Display API | Research API | ToolzerHub | |
|---|---|---|---|
| Hashtag filter | No | Yes, hashtag_name |
Yes, by ID |
| Who can use it | Apps a user logs in to | Approved researchers | Anyone with a key |
| Per call | 20 videos | Up to 100 videos | 30 by default, count up to 50 |
List a hashtag's videos with ToolzerHub
We make ToolzerHub. Our TikTok API takes the hashtag name, not its ID, in the first call and returns the ID for the second. Both are GET requests with the x-api-key header, 1 credit each.
export TOOLZERHUB_API_KEY=your_key
curl -H "x-api-key: $TOOLZERHUB_API_KEY" \
"https://api.toolzerhub.com/v1/tiktok/hashtag?hashtag=booktok"
The response, with the description cut:
{
"data": {
"id": "1622962893630470",
"title": "BookTok",
"stats": { "video_count": 0, "view_count": 560400000000 },
"stats_v2": { "video_count": "88024978", "view_count": "560368729997" }
}
}
stats.video_count came back 0, so read the total from stats_v2, where the count is a string. Then pass id as challenge_id:
curl -H "x-api-key: $TOOLZERHUB_API_KEY" \
"https://api.toolzerhub.com/v1/tiktok/hashtag_videos?challenge_id=1622962893630470"
The first of the 30 videos that came back, trimmed:
{
"id": "7223827494913215786",
"desc": "#WorldBookDay is 4/23, and I've joined the #BookTok community ...",
"create_time": 1681928430,
"author": { "unique_id": "jenna_bushhager", "verified": true },
"stats": { "play_count": 39200, "digg_count": 997, "comment_count": 32, "share_count": 36 },
"hashtags": ["worldbookday", "BookTok", "passthebook"]
}
The response also holds next_cursor ("53") and has_more (true). count goes up to 50 and defaults to 30.
Page through with the cursor
Send next_cursor back as cursor, exactly as returned. It is TikTok's own token, not a page number: #BookTok's first page gave "53" after 30 videos. The script below runs both calls per hashtag, drops repeated videos within a hashtag and writes a CSV. Hashtags are comma-separated, with or without #.
import csv
import os
import sys
import time
from datetime import datetime, timezone
import requests
BASE = "https://api.toolzerhub.com/v1/tiktok"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["hashtag", "video_id", "author", "posted", "plays", "likes", "comments", "shares",
"description", "url"]
calls = 0
def cell(value):
# Spreadsheets run text starting with = + - or @ as a formula; an apostrophe keeps it text.
if isinstance(value, str) and value[:1] in ("=", "+", "-", "@", "\t", "\r"):
return "'" + value
return value
def get(path, params, tries=4):
# 503 means TikTok challenged the request; wait longer before each retry.
global calls
for attempt in range(tries):
last = attempt == tries - 1
try:
r = requests.get(f"{BASE}/{path}", headers=HEADERS, params=params, timeout=90)
except requests.RequestException:
if last:
raise
time.sleep(10 * (attempt + 1))
continue
calls += 1
if r.status_code in (429, 500, 503) and not last:
wait = r.headers.get("Retry-After", "")
time.sleep(int(wait) if wait.isdigit() else 10 * (attempt + 1))
continue
r.raise_for_status()
return r.json().get("data") # None when the hashtag does not exist
def hashtag_videos(tag, max_pages):
info = get("hashtag", {"hashtag": tag}) # call 1: name -> challenge id
if not info:
print(f"skipped #{tag}: no such hashtag")
return
params = {"challenge_id": info["id"], "count": 30}
for _ in range(max_pages): # call 2, repeated: id -> videos
data = get("hashtag_videos", params)
if not data or not data["entries"]:
return
yield from data["entries"]
if not data["has_more"]:
return
params["cursor"] = data["next_cursor"] # pass it back as returned
def row(tag, v):
stats = v.get("stats") or {}
posted = v.get("create_time")
return {
"hashtag": tag, "video_id": v["id"], "author": v["author"]["unique_id"],
"posted": datetime.fromtimestamp(posted, timezone.utc).date().isoformat() if posted else "",
"plays": stats.get("play_count"), "likes": stats.get("digg_count"),
"comments": stats.get("comment_count"), "shares": stats.get("share_count"),
"description": (v.get("desc") or "").replace("\n", " "),
"url": f"https://www.tiktok.com/@{v['author']['unique_id']}/video/{v['id']}",
}
def main(tags, pages="3", out="hashtag_videos.csv"):
seen = set()
with open(out, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
for tag in [t.strip().lstrip("#") for t in tags.split(",") if t.strip()]:
for v in hashtag_videos(tag, int(pages)):
if (tag, v["id"]) not in seen: # pages can repeat a video
seen.add((tag, v["id"]))
writer.writerow({k: cell(x) for k, x in row(tag, v).items()})
print(f"wrote {len(seen)} videos to {out} in {calls} calls")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit('usage: python tiktok_hashtag_videos.py "booktok,cooking" [pages] [file.csv]')
if not HEADERS["x-api-key"]:
sys.exit("set TOOLZERHUB_API_KEY first")
main(*sys.argv[1:])
python tiktok_hashtag_videos.py "cooking,gardening" 2 wrote 113 videos (57 and 56) in 6 calls. A video_id has 19 digits, so import it into Excel as Text.
What came back for #BookTok
Two pages gave 30 and 29 videos, 59 unique IDs from 59 different accounts, with no video on both pages.
- The order is not by date: the videos run from 2021 to 2026, mixed, and 20 of the 59 were posted in the 30 days before the call.
- It is not sorted by plays either. Plays run from 249 to 58.3 million, median 216,100.
How deep a hashtag list goes
A hashtag listing ends long before the hashtag does. #BookTok shows 88 million videos in stats_v2. We paged twelve walks over eight other hashtags until has_more turned false, and nine finished with 109 to 174 unique videos in 4 to 7 pages (#football 162, #travel 174, #dance 109). Entries counted 6 to 29 more than unique IDs on those walks, so keep the de-duplication step.
The list is a sample of what TikTok shows under the tag. It takes no date range, region or sort order.
What it costs
Each call is 1 credit, so a whole hashtag is the lookup plus 4 to 7 pages: 5 to 8 credits for 109 to 174 videos. You get 100 free credits a month; $19 buys 10,000 credits that never expire, and plans for higher volume cover more hashtags. Set your key and run the script on your first hashtag.
For one account's own videos, see getting a TikTok user's videos. For a free script that runs on your machine, see scraping TikTok with Python.
We are not affiliated with TikTok or ByteDance.