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
}
| Field | What it is |
|---|---|
stats.followerCount | Followers. |
stats.followingCount | Accounts this account follows. |
stats.heartCount | Total likes received across all of the account's videos. heart is the same number. |
stats.diggCount | Likes the account has given to other videos, not likes received. Easy to mix up with heartCount. |
stats.videoCount | Videos posted. |
user.id | The numeric account ID. It does not change when the handle does. |
user.uniqueId | The current handle, without @. |
user.verified, user.privateAccount | The 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:
| Posted | Video | Views | Likes | Comments | Shares |
|---|---|---|---|---|---|
| Oct 2 | Weekly "NASA Minute" recap | 51,300 | 3,912 | 126 | 72 |
| Oct 1 | Crew-13 heads to the ISS | 291,500 | 36,100 | 658 | 1,670 |
| Oct 1 | Crew-13 suits up before launch | 992,700 | 107,500 | 1,378 | 2,740 |
| Sep 29 | Landsat 9 images of Earth | 114,800 | 6,453 | 188 | 239 |
| Sep 28 | NASA x NFL reply video | 141,100 | 6,969 | 212 | 71 |
| Sep 25 | Late-September "NASA Minute" | 146,900 | 5,311 | 245 | 109 |
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
CONCURRENCYto 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 produce429responses, which cost nothing but slow the batch down. 429is retried with a growing pause, as are timeouts and server errors. A400(bad input),401(bad key) or402(out of credits) is raised at once, and failed calls are refunded.- One bad handle does not stop the batch.
safe_statsprints which handle was skipped and why, and the final line says how many of your handles made it into the file. pool.mapkeeps 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
- Find creators worth looking up: the videos that use a sound give you every handle that used a song, sorted by views.
- Read the audience directly: export a video's comments and replies to CSV.
- Watching a sound rather than a creator? The daily sound tracker stores snapshots in SQLite.
- The /user/info and followers references list every parameter, the TikTok API overview lists the other endpoints, and the TikTok scraping guide covers the wider picture.
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.