TikTok Follower Scraper for extracting follower records, the exact follower total and a next-page cursor for a public TikTok account from TikTok.com. This repo has a free script that shows you exactly where the free path stops, and a TikTok follower data API that returns 30 records per page with cursor pagination.
This is the follower endpoint on its own. The TikTok Scraper repo covers the rest of the surface.
This repo reads public accounts only, and it does not publish anybody's follower list. Follower rows are real members of the public: on the page we sampled, 0 of 30 were verified, the median account had 100 followers, and 16 of 30 carried a written bio. So the committed sample is the response envelope with the followers array cut to 0 of 30, the row shape is documented as a schema table rather than a data dump, and the example script writes rows to a local gitignored file. More on that in Scope.
Last updated: 2026-07-20. Working against TikTok.com as of July 2026, and re-verified whenever TikTok changes their markup.
Every JSON block on this page was captured from the live API on 2026-07-20. The followers array is cut to 0 rows everywhere, and each block says so; the envelope fields shown are verbatim. The committed sample is in tiktok_follower_scraper_api_data/. Every code example calls the actual API and is runnable from tiktok_follower_scraper_api_codes/.
pip install requests
export CHOCODATA_API_KEY="your_key" # free: 1,000 requests, one-time, no card
python tiktok_follower_scraper_api_codes/follower.py tiktokThose three lines return this envelope, live from TikTok.com (the followers array holds 30 rows, cut to 0 here):
{
"uniqueId": "tiktok",
"follower_count": 94900000,
"total": 94891522,
"followers_count": 30,
"has_more": true,
"next_cursor": "1784582209",
"followers": []
}The total is the field people come for, and it is the exact count (94,891,522) where follower_count is the rounded display number. That total is the surface a direct call cannot reach on its own:
That is the whole point of this repo. The rest of this page is the scope it holds itself to, the free path and where it stops, and the endpoint reference.
- Scope: public accounts, personal data stays local
- Free TikTok Follower Scraper
- Avoid getting blocked when scraping TikTok followers
- TikTok Follower Scraper API reference
- Page through a public account's follower list
- Measured latency
- License
A follower list is a list of people, and most of them are private individuals with nothing public about them beyond the fact that they follow an account. That shapes what this repo is willing to be.
What it does: read the follower records TikTok exposes for a public account, one page of 30 at a time, plus the exact follower total and a cursor for the next page.
What it does not do: publish those rows. On the page we captured for @tiktok, 0 of 30 accounts were verified, the median account had 100 followers, and 16 of 30 carried a bio. That is a list of ordinary members of the public, so:
- The committed sample (
follower_envelope.json) is the envelope withfollowerscut to 0 of 30. - The per-row fields are documented as a schema table, and their shape is committed as
follower_row_schema.jsonwith counts only, no rows. - The example scripts print the envelope and per-page aggregates, and write the rows themselves to a gitignored
followers.jsonl, so a follower list does not land in your terminal, a screenshot, or a CI log.
The data is yours to handle under whatever policy applies to you. This repo just keeps it off the public internet by default.
Reading a follower list from TikTok's own web endpoints is a two-hop job, and the free path gets through the first hop and stops dead at the second. No key, no cost:
python free_scraper/tiktok_follower_free_scraper.py tiktokSource: free_scraper/tiktok_follower_free_scraper.py. Hop 1 fetches the profile page and reads secUid out of the rehydration blob, which works from a plain client. Hop 2 calls TikTok's own follower-list XHR with that secUid, and that is the wall:
Hop 2 returns HTTP 200 with a zero-byte body, 3 times out of 3. Measured on 2026-07-20: the profile page handed over secUid (293,593 bytes, 200), and the follower-list XHR then answered 200 with 0 bytes on every one of three tries. TikTok's list endpoint needs a signature its own JavaScript generates in a real browser; without it you get a 200 and nothing to parse. It is not a 403 and not an error body, so r.json() throws on an empty string and a naive wrapper logs "no followers" forever. The script prints no follower rows, because on the free path there are none.
The empty 200 above is the headline, and it is not the only thing that fails quietly on this surface. All of the following is measured against TikTok on 2026-07-20, on @tiktok.
follower_count is rounded, total is exact, in the same envelope. follower_count is 94,900,000, the display number; total is 94,891,522, the real one. Read the wrong field into a time series and you record a number that is wrong by tens of thousands, with nothing to alert on.
The page size is 30, and asking for more returns nothing. count=30 returned 30 rows; count=31, 40 and 50 each returned 502, not a truncation to 30. So 30 is the ceiling per call, and you reach the rest through the cursor.
The uncursored first page is a moving target. Calling without a cursor returned a different set of 30 accounts each time, so its aggregates move between calls: the per-page median own-follower-count was 100, 119, 192 and 236 across the uncursored calls we made. Paging forward with next_cursor is stable and does not repeat rows (0 overlapping ids across two consecutive pages), but re-fetching "page one" compares different strangers each time.
| What bites you | Why | What it costs you |
|---|---|---|
The list XHR answers 200 with an empty body |
TikTok's follower-list endpoint needs a browser-minted signature; a plain client gets a zero-byte 200, 3 of 3. |
A try/except around r.json() logs "no followers" forever, with no error and no 403 to tell you it is a wall. |
follower_count is rounded, total is exact |
Both ship in the same envelope; follower_count is TikTok's display value, total is the real count. |
A trend built on follower_count is quietly wrong by tens of thousands. |
count caps at 30 |
30 returned 30 rows; 31, 40 and 50 each returned 502. |
Ask for 50 and you get a 502, not 30 rows. Page with the cursor instead. |
| The uncursored first page churns | It returned a different set of 30 accounts on each call; the cursor pages forward without repeating (0 overlap over two pages). | Re-fetch "page one" to diff growth and you are comparing different people, not new followers. |
| Rows are personal data | 0 of 30 verified, median 100 followers, 16 of 30 with a bio: ordinary members of the public. | Log or publish them and you have shipped a list of private individuals. |
| Avatar URLs expire | Each row's avatar is a signed CDN URL with an expiry parameter. |
Store the URL and it 403s later. Fetch the image promptly if you need it. |
The managed option, and the one this repo is built around: the Chocodata TikTok Follower Scraper API. One GET returns the page of 30 follower records that a direct call answers with an empty 200, plus the exact follower total and the next_cursor, with a ~99% success rate and no proxy management. It resolves the account's secUid for you and pages the list. Free for the first 1,000 requests.
Below is the TikTok Follower Scraper API reference to get you started: authentication, the error bodies, the rate limits, and the endpoint itself.
curl "https://api.chocodata.com/api/v1/tiktok/follower?api_key=YOUR_KEY&username=tiktok&count=30"import requests
r = requests.get(
"https://api.chocodata.com/api/v1/tiktok/follower",
params={"api_key": "YOUR_KEY", "username": "tiktok", "count": 30},
timeout=120,
)
d = r.json()
print(d["total"], d["followers_count"], d["has_more"])
# 94891522 30 TrueAfter running the example script, your terminal should look something like this. It prints the envelope and per-page aggregates, and keeps the rows out of view:
Get a key at chocodata.com (1,000 requests, one-time, no card).
Pass your key as the api_key query parameter. There is no header form and no OAuth step.
https://api.chocodata.com/api/v1/tiktok/follower?api_key=YOUR_KEY&username=tiktok
Nothing below is billed: you are only charged on a 2xx.
| Status | error code |
Meaning | Billed | What to do |
|---|---|---|---|---|
400 |
invalid_params |
Neither username nor url was supplied. The body names the issue and its path. |
no | Fix the query string. |
401 |
INVALID_API_KEY |
Key missing, unrecognised, or revoked. | no | Check api_key. Get one at chocodata.com. |
402 |
INSUFFICIENT_CREDITS |
Balance exhausted. | no | Top up or upgrade. |
404 |
item_not_found |
The account does not exist, was deleted, or is private. | no | Check the handle. |
429 |
RATE_LIMITED |
Over your plan's concurrency. | no | Back off and retry. |
502 |
extraction_failed |
TikTok did not serve this page. A count above 30 also lands here. |
no | Retry, and keep count at 30 or below. |
Unlike the per-video endpoints, this one does return a 404: because it resolves the profile first, it can tell a missing or private account apart from a page it simply could not serve. We ran https://example.com through the url parameter as a control and it returned 502, not a 200 carrying some other account under the TikTok schema.
A missing account, verbatim:
curl "https://api.chocodata.com/api/v1/tiktok/follower?api_key=YOUR_KEY&username=this_handle_does_not_exist_zzq123"{"error": "item_not_found", "message": "account_not_found_or_private", "retryable": false}A bad key, verbatim:
{"error": {"code": "INVALID_API_KEY", "message": "Api key not recognised."}}A call with no account, verbatim:
{"error": "invalid_params", "issues": [{"code": "custom", "message": "username or url is required", "path": []}]}The scripts in this repo turn each documented status into an actionable message, so a typo'd handle does not hand you a stack trace:
The limit is concurrency: how many requests you may have in flight at once.
| Plan | Concurrent requests |
|---|---|
| Free | 10 |
| Vibe | 30 |
| Pro | 50 |
| Custom | 100 to 500+ |
Exceed it and you get 429, not a queue. Every call is a synchronous GET: there is no webhook, callback or async job to poll. The examples use timeout=120 because this endpoint makes two upstream hops (the profile page, then the list) and the slowest call in the latency run below took 3.91s.
One page of follower records for a public account, plus the exact follower total and a cursor for the next page. This is the surface a direct call cannot reach: TikTok's own follower-list XHR answers a plain client with HTTP 200 and a zero-byte body.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
username |
string | one of username/url |
- | The TikTok handle. Use this one. A leading @ is stripped for you. |
url |
string (URL) | one of username/url |
- | Full profile URL; the handle is parsed out of it. |
count |
integer | no | 30 |
Rows to return this call. 1 to 30. 31, 40 and 50 each returned 502, so 30 is the page size. |
cursor |
string | no | - | The next_cursor from a previous call. Page forward with it rather than re-fetching page one. |
country |
string (ISO-2) | no | us |
Egress location. Pin it and keep it constant across a series. |
api_key |
string | yes | - | Your key. Query parameter, not a header. |
curl "https://api.chocodata.com/api/v1/tiktok/follower?api_key=YOUR_KEY&username=tiktok&count=30"Real response envelope. The 10 envelope fields are verbatim; the followers array is cut to 0 of 30 rows, because those rows are members of the public and this repo does not publish them (committed sample, same cut):
{
"uniqueId": "tiktok",
"secUid": "MS4wLjABAAAAv7iSuuXDJGDvJkmH_vz1qkDZYo1apxgzaxdBSeIuPiM",
"url": "https://www.tiktok.com/@tiktok",
"socialPlatform": "tiktok",
"follower_count": 94900000,
"followers": [],
"followers_count": 30,
"has_more": true,
"next_cursor": "1784582209",
"total": 94891522
}Each entry in followers carries these 11 fields. This is a schema, not a data dump:
| Field | Type | Description |
|---|---|---|
position |
integer | 1-based index within this page. |
id |
string | Numeric account id. |
uniqueId |
string | The @handle. |
nickname |
string | Display name. |
signature |
string | null | Bio text. Present on 16 of the 30 rows sampled. |
verified |
boolean | 0 of 30 were verified on the page sampled. |
secUid |
string | Stable account identifier. |
avatar |
string | Signed CDN URL, expires. |
url |
string | Profile URL built from the handle. |
followerCount |
integer | Their own follower count. Median 100 across the sample. |
followingCount |
integer | Their own following count. |
total is the field most people come for, and it is the exact one. follower_count is 94,900,000 rounded; total is 94,891,522. Same rounded-versus-exact split you see elsewhere on TikTok, different field names.
Field notes:
followers_countis the number of rows in this response (30), not the account's follower total. That istotal.- Pagination does not repeat itself. Two consecutive pages of 30 returned 0 overlapping ids. Pass
next_cursorthrough ascursorand keep going whilehas_moreis true. countabove 30 is a hard502, not a truncation to 30. Ask for 31 and you get nothing.- The example keeps rows out of your terminal.
follower.pyprints the envelope and per-page aggregates, and appends rows to a gitignoredfollowers.jsonlonly when you pass--write. Follower rows are personal data and do not belong in your scroll-back.
Runnable: tiktok_follower_scraper_api_codes/follower.py
The job this endpoint is built for: walk the whole list, one page of 30 at a time, following the cursor. paginate_followers.py does that, writes the rows to a gitignored file, and prints per-page counts only:
export CHOCODATA_API_KEY="your_key"
python tiktok_follower_scraper_api_codes/paginate_followers.py tiktok --pages 2Each page reports its row count, how many were verified, how many carried a bio, the median own-follower-count, and the overlap with everything seen so far. That overlap was 0 across two consecutive pages, which is how you confirm the cursor is advancing rather than handing back the same rows. The rows go to followers.jsonl; the terminal never shows a handle.
Source: tiktok_follower_scraper_api_codes/paginate_followers.py
6 consecutive calls to /tiktok/follower from a laptop against the live API on 2026-07-20. All 6 returned 200. Each call makes two upstream hops, the profile page then the list, which is why it runs slower than the single-page endpoints.
| Metric | Value |
|---|---|
| calls made | 6 |
200 responses |
6 of 6 |
| median | 2.31s |
| fastest | 1.53s |
| slowest | 3.91s |
6 calls from one machine on one evening is a small sample, which is why the examples set timeout=120. Reproduce it with the scripts in this repo.
MIT. See LICENSE.







