TikTokAnswer

TikTok hashtag API: list a hashtag's videos in two calls

TikTok's Research API filters by hashtag for researchers only. Ours lists a hashtag's videos in 2 calls; a whole hashtag costs 5 to 8 credits.

6 min read

Short answer

  • TikTok's Display API has three endpoints and none searches by hashtag. Only the Research API filters by hashtag, and it is for approved researchers.
  • ToolzerHub lists a hashtag's public videos in two calls: one turns the name into an ID, the other pages that ID's videos with a cursor, about 30 a page.
  • A hashtag list is a sample: it ended after 109 to 174 unique videos on nine walks. Each call is 1 credit, so a whole hashtag is 5 to 8 credits.
On this page

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.

bash
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:

json
{
  "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:

bash
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:

json
{
  "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 #.

python
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.

Get this data from your own code

The TikTok API returns it as JSON with one x-api-key header. 100 free credits monthly.