---
title: "TikTok Scraper: How to Scrape TikTok Data in 2026"
description: "Build a TikTok scraper that survives: official APIs vs. scraping, why plain HTTP fails, and working Python code for profiles, videos, hashtags, and comments."
url: https://webscraping.ai/blog/how-to-scrape-tiktok
markdown_index: https://webscraping.ai/llms.txt
---
# TikTok Scraper: How to Scrape TikTok Data in 2026

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 answers many clients with a JavaScript challenge page instead of content, loads its feeds and comments client-side through API calls signed by obfuscated JavaScript, and judges every request by IP reputation and fingerprint. 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.
- **A plain `requests.get()` often gets a challenge page, not the profile.** When the real page does come back, profile and video data are embedded in it as JSON, but video grids, comments and hashtag feeds still load client-side over signed XHR.
- **IP quality matters more than proxy type.** In our September 2026 tests, plain requests from an ordinary broadband connection got the challenge every time, while a good datacenter proxy pool loaded profile and video pages 30 times out of 30. Measure your own pool and keep residential IPs as the fallback.
- **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-e2e` attributes are the most stable hooks, and AI field extraction removes the selector problem entirely.
- **For standard fields, skip parsing entirely.** The `/data` endpoint returns a TikTok video or profile as JSON from its URL for a flat 15 credits.
- **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, but not in the page's JSON.** The profile page shows `9.2M`, while the JSON embedded in the same page carries the exact figure. Video view counts are the exception: logged-out pages only expose them rounded to three significant figures.
- **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](https://webscraping.ai/use-cases/influencer-analytics) — 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.

**Challenge pages and client-side rendering.** A plain `GET` on `https://www.tiktok.com/@nasa` can return a ~1.5 KB "Please wait..." page that runs a JavaScript challenge before anything else loads (it did every time in our tests from a broadband connection, with Python `requests`, curl and Node alike). When you do get the real page, the profile or video record sits in a ` __UNIVERSAL_DATA_FOR_REHYDRATION__ ` JSON script, but the video grid, comments and hashtag feeds are fetched after hydration. A scraper built on `requests` + [Beautiful Soup](https://webscraping.ai/blog/beautiful-soup-python) alone gets the challenge page some or all of the time, and never gets the feeds.

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

```python
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 good proxies (residential on the heavier pages) and fingerprint patching. Expect to spend most of your maintenance time on detection rather than parsing. Our [Playwright guide](https://webscraping.ai/blog/playwright-web-scraping) and [headless browser guide](https://webscraping.ai/blog/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.

```bash
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](https://webscraping.ai/) 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:

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

### Skipping the parsing: structured TikTok data with `/data`

If you only need the standard fields, our [TikTok scraper API](https://webscraping.ai/tiktok-scraper-api) (the [`/data` endpoint](https://webscraping.ai/docs#data)) returns TikTok videos and profiles as JSON from their normal URL. Proxies, rendering and parsing happen on our side, and it costs a flat 15 credits per page:

```python
def tiktok_data(url: str) -> dict:
    response = requests.get(
        "https://api.webscraping.ai/data",
        params={"api_key": API_KEY, "url": url},
        timeout=120,
    )
    response.raise_for_status()
    return response.json()

profile = tiktok_data("https://www.tiktok.com/@nasa")
print(profile["parse_status"]) # "ok", "parse_failed" or "not_found" (all billed)
if profile["data"]:
    print(profile["data"]["nick_name"], profile["data"]["fans"], profile["data"]["heart"])

video = tiktok_data("https://www.tiktok.com/@nasa/video/7301234567890123456")
if video["data"]:
    print(video["data"]["text"], video["data"]["play_count"], video["data"]["digg_count"])
```

Field names follow the most widely used TikTok scraper output (`fans`, `heart`, `digg_count`, `play_count`, `author_meta`, `music_meta`), so existing pipelines map across with few changes. A profile's post list isn't included, and logged-out view counts are rounded to three significant figures. Use `/html` or `/ai/fields` below when you need more than the standard fields.

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

```python
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](https://webscraping.ai/ai-web-scraping) 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:

```python
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](https://webscraping.ai/use-cases/brand-sentiment-tracking), 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.

```python
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](https://webscraping.ai/blog/is-web-scraping-legal) 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](https://webscraping.ai/use-cases/social-media-monitoring) or [influencer analytics](https://webscraping.ai/use-cases/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](https://webscraping.ai/) 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.
