TikTokAnswer

TikTok Research API access: who can apply and what to use if you can't

TikTok's Research API is for approved academic and non-profit researchers: 1,000 requests a day, a 4-week review, no commercial use. What else works.

Updated 10 min read

Short answer

  • TikTok's Research API, now titled Research Tools, is open to approved researchers at academic institutions or not-for-profits in eligible regions. TikTok's FAQ says creators, advertisers and commercial users are not eligible (developers.tiktok.com, read 2026-10-16).
  • Approved projects get 1,000 requests and 100,000 records a day, with a review that typically takes 4 weeks. The terms bar commercial use and require a refresh of downloaded data at least every 30 days.
  • ToolzerHub's /v1 TikTok routes return public profiles, videos by hashtag and videos by sound as JSON, with no approval: 1 credit per call, $0.99 to $1.90 per 1,000 credits, 100 free credits a month. A hashtag listing ends at about 160 unique videos.
On this page

TikTok's Research API is only for approved researchers: academics and not-for-profit organizations in eligible regions, reviewed project by project. TikTok's FAQ says creators, advertisers and commercial users are not eligible, and a developer account alone does not give access. We read TikTok's documentation and terms on 2026-10-16.

Who TikTok lets apply

TikTok's pages now title it Research Tools. Its product page says "Qualified researchers in the U.S., Europe, Canada, and Brazil can apply to study public data about TikTok content and accounts." The eligible organizations, from the same page:

  • Academic institutions in the U.S., EEA, UK, Canada or Switzerland.
  • Not-for-profit or independent research bodies in the EU. TikTok adds: "We are currently beta testing this service with select researchers in the U.S., UK, Switzerland, Norway, Iceland and Liechtenstein."
  • Academic institutions or not-for-profits in Brazil studying "online youth safety".

Applicants must be "independent of commercial interests", disclose funding, submit a research proposal and "provide evidence that the research has undergone an ethical review". PhD candidates need an endorsement letter from an advisor. You create a TikTok for Developers account with a professional email, submit the form and wait. TikTok says "You can typically expect to hear back from us within 4 weeks of submission." The pages we read print no application fee.

The FAQ asks "I am a creator, advertiser, or commercial user. Am I eligible for access to the Research Tools?" and answers "No." It adds: "Your developer account alone is not sufficient to grant you access to Research Tools."

What approved researchers get

There is no API key before approval. After it, TikTok generates a client key and secret, which you exchange for an access token (scope research.data.basic). The API reference lists endpoints for video queries, user info, comments, followers, following, liked, pinned and reposted videos, playlists and EU TikTok Shop data.

Video queries filter by region_code, keyword, hashtag_name and username, return up to 100 videos, and cover a creation-date window of at most 30 days. The FAQ sets the quota: "Currently, the daily limit is set at 1000 requests per day, allowing you to obtain up to 100,000 records per day across our APIs." Followers and following lists have their own allowance of 20,000 calls a day, up to 2M records. The quota resets at 12 AM UTC. Not-for-profits that get data through TikTok's Virtual Compute Environment instead are limited at first: "you can access up to 5,000 records per day in the 'Test Stage'".

The video search runs on an archive: "New videos take up to 48 hours to be added to the search engine, and statistics such as view count and follower count can take up to 10 days to update."

Terms to read before you apply

The Research Tools Terms "apply with effect from: January 22, 2026". Four clauses matter most:

  • No commercial use: you may not use the tools "for any commercial or unauthorized purpose, including beyond the scope of the approved Research", except publishing papers about it.
  • TikTok sets your quota "at its sole discretion" and can change or revoke it.
  • You agree to "regularly refresh TikTok Research Data at least every thirty (30) days, and delete data that is not available from the TikTok Research Tools at the time of each refresh."
  • You send TikTok a copy of any publication at least seven days before release.

What you can read without research approval

TikTok's other official route is the Display API, for showing "a TikTok creator's videos and their profile information" in your product. Its /v2/video/list/ call takes a token "that bears the authorization of the TikTok user" and returns that user's public videos, newest first, up to 20 per page. It needs Login Kit approval, so it reads accounts that sign in to your app, not arbitrary public accounts.

Research Tools Display API ToolzerHub
Who Researchers Sign-in apps API key
Data Public accounts Signed-in user Profiles, tags
Per call 100 videos 20 videos 30 videos

Get public TikTok data with ToolzerHub

We make ToolzerHub. Our TikTok API reads public data with one API key and no TikTok app, login or approval. The /v1 routes are profile, video, hashtag, hashtag videos and videos by sound, 1 credit per call each (our OpenAPI file, 2026-10-16). On /v1, the profile route returns an account's stats, rounded to three significant figures, not its recent videos, so you list videos through a hashtag or a sound. To list one account's own videos, see getting a TikTok user's videos. This script writes profiles and a hashtag's videos to two CSV files; prefix hashtags with #.

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", "")}
PROFILE_FIELDS = ["username", "user_id", "nickname", "verified", "followers", "following",
                  "likes", "videos", "bio"]
VIDEO_FIELDS = ["hashtag", "video_id", "author", "posted", "plays", "likes", "comments",
                "shares", "saves", "description", "url"]
calls = 0


def cell(value):
    # Spreadsheets run text that starts 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=5):
    # 429 and 503 are retryable; the error body may say how long to wait.
    global calls
    for attempt in range(tries):
        r = requests.get(f"{BASE}/{path}", headers=HEADERS, params=params, timeout=90)
        calls += 1
        if r.status_code == 400:  # malformed username or hashtag: skip it, keep the rest of the run
            print("bad request:", r.json()["error"]["message"].splitlines()[0])
            return None
        if r.status_code not in (429, 503) or attempt == tries - 1:
            r.raise_for_status()
            return r.json().get("data")  # None when the account or hashtag does not exist
        try:
            wait = r.json()["error"]["details"]["retryAfter"]
        except (ValueError, KeyError, TypeError):
            wait = 5 * (attempt + 1)
        time.sleep(min(wait, 60))


def profile_row(data):
    user, stats = data["user"], data.get("stats") or {}
    return {
        "username": user["unique_id"], "user_id": user["id"], "nickname": user.get("nickname"),
        "verified": user.get("verified"), "followers": stats.get("follower_count"),
        "following": stats.get("following_count"), "likes": stats.get("heart_count"),
        "videos": stats.get("video_count"),
        "bio": (user.get("signature") or "").replace("\n", " "),
    }


def video_row(tag, v):
    stats, stats_v2 = v.get("stats") or {}, v.get("stats_v2") 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"),
        "saves": stats_v2.get("collectCount"),
        "description": (v.get("desc") or "").replace("\n", " "),
        "url": f"https://www.tiktok.com/@{v['author']['unique_id']}/video/{v['id']}",
    }


def hashtag_videos(tag, max_pages):
    # A hashtag name resolves to a numeric challenge id; then page with the cursor.
    info = get("hashtag", {"hashtag": tag})
    if not info:
        print(f"skipped #{tag}: no such hashtag")
        return
    params = {"challenge_id": info["id"], "count": 30}
    for _ in range(max_pages):
        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"]


def main(sources, pages="2", prefix="tiktok"):
    people, tags = [], []
    for s in [s.strip() for s in sources.split(",") if s.strip()]:
        (tags if s.startswith("#") else people).append(s.lstrip("#@"))
    seen, n_profiles = set(), 0
    with open(f"{prefix}_profiles.csv", "w", newline="", encoding="utf-8") as pf, \
         open(f"{prefix}_videos.csv", "w", newline="", encoding="utf-8") as vf:
        profiles = csv.DictWriter(pf, fieldnames=PROFILE_FIELDS)
        videos = csv.DictWriter(vf, fieldnames=VIDEO_FIELDS)
        profiles.writeheader()
        videos.writeheader()
        for name in people:
            data = get("profile", {"username": name})
            stats = (data or {}).get("stats") or {}
            if data and not stats.get("follower_count") and stats.get("video_count"):
                data = get("profile", {"username": name})  # zero followers with videos: ask once more
            if not data:  # unknown usernames return 200 with data null
                print(f"skipped {name}: no such account")
                continue
            n_profiles += 1
            profiles.writerow({k: cell(v) for k, v in profile_row(data).items()})
        for tag in tags:
            for v in hashtag_videos(tag, int(pages)):
                if v["id"] not in seen:  # pages can overlap
                    seen.add(v["id"])
                    videos.writerow({k: cell(x) for k, x in video_row(tag, v).items()})
    print(f"wrote {n_profiles} profiles and {len(seen)} videos in {calls} calls")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit('usage: python tiktok_export.py "nasa,nba,#space" [pages_per_hashtag] [file_prefix]')
    if not HEADERS["x-api-key"]:
        sys.exit("set TOOLZERHUB_API_KEY first")
    main(*sys.argv[1:])

An unknown username or hashtag returns HTTP 200 with {"data": null}, and a malformed username returns 400; the script skips both with a message. cell() puts an apostrophe in front of text that starts with =, +, - or @, so a spreadsheet shows it as text instead of running it as a formula.

Run the script

bash
export TOOLZERHUB_API_KEY=your_key
python tiktok_export.py "tiktok,nasa,nba,natgeo,#space" 2

Output on 2026-10-16, with an unknown username and hashtag added to test the skips:

text
skipped zz_nobody_here_qq9x: no such account
skipped #zznobodyqq9x: no such hashtag
wrote 4 profiles and 60 videos in 9 calls

It took 15 seconds on that run; a later 4-call run took 57 seconds. The profile file, trimmed:

username followers following videos
tiktok 96,300,000 1 1,510
nasa 2,100,000 23 57
nba 27,300,000 267 24,300
natgeo 9,700,000 62 1,539

Notes on the output:

  • /v1/tiktok/profile rounds counts: all four follower counts have three significant figures, so use them for size, not daily growth.
  • In an earlier run, natgeo came back once with followers and following at 0; the repeat call gave 9,700,000. The script asks again when a profile has videos but zero followers.
  • The two #space pages gave 60 unique videos from 54 accounts, dated 2022-06-18 to 2026-10-16, in no date order. Median: 503,050 plays and 54,500 likes.
  • A hashtag listing is a sample, not an archive. Paging until has_more turned false on 2026-10-16, #space ended after 6 pages with 160 unique videos (13 repeats) and #nasa after 7 pages with 167 (22 repeats). The script drops repeats by video_id.
  • A video_id has 19 digits, so import it into Excel as Text. /v1/tiktok/video returned create_time: null for one video, so a date can be missing.

Where TikTok's Research API is the better pick

  • Comments, follower and following lists, and liked, pinned and reposted videos. Our /v1 routes have none of them. Our /v2 TikTok routes add keyword video search, a user's video list, comments, followers and following at 2 credits per call; we did not test them for this post.
  • Search across the whole platform by keyword, region and a date window, with up to 100 videos per call. A /v1 hashtag listing stops at a few hundred entries.
  • Research that needs a documented data source and a contract with TikTok. If you qualify, apply.

What it costs

Each /v1 call is 1 credit. The run above made 9 calls, so at most 9 credits: 1 to 2 cents at $0.99 to $1.90 per 1,000 credits ($1.45 on the $29 plan, packs from $19 for 10,000 credits). A whole hashtag listing is 1 lookup plus 6 or 7 pages: 7 or 8 credits for about 160 unique videos, under 2 cents. The free tier's 100 credits a month covers about 12 full hashtags. Failed calls cost 0 credits; we did not check whether a 200 with null data is charged.

Other official APIs that closed or restricted access: what X's API costs and the Instagram Basic Display API replacement.

We are not affiliated with TikTok or ByteDance.

Sources

Get this data from your own code

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