If you want TikTok data, check the official APIs first — TikTok's Research API is free and comprehensive, but it is restricted to non-profit academic researchers in the US, EEA, UK, Switzerland, and Brazil. Everyone else is scraping the public web app, and that means solving three problems: TikTok renders everything client-side, signs its internal API calls with parameters generated by obfuscated JavaScript, and blocks datacenter IPs on sight. This guide covers what is actually reachable, what the official APIs give you, and working Python code for the two approaches that hold up in production.
Key Takeaways
- The Research API is free but non-commercial only. Applicants must "be independent from commercial interests and be able to conduct research on a not-for-profit or non-commercial basis pursuant to a public-interest mission." If you are a company, you do not qualify.
- Plain
requests.get()returns an empty shell. TikTok's web app is a client-rendered SPA; captions, counts, and comments arrive over XHR after the JavaScript runs. - Datacenter proxies are effectively burned on TikTok. Budget for residential IPs from request one rather than discovering it at scale.
- HTTP 200 does not mean success. TikTok serves login walls and CAPTCHA interstitials with a 200 status. Validate the presence of expected content, not the status code.
- Selectors are the main maintenance cost. TikTok ships markup changes often;
data-e2eattributes are the most stable hooks, and AI field extraction removes the selector problem entirely. - Scraping TikTok violates its Terms of Service and comments/profiles are personal data under GDPR. That is a real risk to price in, not a footnote.
What TikTok data can you scrape without logging in?
A logged-out browser can reach a useful subset of TikTok. Everything below is visible at a public URL with no session:
| Surface | URL pattern | What you get |
| Profile | /@username | Bio, follower/following counts, total likes, verified badge, video grid |
| Video | /@username/video/<id> | Caption, hashtags, sound, like/comment/share counts, upload date |
| Comments | rendered on the video page | Comment text, author handle, like count, reply counts |
| Hashtag | /tag/<hashtag> | Top and recent videos for the tag, aggregate view count |
| Sound | /music/<slug>-<id> | Videos using a given audio track |
| Search | /search?q=<query> | Videos and creators matching a keyword |
| Discover | /discover/<term> | Curated results for a term |
What you genuinely cannot get without an authenticated session, and should not try to: follower and following lists (paginated behind login), private or friends-only accounts, direct messages, draft or unpublished videos, and any per-video analytics that TikTok shows only to the account owner in Creator Tools. Anything gated behind a login is off the table — bypassing an access control is a materially different act from reading a public page, both legally and ethically.
Two nuances worth knowing before you plan a crawl:
- Follower counts are rounded in the DOM. The profile page shows
9.2M, not9,214,883. If you need exact figures, the rounded string is all the public web gives you. - View counts on hashtag pages are aggregate and lag. Treat them as trend signals, not accounting.
Should you use TikTok's official APIs first?
Yes — check them before writing any scraper. TikTok ships three distinct APIs, and they solve very different problems.
| API | Who qualifies | What it returns |
| Research API | Academic institutions in the US, EEA, UK, Switzerland; not-for-profit research organizations in the EU; academic/non-profit orgs in Brazil (youth-safety focus) | Public videos, comments, account info, followers/following, liked and pinned videos, reposts, TikTok Shop data |
| Display API | Any developer, via creator OAuth | Only the authenticating creator's own profile and public videos (user.info.basic, video.list scopes) |
| Commercial Content API | Researchers and professionals globally, by application | Ad library: paid ads, advertiser names, targeting details, impression counts, first/last-seen dates. EU data only |
The Research API is the real prize and the reason to check eligibility carefully. It queries public videos with 15+ filterable fields — hashtags, music, effects, region, engagement metrics — and returns up to 100 videos per request. Approval requires a defined research proposal, disclosed funding sources, demonstrated research expertise, ethical review approval, and agreement to TikTok's Research Tools Terms of Service. In the EU, the Digital Services Act adds a separate "Vetted Researcher" pathway via national Digital Services Coordinators.
The Display API is frequently misunderstood. It is an integration surface for letting creators embed their own TikTok content on your platform — it will not give you a third party's data. If your plan involves a creator connecting their own account, the Display API is the correct and fully sanctioned route, and you should use it instead of scraping.
The Commercial Content API is the one commercial teams overlook. If your question is about advertising on TikTok in the EU, it is an official, applicant-friendly source with a stated response time of about two business days.
If none of those fit — a brand tracking hashtag sentiment, an agency benchmarking influencer performance — you are scraping the public web app, with the trade-offs below.
Why do naive TikTok scrapers fail?
Four independent defenses, and you have to clear all of them.
Everything is client-rendered. A plain GET on https://www.tiktok.com/@nasa returns an app shell plus a bundle of JavaScript. The video grid, counts, and bio are injected after hydration. Any scraper built on requests + Beautiful Soup alone sees nothing useful.
The internal API is signed. Watch DevTools while you browse and you will see clean JSON coming back from endpoints like /api/post/item_list/ and /api/comment/list/. Those requests carry signed parameters — X-Bogus, msToken, _signature and successors — produced by obfuscated JavaScript that TikTok rotates. Replaying a captured URL works for minutes, not days.
Fingerprinting. TikTok profiles the client beyond the IP: TLS fingerprint, canvas and WebGL rendering, navigator properties, timing behaviour. A default headless Chrome is identifiable, which is why bare Playwright gets CAPTCHAs that a real browser does not.
Rate limiting per IP, plus soft failures. Tolerance per IP is low, and when you cross it TikTok usually does not return 429. It returns a 200 with a login wall or a verification page. Scrapers that check response.status_code and move on will silently record empty rows for hours.
Method 1: Scraping TikTok with a headless browser
Driving a real browser sidesteps request signing entirely: TikTok's own JavaScript computes the signatures, and you read the rendered result. This is the right choice for one-off research and small volumes.
from playwright.sync_api import sync_playwright
def scrape_profile(handle: str) -> dict:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1280, "height": 900},
locale="en-US",
user_agent=(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/140.0.0.0 Safari/537.36"
),
)
page = context.new_page()
page.goto(f"https://www.tiktok.com/@{handle}", wait_until="domcontentloaded")
# Soft-failure check: a login wall also returns HTTP 200.
page.wait_for_selector("[data-e2e='user-post-item']", timeout=20_000)
data = {
"handle": handle,
"followers": page.text_content("[data-e2e='followers-count']"),
"likes": page.text_content("[data-e2e='likes-count']"),
"bio": page.text_content("[data-e2e='user-bio']"),
"videos": page.eval_on_selector_all(
"[data-e2e='user-post-item'] a",
"els => els.map(e => e.href)",
),
}
browser.close()
return data
print(scrape_profile("nasa"))
Two things carry this example: wait_until="domcontentloaded" instead of the default load (TikTok's video preloading means load may never settle), and wait_for_selector doubling as the success check — if the selector never appears, you got a wall, not a profile.
The honest cost: a full browser per page is slow and memory-hungry, and this script still gets blocked without residential proxies and fingerprint patching. Expect to spend most of your maintenance time on detection rather than parsing. Our Playwright guide and headless browser guide cover the hardening in depth.
Method 2: Unofficial API wrappers
Libraries like TikTokApi for Python wrap TikTok's internal endpoints and solve signing by running Playwright in the background to mint valid tokens. You get structured JSON with no HTML parsing.
pip install TikTokApi
python -m playwright install
The trade-off is fragility. The library's own docs concede that "TikTok changes their structure from time to time," and its most common issue is EmptyResponseException — TikTok detecting the client as a bot. It also has no support for authenticated routes at all. Useful for exploration; do not put a production pipeline on it without a fallback path.
Method 3: Scraping TikTok with a scraping API
A web scraping API moves rendering, proxy rotation, and fingerprinting to the provider. You send a URL and get back rendered HTML from a real browser on a residential IP:
import requests
API_KEY = "YOUR_API_KEY"
def fetch_rendered(url: str) -> str:
response = requests.get(
"https://api.webscraping.ai/html",
params={
"api_key": API_KEY,
"url": url,
"js": "true",
"proxy": "residential",
"wait_for": "[data-e2e='user-post-item']",
"timeout": 30000,
},
timeout=60,
)
response.raise_for_status()
return response.text
html = fetch_rendered("https://www.tiktok.com/@nasa")
wait_for is doing the same job as wait_for_selector above: the request only returns once real content is present, so a login wall surfaces as a timeout instead of a silently empty page. Failed requests are not billed, which matters on a target that fails as often as TikTok does.
Surviving markup changes with AI field extraction
The data-e2e attributes are the most stable hooks TikTok offers, but "most stable" is not "stable." Every time the grid markup changes, a selector-based scraper starts writing nulls. The /ai/fields endpoint describes fields in plain language and lets a model locate them in the rendered page:
import requests
def extract_profile(handle: str) -> dict:
response = requests.get(
"https://api.webscraping.ai/ai/fields",
params={
"api_key": API_KEY,
"url": f"https://www.tiktok.com/@{handle}",
"js": "true",
"proxy": "residential",
"fields[username]": "The @handle shown on the profile",
"fields[display_name]": "The creator's display name",
"fields[followers]": "Follower count as displayed, e.g. '9.2M'",
"fields[likes]": "Total likes on the profile, as displayed",
"fields[bio]": "The profile bio text, or empty string if none",
"fields[verified]": "true if a verified badge is shown, otherwise false",
},
timeout=120,
)
response.raise_for_status()
return response.json()
print(extract_profile("nasa"))
Field descriptions survive markup rewrites that break CSS selectors. The trade-off is cost and latency: AI extraction adds 5 credits per request and takes noticeably longer than returning raw HTML. The pattern that works well is AI extraction for the fields that move (engagement counts, bios) and plain /html plus your own parsing for the parts that don't (video URLs and IDs).
Cost, so you can budget honestly: residential + JavaScript rendering is 25 credits per request. The free tier's 2,000 credits/month is therefore about 80 rendered TikTok pages — enough to validate an approach, not to run a pipeline. Failed requests are free.
Scraping TikTok comments
Comments load lazily as you scroll, so a single render returns only the first batch. Use js_script to scroll and js_timeout to give the XHR time to land:
def fetch_comments_html(video_url: str) -> str:
response = requests.get(
"https://api.webscraping.ai/html",
params={
"api_key": API_KEY,
"url": video_url,
"js": "true",
"proxy": "residential",
"js_script": "window.scrollTo(0, document.body.scrollHeight);",
"js_timeout": 8000,
"timeout": 60000,
},
timeout=90,
)
response.raise_for_status()
return response.text
This gets you the first two or three pages of comments. Going deeper means repeated calls with more aggressive scrolling, and the return per credit drops fast. For most brand sentiment work, the top few hundred comments across many videos beats exhausting one video's thread — and it collects far less personal data, which is the point of the next section.
How do you handle rate limits, pagination, and deduplication?
Three practical rules, and the code that implements them.
Deduplicate on the video ID, not the URL. The same video appears under a profile, a hashtag, and a sound, with different query strings each time. The numeric ID in /video/<id> is the stable key.
Persist as you go. A mid-run block should cost you the current page, not the batch. Write each record before fetching the next.
Pace yourself and jitter. Uniform intervals are themselves a fingerprint. On the free tier you also have a hard ceiling of 2 concurrent connections.
import random
import re
import sqlite3
import time
VIDEO_URL = re.compile(r"https://www\.tiktok\.com/@[\w.]+/video/(\d+)")
db = sqlite3.connect("tiktok.db")
db.execute("CREATE TABLE IF NOT EXISTS videos (id TEXT PRIMARY KEY, url TEXT, handle TEXT)")
def crawl(handles: list[str]) -> None:
for handle in handles:
try:
html = fetch_rendered(f"https://www.tiktok.com/@{handle}")
except requests.HTTPError as exc:
print(f"skipping @{handle}: {exc}")
continue
found = {m.group(1): m.group(0) for m in VIDEO_URL.finditer(html)}
inserted = 0
for video_id, url in found.items():
cur = db.execute(
"INSERT OR IGNORE INTO videos (id, url, handle) VALUES (?, ?, ?)",
(video_id, url, handle),
)
inserted += cur.rowcount
db.commit()
# Pagination signal: an all-duplicates page means you have caught up.
if found and inserted == 0:
print(f"@{handle}: no new videos, stop paginating")
time.sleep(random.uniform(4, 9))
The duplicate ratio is also your pagination signal for infinite-scroll pages. When a scroll-and-fetch round returns only IDs you already hold, you have reached the end of what that surface will give you — stop rather than burning credits on the same grid.
Is scraping TikTok legal?
Be clear-eyed about this: scraping TikTok breaches TikTok's Terms of Service, which prohibit automated access to the platform. That is a contractual matter rather than a criminal one in most jurisdictions, and the realistic consequences are IP and account blocks, cease-and-desist letters, and — for anyone with a commercial relationship with TikTok — potential loss of that relationship. It is a risk to weigh, not one to wave away.
Separately, and more consequentially, TikTok data is personal data. Usernames, bios, profile images, and comment text all identify individuals. Under GDPR that means you need a lawful basis before you collect it, and legitimate interest requires a documented balancing test — the fact that a page is public does not itself create one. European regulators have repeatedly held that scraping public profiles is still processing personal data. Under the CCPA/CPRA, publicly available information is carved out of "personal information," but that exemption is narrower than it sounds and does not cover inferences you derive and store.
Practical guardrails that materially reduce your exposure:
- Aggregate rather than accumulate. Store hashtag-level sentiment scores, not a database of who said what. Most brand-monitoring questions are answerable from aggregates.
- Do not build creator profiles. Cross-referencing handles across platforms into person-level dossiers is where scraping stops being analytics.
- Honour deletion. If a video or comment is removed, remove it from your store too.
- Set retention limits. Raw comment text with a 30-day TTL is a very different risk posture from an indefinite archive.
- Never touch gated data. Private accounts, follower lists behind login, DMs — out of scope, always.
Our guide to web scraping legality goes into the case law. If you are scraping personal data at scale in the EU, this is a conversation to have with a lawyer rather than with a blog post.
Which TikTok scraping approach should you use?
| Your situation | Use this |
| Academic, non-profit, in a supported region | Research API — free, comprehensive, sanctioned |
| Creators connect their own accounts | Display API — sanctioned, no scraping needed |
| Researching EU ads and advertisers | Commercial Content API |
| One-off analysis, tens of pages | Playwright locally, accept the CAPTCHAs |
| Exploratory JSON, tolerant of breakage | TikTokApi wrapper, with a fallback |
| Ongoing pipeline, hundreds to thousands of pages | Scraping API with js=true and residential proxies |
Conclusion
TikTok scraping is an arms race the platform is well funded to keep winning, so the question is not which technique defeats it permanently — none does — but how much of the maintenance you want to own. Check the official APIs first; if you qualify for the Research API, nothing else comes close. If you don't, a headless browser is fine for experiments and becomes a treadmill of fingerprint patches at scale.
For ongoing social media monitoring or influencer analytics pipelines, a scraping API absorbs the browsers, residential proxies, and retries so you are maintaining data models instead of evasion tactics — and AI field extraction means TikTok's next markup change costs you nothing. WebScraping.AI's free tier is 2,000 credits a month with no credit card, which is roughly 80 rendered TikTok pages: enough to point it at your own target list and see the block rate for yourself before committing.