Liveness probe — returns service name and server time.
curl -s "$BASE/api/health"
{
"status": "ok",
"app": "TikTokScope",
"time": 1760000000
}
A read-only JSON API for public TikTok and Douyin data — profiles, followers, videos, comments, hashtags, sounds, live status and tracked history. No API key, no third-party scraping vendor. Every route is served from the same origin as this page.
https://tiktokscope.382972.xyz
— same origin, so all routes below are relative (/api/…).
Set BASE to the origin you are calling (the site that serves this page),
then hit any route. Responses are JSON unless noted otherwise; CSV exports stream as
text/csv attachments.
# All paths are relative to the serving origin.
export BASE="https://tiktokscope.382972.xyz"
# 1. Liveness — cheapest way to verify the API is up.
curl -s "$BASE/api/health" | jq
# 2. A profile record.
curl -s "$BASE/api/profile/charlidamelio" | jq
# 3. The first page of that account's followers.
curl -s "$BASE/api/followers/charlidamelio?cursor=0&count=20" | jq '.users[0]'
# 4. A video plus its engagement stats.
curl -s "$BASE/api/video?url=https://www.tiktok.com/@charlidamelio/video/7300000000000000000" | jq
GET except the three
mutation routes under /api/track. Errors are returned as
{"detail": "…"} with HTTP 400 (bad input) or
502 (upstream scrape failure).
Liveness probe — returns service name and server time.
curl -s "$BASE/api/health"
{
"status": "ok",
"app": "TikTokScope",
"time": 1760000000
}
The most recent lookups recorded by the server (profiles, videos, hashtags, exports).
curl -s "$BASE/api/history"
{
"items": [
{
"kind": "profile",
"query": "charlidamelio",
"summary": "charli d'amelio",
"created_at": 1760000000.12
},
{
"kind": "video",
"query": "https://www.tiktok.com/@charlidamelio/video/7300000000000000000",
"summary": "day in the life #fyp",
"created_at": 1759999800.44
},
{
"kind": "hashtag",
"query": "fyp",
"summary": "321000000 videos",
"created_at": 1759999500.03
}
]
}
Full public profile record: identity, bio, verification and lifetime counters.
Path: username — handle with or without a leading @.
curl -s "$BASE/api/profile/charlidamelio"
{
"username": "charlidamelio",
"nickname": "charli d'amelio",
"user_id": "5831967",
"sec_uid": "MS4wLjABAAAA7Q8Yw2m9...",
"bio": "hi",
"verified": true,
"private": false,
"region": "US",
"avatar": "https://p16-sign.tiktokcdn-us.com/...",
"followers": 155400000,
"following": 1200,
"likes": 11500000000,
"videos": 2400
}
Aggregated engagement metrics for the account's recent videos, plus per-video breakdowns.
Path: username. Query: limit (int, recent videos to score).
curl -s "$BASE/api/profile/charlidamelio/analytics"
{
"username": "charlidamelio",
"generated_at": 1760000000,
"profile": {
"followers": 155400000,
"following": 1200,
"likes": 11500000000,
"videos": 2400
},
"recent_videos": [
{
"id": "7300000000000000000",
"desc": "day in the life #fyp",
"stats": {
"plays": 2500000,
"likes": 340000,
"comments": 4200,
"shares": 18000,
"collects": 9600
},
"metrics": {
"engagement_rate": 0.1449,
"save_rate": 0.0038,
"share_rate": 0.0072,
"comment_rate": 0.0017,
"like_rate": 0.136,
"like_per_1k": 136.0,
"amplification": 0.0812,
"est_watch_proxy": 2.1735
}
}
],
"averages": {
"engagement_rate": 0.121,
"save_rate": 0.0031,
"share_rate": 0.0056,
"comment_rate": 0.0014,
"like_per_1k": 118.7,
"amplification": 0.0742
}
}
Resolve many profiles in one request; failures are reported per row rather than aborting the batch.
Query: users — comma-separated handles (e.g. a,b,c).
curl -s "$BASE/api/batch/profiles?users=charlidamelio,khaby.lame,ghost_user"
{
"count": 3,
"results": [
{
"username": "charlidamelio",
"nickname": "charli d'amelio",
"verified": true,
"followers": 155400000,
"following": 1200,
"likes": 11500000000,
"videos": 2400,
"error": null
},
{
"username": "khaby.lame",
"nickname": "Khabane lame",
"verified": true,
"followers": 162800000,
"following": 48,
"likes": 2400000000,
"videos": 1100,
"error": null
},
{
"username": "ghost_user",
"nickname": null,
"verified": false,
"followers": null,
"following": null,
"likes": null,
"videos": null,
"error": "No user data for @ghost_user"
}
]
}
One page of a user's follower list, with the profile header included.
Path: username. Query: cursor (int, default 0), count (1–50, default 20).
curl -s "$BASE/api/followers/charlidamelio?cursor=0&count=20"
{
"profile": {
"username": "charlidamelio",
"nickname": "charli d'amelio",
"sec_uid": "MS4wLjABAAAA7Q8Yw2m9...",
"followers": 155400000
},
"users": [
{
"user_id": "6901234567890123456",
"username": "somefan",
"nickname": "Some Fan",
"sec_uid": "MS4wLjABAAAA...",
"bio": "fan account",
"avatar": "https://p16-sign.tiktokcdn-us.com/...",
"verified": false,
"private": false,
"followers": 1234,
"following": 321,
"likes": 5678,
"videos": 42
}
],
"total": 155400000,
"has_more": true,
"next_cursor": 20
}
Stream the follower list as a downloadable CSV, paginating upstream automatically.
Path: username. Query: limit (1–5000, default 500).
curl -s "$BASE/api/followers/charlidamelio/export?limit=500" -o charli-followers.csv
user_id,username,nickname,sec_uid,bio,verified,private,followers,following,likes,videos
6901234567890123456,somefan,Some Fan,MS4wLjABAAAA...,fan account,False,False,1234,321,5678,42
6901234567890123457,anotherfan,Another Fan,MS4wLjABAAAA...,,False,True,88,120,301,7
...
Full video record: author, lifetime stats, media URLs, music, hashtags and subtitle tracks.
Query: url (canonical video URL) or id + author.
# By URL
curl -s "$BASE/api/video?url=https://www.tiktok.com/@charlidamelio/video/7300000000000000000"
# Or by bare id + author
curl -s "$BASE/api/video?id=7300000000000000000&author=charlidamelio"
{
"id": "7300000000000000000",
"desc": "day in the life #fyp",
"create_time": 1699000000,
"duration": 15,
"author": {
"username": "charlidamelio",
"nickname": "charli d'amelio",
"sec_uid": "MS4wLjABAAAA7Q8Yw2m9...",
"avatar": "https://p16-sign.tiktokcdn-us.com/...",
"verified": true
},
"stats": {
"plays": 2500000,
"likes": 340000,
"comments": 4200,
"shares": 18000,
"collects": 9600
},
"cover": "https://p16-sign.tiktokcdn-us.com/...",
"play_url": "https://v16-webapp.tiktokcdn.com/...mp4",
"download_url": "https://v16-webapp.tiktokcdn.com/...mp4",
"music": "original sound",
"music_id": "7000000000000000000",
"music_author": "charlidamelio",
"subtitles": [
{ "lang": "eng-US", "format": "webvtt", "url": "https://.../subtitle.vtt" }
],
"challenges": ["fyp", "dayinthelife"]
}
Clean anonymous video-detail fallback (useful when the main record is gated); includes subtitle URLs.
Query: url or id.
curl -s "$BASE/api/video/reflow?id=7300000000000000000"
{
"id": "7300000000000000000",
"desc": "day in the life #fyp",
"create_time": 1699000000,
"author": {
"username": "charlidamelio",
"nickname": "charli d'amelio",
"sec_uid": "MS4wLjABAAAA7Q8Yw2m9..."
},
"stats": {
"plays": 2500000,
"likes": 340000,
"comments": 4200,
"shares": 18000
},
"music": "original sound",
"music_id": "7000000000000000000",
"cover": null,
"play_url": null,
"download_url": null,
"subtitles": [
{ "lang": "eng-US", "format": "webvtt", "url": "https://.../subtitle.vtt" }
],
"challenges": ["fyp"]
}
Parsed caption lines for a video, as an array and as a single joined string.
Query: url or id (+ optional author), lang (default eng-US).
curl -s "$BASE/api/video/subtitles?url=https://www.tiktok.com/@charlidamelio/video/7300000000000000000&lang=eng-US"
{
"lines": [
"hey guys",
"welcome back to my channel",
"today we are doing something fun"
],
"text": "hey guys\nwelcome back to my channel\ntoday we are doing something fun"
}
Proxy a TikTok CDN media URL as an MP4 attachment. Non-TikTok hosts are refused.
Query: url — a tiktokcdn, tiktokv or tiktok.com media URL.
curl -s "$BASE/api/download?url=https://v16-webapp.tiktokcdn.com/...mp4" -o tiktok.mp4
HTTP/1.1 200 OK
content-type: video/mp4
content-disposition: attachment; filename="tiktok.mp4"
<binary mp4 bytes>
Hashtag/challenge stats: title, description and lifetime view/video counts.
Path: tag — with or without a leading #.
curl -s "$BASE/api/hashtag/fyp"
{
"id": "2284",
"title": "fyp",
"desc": "",
"views": 59800000000000,
"videos": 321000000
}
Videos using a sound, paginated. Each item is a full video record.
Path: music_id. Query: cursor (int, default 0), count (1–50, default 20).
curl -s "$BASE/api/sound/7000000000000000000?cursor=0&count=20"
{
"items": [
{
"id": "7300000000000000000",
"desc": "day in the life #fyp",
"author": {
"username": "charlidamelio",
"nickname": "charli d'amelio"
},
"stats": {
"plays": 2500000,
"likes": 340000,
"comments": 4200,
"shares": 18000,
"collects": 9600
},
"music_id": "7000000000000000000"
}
],
"has_more": true,
"next_cursor": 20,
"total": 125000
}
Trending topics and hashtags surfaced by TikTok's discover module.
curl -s "$BASE/api/discover"
{
"items": [
{ "title": "#fyp", "link": "https://www.tiktok.com/tag/fyp" },
{ "title": "#summer", "link": "https://www.tiktok.com/tag/summer" },
{ "title": "#foodtok", "link": "https://www.tiktok.com/tag/foodtok" }
]
}
Douyin (抖音) hot-search leaderboard, ranked with hot values and labels.
curl -s "$BASE/api/douyin/trending"
{
"items": [
{
"rank": 1,
"word": "热歌推荐",
"hot_value": 12345678,
"label": 1,
"cover": "https://p3-sign.douyinpic.com/..."
},
{
"rank": 2,
"word": "夏日穿搭",
"hot_value": 9876543,
"label": null,
"cover": null
}
]
}
Douyin challenge (话题) detail with stats and a page of associated videos.
Path: ch_id. Query: cursor (int, default 0), count (1–50, default 20).
curl -s "$BASE/api/douyin/challenge/1234567890"
{
"id": "1234567890",
"title": "夏日穿搭",
"views": 42000000000,
"videos": 8600000,
"is_commerce": false,
"items": [
{
"id": "7300000000000000000",
"desc": "#夏日穿搭 今天穿这套",
"author": { "username": "dy_user", "nickname": "抖音用户" },
"stats": { "plays": 1200000, "likes": 98000, "comments": 1200, "shares": 5400 }
}
],
"has_more": true,
"next_cursor": 20
}
Live-status check for an account, with room metadata and stream URLs when live.
Path: username.
curl -s "$BASE/api/live/charlidamelio"
{
"username": "charlidamelio",
"is_live": true,
"room_id": "7300000000000000000",
"title": "come hang out 💕",
"viewer_count": 48210,
"like_count": 1204500,
"started_at": 1760000000,
"cover": "https://p16-sign.tiktokcdn-us.com/...",
"stream_urls": {
"flv": "https://pull-flv-l1.tiktokcdn.com/...",
"hls": "https://pull-hls-l1.tiktokcdn.com/....m3u8"
}
}
Keyword autocomplete suggestions for search/typeahead.
Query: q — the partial term.
curl -s "$BASE/api/suggest?q=charl"
{
"query": "charl",
"suggestions": [
"charlidamelio",
"charlie puth",
"charlotte tilbury",
"charlie brown"
]
}
Standard oEmbed metadata for a video URL, suitable for link unfurling.
Query: url — the canonical video URL.
curl -s "$BASE/api/oembed?url=https://www.tiktok.com/@charlidamelio/video/7300000000000000000"
{
"version": "1.0",
"type": "video",
"title": "day in the life #fyp",
"author_name": "charli d'amelio",
"author_url": "https://www.tiktok.com/@charlidamelio",
"width": 325,
"height": 570,
"html": "<blockquote class=\"tiktok-embed\" data-video-id=\"7300000000000000000\">…</blockquote>",
"thumbnail_width": 720,
"thumbnail_height": 1280,
"thumbnail_url": "https://p16-sign.tiktokcdn-us.com/..."
}
Track entities you care about, capture metric snapshots over time, and read the
history back as a time series. The three mutation routes share the /api/track path.
Start tracking an entity. Idempotent — re-posting updates the label.
Body (JSON): entity_type, entity_id, label.
curl -s -X POST "$BASE/api/track" \
-H 'Content-Type: application/json' \
-d '{"entity_type":"profile","entity_id":"charlidamelio","label":"Charli D Amelio"}'
{
"tracked": true,
"entity": {
"entity_type": "profile",
"entity_id": "charlidamelio",
"label": "Charli D Amelio",
"created_at": 1760000000.0
}
}
List tracked entities, optionally filtered by type.
Query: entity_type (optional, e.g. profile, video, hashtag).
curl -s "$BASE/api/track?entity_type=profile"
{
"items": [
{
"entity_type": "profile",
"entity_id": "charlidamelio",
"label": "Charli D Amelio",
"created_at": 1760000000.0
},
{
"entity_type": "video",
"entity_id": "7300000000000000000",
"label": "day in the life",
"created_at": 1759999000.0
}
],
"count": 2
}
Stop tracking an entity.
Query: entity_type, entity_id.
curl -s -X DELETE "$BASE/api/track?entity_type=profile&entity_id=charlidamelio"
{
"deleted": true,
"entity_type": "profile",
"entity_id": "charlidamelio"
}
Snapshot history for one tracked entity, oldest first — the metric time series.
Query: entity_type, entity_id, limit (default 200).
curl -s "$BASE/api/track/history?entity_type=profile&entity_id=charlidamelio&limit=200"
{
"entity_type": "profile",
"entity_id": "charlidamelio",
"items": [
{
"captured_at": 1759990000.0,
"metrics": { "followers": 155380000, "following": 1200, "likes": 11498000000, "videos": 2400 }
},
{
"captured_at": 1760000000.0,
"metrics": { "followers": 155400000, "following": 1200, "likes": 11500000000, "videos": 2400 }
}
],
"count": 2
}
Capture a fresh metrics snapshot now. Omit the body to snapshot every tracked entity.
Body (JSON, optional): entity_type, entity_id.
curl -s -X POST "$BASE/api/track/snapshot" \
-H 'Content-Type: application/json' \
-d '{"entity_type":"profile","entity_id":"charlidamelio"}'
{
"snapshot": {
"entity_type": "profile",
"entity_id": "charlidamelio",
"captured_at": 1760003600.0,
"metrics": {
"followers": 155410000,
"following": 1200,
"likes": 11500400000,
"videos": 2400
}
}
}
These derived values are computed from a video's stats (and its
duration), and the comment sentiment from the text returned by
/api/video/comments. They appear in the analytics payload and
are useful for comparing videos of different sizes.
| Metric | Formula | Meaning | Range |
|---|---|---|---|
engagement_rate |
(likes + comments + shares) / plays |
Share of views that produced an interaction. | 0–1+ (typ. 0.01–0.20) |
save_rate |
collects / plays |
How often a view leads to a bookmark — intent to revisit. | 0–1 |
share_rate |
shares / plays |
How often a view leads to a share — viral pull. | 0–1 |
comment_rate |
comments / plays |
Conversation intensity per view. | 0–1 |
like_per_1k |
(likes / plays) × 1000 |
Likes per 1,000 views — size-independent comparison. | 0–1000 |
amplification |
(shares + collects) / likes |
Extra distribution per like — how much the audience spreads and saves it. | 0–∞ (typ. 0.01–0.2) |
est_watch_proxy |
engagement_rate × duration |
Engagement rate scaled by video length (seconds) — a relative watch-time signal. | seconds (relative; null without duration) |
sentiment |
lexicon polarity over comment text | Tone of the comment section; returns a label and a score. |
score −1 to 1 |
null — never an invented number — while a missing
numerator counts as zero. Rounded to 4 decimals.
600s, followers 300s, video/reflow 1800s, comments 600s, feed/sound/trending/discover 600s, suggestions 3600s.4 concurrent requests, with a random 0.8–2.0s delay between calls. Exports page at 50 rows per request and sleep 1s between pages.count from 1 to 50 (default 20). Exports accept limit up to 5000 (default 500).400; an upstream scrape failure returns 502 with {"detail": "…"}. Empty upstream payloads usually mean throttling — back off and retry.
Comments
One page of top-level comments on a video, newest first, with cursor paging.
Query:
urlorid,cursor(int, default 0),count(1–50, default 20).Stream all comments for a video as CSV, paging upstream until the limit is reached.
Query:
urlorid,limit(1–5000, default 500).Replies to a specific comment, in the same shape as the top-level comment list.
Query:
item_id(video id),comment_id,cursor,count(1–50).