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 #.
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
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:
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/profilerounds 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
#spacepages 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_moreturned false on 2026-10-16,#spaceended after 6 pages with 160 unique videos (13 repeats) and#nasaafter 7 pages with 167 (22 repeats). The script drops repeats byvideo_id. - A
video_idhas 19 digits, so import it into Excel as Text./v1/tiktok/videoreturnedcreate_time: nullfor 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
/v1routes have none of them. Our/v2TikTok 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
/v1hashtag 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
- TikTok, Research Tools (product page), https://developers.tiktok.com/products/research-api/, read 2026-10-16
- TikTok, Research Tools Frequently Asked Questions (last updated September 1, 2026), https://developers.tiktok.com/doc/research-api-faq, read 2026-10-16
- TikTok, Getting Started - Research API (last updated September 1, 2026), https://developers.tiktok.com/doc/research-api-get-started, read 2026-10-16
- TikTok, Research API reference: Query Videos, Query User Info, Query Video Comments, Query User Followers (last updated September 1, 2026), https://developers.tiktok.com/doc/research-api-specs-query-videos, read 2026-10-16
- TikTok, Codebook for Research Tools, https://developers.tiktok.com/doc/research-api-codebook, read 2026-10-16
- TikTok, Research Tools Terms of Service (effective January 22, 2026), https://www.tiktok.com/legal/page/global/terms-of-service-research-api/en, read 2026-10-16
- TikTok, Display API overview (last updated August 4, 2026), https://developers.tiktok.com/doc/display-api-overview/, read 2026-10-16
- TikTok, Display API: List Videos (last updated August 4, 2026), https://developers.tiktok.com/doc/tiktok-api-v2-video-list/, read 2026-10-16
- TikTok, Display API: Get Started, https://developers.tiktok.com/doc/display-api-get-started/, read 2026-10-16
- ToolzerHub TikTok reference, https://api.toolzerhub.com/openapi.json, checked 2026-10-16
- ToolzerHub pricing, https://toolzerhub.com/pricing, checked 2026-10-16