Guide · Python

Get TikTok follower and like counts with Python

Pull followers, total likes and video counts for a list of TikTok creators, add engagement measured on their latest videos, and get one CSV row per creator.

ScrapingBot 7 min read
Endpoints
/user/info, /user/posts
Cost
2 credits per creator
Language
Python 3.9+
Needs
requests
On this page

A follower count says how many people once tapped follow. It does not say how many of them still watch. For shortlisting creators, reporting on competitors or tracking your own accounts, you want both numbers side by side: the profile counters, and how the last few videos actually performed.

This guide gets the counters from /user/info, the latest videos from /user/posts, and turns them into an engagement rate and a typical view count. A small thread pool runs the lookups for a whole list of handles within your plan's concurrency limit. The examples use NASA's account, and every number shown is from a real response.

Before you start

  • A ScrapingBot API key. Create a free account for 100 credits, enough for 50 creators. No card needed.
  • Python 3.9 or newer and pip install requests.
  • A list of TikTok handles, with or without the @.

Followers and likes: /user/info

Send the handle as unique_id. A profile URL such as https://www.tiktok.com/@nasa works too.

curl -X POST https://scrapingbot.io/api/v1/tiktok \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endpoint": "/user/info", "params": {"unique_id": "nasa"}}'

The response has two parts: user with the profile, and stats with the counters.

{
  "success": true,
  "data": {
    "user": {
      "id": "7664638705177150477",
      "uniqueId": "nasa",
      "nickname": "NASA",
      "verified": true,
      "privateAccount": false,
      "signature": "Making the seemingly impossible, possible.✨",
      "secUid": "MS4wLjABAAAAU9BRVzC8oCaegVnia8IbqWhPb_-dbU7s00Y3wS1_Nx8g5RUaYvyXrpejgjdxTwd6",
      "avatarLarger": "https://p16-common-sign.tiktokcdn-us.com/…",
      …
    },
    "stats": {
      "followerCount": 1900000,
      "followingCount": 23,
      "heartCount": 10200000,
      "heart": 10200000,
      "diggCount": 0,
      "videoCount": 52
    }
  },
  "duration": "0.74",
  "statusCode": 200,
  "creditsUsed": 1,
  "processed_time": 0.5828
}
FieldWhat it is
stats.followerCountFollowers.
stats.followingCountAccounts this account follows.
stats.heartCountTotal likes received across all of the account's videos. heart is the same number.
stats.diggCountLikes the account has given to other videos, not likes received. Easy to mix up with heartCount.
stats.videoCountVideos posted.
user.idThe numeric account ID. It does not change when the handle does.
user.uniqueIdThe current handle, without @.
user.verified, user.privateAccountThe verified badge, and whether the account is private.

Note the round numbers. Large counts come back rounded the way TikTok displays them, so NASA has "1,900,000" followers rather than an exact figure. That is fine for sorting creators into tiers, but on a big account a change of a few thousand followers will not show up from one day to the next.

Recent performance: /user/posts

The counters above add up an account's whole history. To see how it is doing now, fetch its latest videos. /user/posts returns them newest first, up to 10 per page; this call for NASA returned six:

PostedVideoViewsLikesCommentsShares
Oct 2Weekly "NASA Minute" recap51,3003,91212672
Oct 1Crew-13 heads to the ISS291,50036,1006581,670
Oct 1Crew-13 suits up before launch992,700107,5001,3782,740
Sep 29Landsat 9 images of Earth114,8006,453188239
Sep 28NASA x NFL reply video141,1006,96921271
Sep 25Late-September "NASA Minute"146,9005,311245109

Each video carries play_count (views), digg_count (likes), comment_count, share_count and collect_count (saves), plus create_time as a Unix timestamp. The newest video is only a day old, so its numbers are still climbing; keep that in mind when an account posted in the last few hours.

Engagement, and why the median

The script computes engagement as likes plus comments plus shares, divided by views, across the recent videos. For NASA that is 173,953 interactions on 1,738,300 views: 10.01%. Dividing by views rather than followers measures how people who actually saw the videos reacted, which is comparable between a creator with 20,000 followers and one with 2 million.

For reach, look at the table again. The average is 289,717 views per video, but only one video got near that: the launch-day clip at 992,700 pulled the average up. The median, 144,000, is a better answer to "how many views does a typical NASA video get right now". The script reports both, and uses the median for views_per_follower (0.076 for NASA), a quick check on whether an audience is still watching.

If saves matter for your use case, for example tutorials or recipes, add collect_count to the sum. It is in every video object.

The script

Save this as profile_stats.py. It makes two calls per handle, runs handles in parallel up to CONCURRENCY, and writes one CSV row per creator.

import csv
import statistics
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import date

import requests

API = "https://scrapingbot.io/api/v1/tiktok"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
CONCURRENCY = 1  # your plan's limit: Free 1, Starter 10, Startup 50


def call(endpoint, params, tries=4):
    """One API call. Failed calls are refunded, so a retry costs nothing extra."""
    for attempt in range(tries):
        res = requests.post(API, headers=HEADERS, timeout=60,
                            json={"endpoint": endpoint, "params": params})
        body = res.json()
        if body.get("success"):
            return body["data"]
        if res.status_code in (400, 401, 402):  # bad input, bad key, no credits
            raise RuntimeError(body.get("error"))
        time.sleep(2 ** attempt)  # 408, 429 or 5xx: wait and try again
    raise RuntimeError(f"{endpoint} kept failing: {body.get('error')}")


def profile_stats(handle):
    """Two calls (2 credits): the profile counters, then the latest videos."""
    profile = call("/user/info", {"unique_id": handle})
    user, stats = profile["user"], profile["stats"]
    videos = call("/user/posts", {"unique_id": handle, "count": 10}).get("videos", [])

    views = sum(v.get("play_count", 0) for v in videos)
    median = statistics.median(v.get("play_count", 0) for v in videos) if videos else 0
    actions = sum(v.get("digg_count", 0) + v.get("comment_count", 0) + v.get("share_count", 0)
                  for v in videos)
    return {
        "handle": user["uniqueId"],
        "user_id": user["id"],  # stays the same if the handle changes
        "name": user["nickname"],
        "verified": user["verified"],
        "followers": stats["followerCount"],
        "following": stats["followingCount"],
        "total_likes": stats["heartCount"],
        "videos": stats["videoCount"],
        "recent_videos": len(videos),
        "avg_views": round(views / len(videos)) if videos else 0,
        "median_views": round(median),
        "engagement_pct": round(100 * actions / views, 2) if views else 0,
        "views_per_follower": round(median / max(stats["followerCount"], 1), 3),
    }


def safe_stats(handle):
    try:
        return profile_stats(handle.strip().lstrip("@"))
    except RuntimeError as err:  # a renamed or private account should not stop the batch
        print(f"skipped {handle}: {err}")
        return None


if __name__ == "__main__":
    handles = sys.argv[1:] or ["nasa"]
    with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
        rows = [r for r in pool.map(safe_stats, handles) if r]

    for r in rows:
        print(f"@{r['handle']}: {r['followers']:,} followers, {r['total_likes']:,} likes, "
              f"{r['videos']} videos; last {r['recent_videos']}: median {r['median_views']:,} views, "
              f"{r['engagement_pct']}% engagement")

    path = f"tiktok_profiles_{date.today().isoformat()}.csv"
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=list(rows[0]) if rows else ["handle"])
        writer.writeheader()
        writer.writerows(rows)
    print(f"saved {len(rows)} of {len(handles)} profiles to {path}")
  • Set CONCURRENCY to your plan's limit. The Free plan allows 1 request in flight at a time, Starter 10 and Startup 50 (the concurrency table lists every plan). More workers than that only produce 429 responses, which cost nothing but slow the batch down.
  • 429 is retried with a growing pause, as are timeouts and server errors. A 400 (bad input), 401 (bad key) or 402 (out of credits) is raised at once, and failed calls are refunded.
  • One bad handle does not stop the batch. safe_stats prints which handle was skipped and why, and the final line says how many of your handles made it into the file.
  • pool.map keeps the input order, so the CSV rows follow your list even though the lookups finish in any order.

Run it

python profile_stats.py nasa
@nasa: 1,900,000 followers, 10,200,000 likes, 52 videos; last 6: median 144,000 views, 10.01% engagement
saved 1 of 1 profiles to tiktok_profiles_2026-10-03.csv
handle,user_id,name,verified,followers,following,total_likes,videos,recent_videos,avg_views,median_views,engagement_pct,views_per_follower
nasa,7664638705177150477,NASA,True,1900000,23,10200000,52,6,289717,144000,10.01,0.076

For a real list, put one handle per line in a file and pass them all: python profile_stats.py $(cat handles.txt). The file name carries the date, so running it weekly leaves a history you can compare in a spreadsheet.

Cost is 2 credits per creator: 100 creators is 200 credits, about four cents on the Starter plan ($49.99 for 275,000 credits).

Handles change, user IDs do not

Creators rename themselves. When they do, a lookup by the old unique_id fails, and any spreadsheet keyed on handles quietly breaks. The numeric user_id (NASA's is 7664638705177150477) stays the same for the life of the account.

That is why the script writes user_id into every row. If you track the same creators over time, key your records on it, and after the first lookup call /user/info with {"user_id": "7664638705177150477"} instead of the handle. The response has the same shape, including the current uniqueId, so you also learn the new handle.

Follower and following lists

/user/followers returns accounts that follow a user, and /user/following the accounts a user follows. Both take the numeric user_id only; a handle is rejected with 400. Each call returns a single page with no cursor, sized by count from 1 to 50, so this is a sample of an audience, not a full export. In the response, total is the account's full follower (or following) count, not the number of accounts returned.

Reusing call from the script, here are the accounts NASA follows:

from profile_stats import call

user_id = "7664638705177150477"  # NASA's data.user.id from /user/info
data = call("/user/following", {"user_id": user_id, "count": 50})
print(f"total: {data['total']}")
for a in data["followings"][:3]:
    print(f"@{a['unique_id']}: {a['follower_count']:,} followers, {a['aweme_count']} videos")
total: 23
@freedom250: 120,895 followers, 167 videos
@snoopy: 3,992,728 followers, 468 videos
@commerce: 8,359 followers, 10 videos

The accounts are in data.followings for /user/following and in data.followers for /user/followers, with the same fields. They use different names from /user/info: follower_count, aweme_count for videos and total_favorited for total likes. Their exact follower counts (120,895 rather than a rounded figure) are a handy side effect.

Where to go from here

Common questions

Is there an API for TikTok follower counts?

Yes. The /user/info endpoint returns followerCount, followingCount, heartCount (total likes) and videoCount for any public account, looked up by username or numeric ID. Each call costs 1 credit and needs no TikTok login.

How do I get TikTok followers with Python?

POST {"endpoint": "/user/info", "params": {"unique_id": "nasa"}} to https://scrapingbot.io/api/v1/tiktok with your key in the x-api-key header, then read data.stats.followerCount. The script in this guide does it for a whole list of handles and writes a CSV.

Why is the follower count a round number?

Large counts come back rounded, the way TikTok displays them: NASA shows 1,900,000 followers and 10,200,000 likes. They are fine for ranking and tiers, but too coarse to measure small daily changes on big accounts.

Can I get the list of accounts that follow someone?

Yes, with /user/followers. It needs the numeric user_id (a username is rejected with 400), returns a single page of up to 50 accounts set by count, and has no cursor. total in the response is the full follower count, not the number returned.

What happens when a creator changes their username?

Lookups by the old handle stop working, but the numeric data.user.id stays the same. Store it on the first lookup and send it as user_id to /user/info afterwards.

Start scraping in the next five minutes.

100 free credits, no credit card. One API key works for websites, TikTok, Instagram, Google, Amazon and ChatGPT.