← All articles

How to Scrape TikTok and Instagram Engagement Data as JSON

If you have ever tried to scrape a TikTok video or an Instagram post with a simple HTTP request, you already know the two problems. The page comes back nearly empty, and the numbers you can see are rounded to things like “1.2M plays”. For social listening, influencer vetting, or campaign reporting, “1.2M” is not good enough. You need the exact integer.

This guide explains why these two sites are hard to scrape, where the real numbers actually live inside the page, and how to extract exact engagement data as structured JSON. Everything here is reproducible with open-source tools, and at the end we cover the shortcut for teams that would rather not maintain the plumbing.

Why TikTok and Instagram Fight Back

Both platforms are single-page applications. The HTML your first request receives is a shell: some meta tags, a title, and a bundle of JavaScript. The content you see in a real browser is built after the page loads, when that JavaScript runs and hydrates the DOM with data.

That has two consequences for scraping:

  • A plain request returns almost nothing. Tools like requests or a bare fetch never run the JavaScript, so the visible text - captions, counts, usernames - is simply not in the response body. You get a page title and little else.
  • The visible numbers are for humans, not machines. When the page does render, it paints abbreviated counts: “1.2M”, “34.5K”, “980”. Those strings are lossy. You cannot recover the exact value from “1.2M”, and rounding boundaries move over time.

On top of that, both sites run anti-bot checks, rate limits, and login walls, so naive scraping gets throttled or blocked quickly.

Where the Real Numbers Actually Live

Here is the good news. Even though the visible text is rounded, the exact integers are almost always present in the page, just not where you are looking. Modern SPAs ship an embedded JSON payload so the app can hydrate instantly. That payload carries the precise data.

  • TikTok embeds a large JSON blob in a script tag, historically SIGI_STATE and more recently __UNIVERSAL_DATA_FOR_REHYDRATION__. Inside it you will find fields like playCount, diggCount (likes), commentCount, shareCount, and collectCount (saves) as real numbers.
  • Instagram exposes post data through embedded JSON and its internal GraphQL responses. A post object typically includes edge_media_preview_like.count, edge_media_to_comment.count, the owner username, and taken_at_timestamp.

The trick is to stop scraping the rendered text and start reading the embedded payload. That is where “1.2M” becomes 1203481.

Extracting the Embedded JSON Yourself

You still need to render the page, or at least get past the anti-bot layer, before you can read that script tag. A headless browser handles both. Playwright is a good default because it drives a real Chromium build and lets you wait for the payload to appear.

import json
from playwright.sync_api import sync_playwright

def scrape_tiktok(url: str) -> dict:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(url, wait_until="networkidle")

        # Read the embedded hydration payload instead of the rendered text
        raw = page.locator(
            "script#__UNIVERSAL_DATA_FOR_REHYDRATION__"
        ).inner_text()
        browser.close()

    data = json.loads(raw)
    scope = data["__DEFAULT_SCOPE__"]["webapp.video-detail"]
    stats = scope["itemInfo"]["itemStruct"]["stats"]
    return {
        "plays": int(stats["playCount"]),
        "likes": int(stats["diggCount"]),
        "comments": int(stats["commentCount"]),
        "shares": int(stats["shareCount"]),
        "saves": int(stats["collectCount"]),
    }

The exact key path shifts over time, so treat it as something you will maintain. A more resilient pattern is to load the JSON and walk it for the stats object rather than hard-coding the full path.

For Instagram, the same idea applies, but you often get the cleanest data by intercepting the GraphQL response the app makes, rather than parsing a static script tag:

from playwright.sync_api import sync_playwright

def scrape_instagram(url: str) -> dict:
    result = {}

    def on_response(response):
        if "graphql/query" in response.url and response.status == 200:
            try:
                media = response.json()["data"]["xdt_shortcode_media"]
            except Exception:
                return
            result.update({
                "likes": media["edge_media_preview_like"]["count"],
                "comments": media["edge_media_to_comment"]["count"],
                "author_username": media["owner"]["username"],
                "posted_at": media["taken_at_timestamp"],
            })

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.on("response", on_response)
        page.goto(url, wait_until="networkidle")
        browser.close()

    return result

Response interception is powerful because it captures the exact payload the app itself uses. You are reading the same numbers the interface reads before it rounds them for display.

Staying Unblocked at Scale

One video works fine from your laptop. A thousand videos per hour from a single IP does not. If you plan to collect at volume, budget for the anti-bot problem up front:

  • Rotate residential IPs. Datacenter IPs get flagged fast on both platforms. Route requests through rotating residential proxies so each request looks like a different ordinary user.
  • Throttle and jitter. Add randomized delays between requests. Predictable, machine-timed traffic is one of the easiest signals to detect.
  • Reuse realistic browser fingerprints. Undetected browser builds and consistent headers reduce automated-client signals. Do not mix a mobile user agent with a desktop viewport.
  • Handle the empty-shell case. If your extractor gets a page with no payload, treat it as a soft block and retry later, rather than recording a zero.

None of this is exotic, but it is ongoing work. Selectors drift, GraphQL shapes change, and blocking gets tuned. That maintenance is the real cost of rolling your own.

The Shortcut: Ask for Parsed JSON

If you would rather skip the browser fleet and the payload archaeology, you can request the parsed data directly. ScrapeUnblocker handles the rendering and anti-bot layer for you, and its parsed-data mode reads the embedded payload on your behalf so you get exact integers instead of the rounded prose the page paints.

With parsed_data=true, a TikTok video URL returns plays, likes, comments, shares, and saves, and an Instagram post returns exact likes and comments, the real author.username, and a real posted_at. You send a URL, you get clean JSON, and you are not the one maintaining selectors when the markup shifts.

import requests

resp = requests.get(
    "https://api.scrapeunblocker.com/getPageSource",
    params={
        "url": "https://www.tiktok.com/@user/video/1234567890",
        "parsed_data": "true",
    },
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
data = resp.json()
print(data["plays"], data["likes"], data["comments"])

See the parsed data guide for the full field list and parameters.

FAQ

Why do the counts I scrape look rounded? Because you are reading the text the interface renders for humans, which is abbreviated (“1.2M”). The exact integer lives in the embedded JSON payload or the GraphQL response, not in the visible DOM text. Parse the payload and you get the real number.

Can I do this without a headless browser? Sometimes. If you can reach the embedded JSON or a JSON endpoint directly, a well-formed HTTP request can work. In practice, both platforms gate those responses behind anti-bot checks, so a real browser or a scraping API that renders the page is the reliable path.

Is it legal to scrape TikTok and Instagram? Scraping publicly visible data is generally treated differently from accessing private or login-gated content, but the rules depend on your jurisdiction, the platform terms, and how you use the data. Collect only public data, avoid personal data you do not need, and get legal advice for anything commercial.

How do I avoid getting blocked? Rotate residential IPs, add randomized delays, use realistic browser fingerprints, and retry soft blocks instead of recording zeros. At volume, an unblocking API removes most of this work because it manages rendering and rotation for you.

Wrapping Up

The reason social engagement data feels hard to scrape is a mismatch: you are reading rounded text, while the exact numbers sit one layer down in an embedded JSON payload. Once you read that payload instead of the DOM, TikTok and Instagram become just another structured source - plays, likes, comments, shares, saves, usernames, and timestamps, all as clean integers.

Build it yourself with Playwright when you want full control, or let ScrapeUnblocker return the parsed JSON directly when you would rather spend your time on the analysis than on the plumbing.

Try ScrapeUnblocker free

95%+ success rate · from 0.55€ per 1,000 calls · 500 free requests on signup.

Try it free → See pricing