Developer documentation

TikTokScope API

A read-only JSON API for public TikTok and Douyin data — profiles, followers, videos, comments, hashtags, sounds, live status and keyword search. No API key, no third-party scraping vendor. Every route is served from the same origin as this page.

Base URL https://tiktokscope.120319.xyz — same origin, so all routes below are relative (/api/…).

Quickstart

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.120319.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
Conventions. Every endpoint is GET and read-only. Errors are returned as {"detail": "…"} with HTTP 400 (bad input), 404 (not found), 502 (upstream scrape failure) or 500 (unexpected).

Meta

GET /api/health

Liveness probe — returns service name and server time.

curl -s "$BASE/api/health"
{
  "status": "ok",
  "app": "TikTokScope",
  "time": 1760000000
}
GET /api/status

Service status with the running release and uptime. This is the source the header version badge is read from.

curl -s "$BASE/api/status"
{
  "status": "ok",
  "app": "TikTokScope",
  "version": "3.1.0",
  "uptime_seconds": 128934,
  "time": 1760000000
}
GET /api/ready

Readiness probe — verifies the SQLite cache and the Node signer used for signed search are usable.

curl -s "$BASE/api/ready"
{
  "status": "ok",
  "checks": {
    "db": "ok",
    "signer": "ok"
  }
}
GET /api/history

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
    }
  ]
}

Profiles

GET /api/profile/{username}

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
}
GET /api/profile/{username}/analytics

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
  }
}
GET /api/batch/profiles

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"
    }
  ]
}
GET /api/creators/top new

Leaderboard of the most-followed creators currently tracked by the server, resolved to live profile cards.

Query: count (1–50, default 20).

curl -s "$BASE/api/creators/top?count=20"
{
  "items": [
    {
      "username": "khaby.lame",
      "nickname": "Khabane lame",
      "sec_uid": "MS4wLjABAAAA...",
      "avatar": "https://p16-sign.tiktokcdn-us.com/...",
      "verified": true
    },
    {
      "username": "charlidamelio",
      "nickname": "charli d'amelio",
      "sec_uid": "MS4wLjABAAAA...",
      "avatar": "https://p16-sign.tiktokcdn-us.com/...",
      "verified": true
    }
  ]
}

Search & analytics

Keyword search is signed server-side with the vendored Node signer. Analytics and benchmarking are computed from cached video records using the metrics defined below.

GET /api/search/users

Keyword search restricted to user accounts. Each hit is a lightweight profile card.

Query: q (required), cursor (int, default 0), count (1–30, default 12).

curl -s "$BASE/api/search/users?q=charli"
{
  "query": "charli",
  "cursor": 12,
  "has_more": true,
  "users": [
    {
      "username": "charlidamelio",
      "nickname": "charli d'amelio",
      "sec_uid": "MS4wLjABAAAA...",
      "signature": "hi",
      "avatar": "https://p16-sign.tiktokcdn-us.com/...",
      "verified": true
    }
  ]
}
GET /api/search/photos

Photo (image-post) search. Each result is a video record whose images array holds the slide URLs; derived metrics are attached as usual.

Query: q (required), cursor (int, default 0), count (1–30, default 12).

curl -s "$BASE/api/search/photos?q=sunset&count=12"
{
  "query": "sunset",
  "items": [
    {
      "id": "7300000000000000001",
      "desc": "golden hour #sunset",
      "create_time": 1699000000,
      "duration": 0,
      "author": {
        "username": "skylover",
        "nickname": "Sky Lover",
        "sec_uid": "MS4wLjABAAAA...",
        "verified": false
      },
      "stats": {
        "plays": 410000,
        "likes": 52000,
        "comments": 610,
        "shares": 2400,
        "collects": 8800,
        "reposts": 120
      },
      "metrics": {
        "engagement_rate": 0.1342,
        "save_rate": 0.0215,
        "share_rate": 0.0059,
        "comment_rate": 0.0015,
        "like_rate": 0.1268,
        "like_per_1k": 126.8,
        "amplification": 0.2154,
        "est_watch_proxy": null
      },
      "images": [
        ["https://p16-sign.tiktokcdn-us.com/...1.jpg"],
        ["https://p16-sign.tiktokcdn-us.com/...2.jpg"]
      ],
      "challenges": ["sunset"]
    }
  ],
  "has_more": true,
  "next_cursor": 12
}
GET /api/search/hashtags new

Hashtag/challenge search. Returns matching tags with their lifetime view and video counts.

Query: q (required).

curl -s "$BASE/api/search/hashtags?q=fy"
{
  "query": "fy",
  "items": [
    { "tag": "fyp", "views": 59800000000000, "videos": 321000000 },
    { "tag": "foryou", "views": 41200000000000, "videos": 214000000 },
    { "tag": "foryoupage", "views": 18900000000000, "videos": 98000000 }
  ]
}
GET /api/search/sounds new

Sound/music search. Each item describes a track and how many videos use it.

Query: q (required).

curl -s "$BASE/api/search/sounds?q=original%20sound"
{
  "query": "original sound",
  "items": [
    {
      "music_id": "7000000000000000000",
      "title": "original sound",
      "author": "chefanna",
      "duration": 24,
      "video_count": 125000,
      "cover": "https://p16-sign.tiktokcdn-us.com/..."
    }
  ]
}
GET /api/profile/{username}/videos

A creator's public video list, unsigned and paginated. Items carry derived metrics.

Path: username. Query: cursor (int, default 0), count (1–19, default 15).

curl -s "$BASE/api/profile/charlidamelio/videos?cursor=0&count=15"
{
  "username": "charlidamelio",
  "items": [
    {
      "id": "7300000000000000000",
      "desc": "day in the life #fyp",
      "create_time": 1699000000,
      "duration": 15,
      "author": {
        "username": "charlidamelio",
        "nickname": "charli d'amelio",
        "sec_uid": "MS4wLjABAAAA..."
      },
      "stats": {
        "plays": 2500000,
        "likes": 340000,
        "comments": 4200,
        "shares": 18000,
        "collects": 9600,
        "reposts": 180
      },
      "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
      }
    }
  ],
  "has_more": true,
  "next_cursor": 15
}
GET /api/profile/{username}/reposts

Videos a creator has reposted, newest first, in the same shape as the video list.

Path: username. Query: cursor (int, default 0), count (1–30, default 20).

curl -s "$BASE/api/profile/charlidamelio/reposts?count=20"
{
  "username": "charlidamelio",
  "items": [
    {
      "id": "7300000000000000009",
      "desc": "this edit #fyp",
      "create_time": 1698990000,
      "duration": 11,
      "author": {
        "username": "editor.friend",
        "nickname": "editor",
        "sec_uid": "MS4wLjABAAAA..."
      },
      "stats": {
        "plays": 540000,
        "likes": 76000,
        "comments": 900,
        "shares": 4100,
        "collects": 5200,
        "reposts": 640
      },
      "metrics": {
        "engagement_rate": 0.1519,
        "save_rate": 0.0096,
        "share_rate": 0.0076,
        "comment_rate": 0.0017,
        "like_rate": 0.1407,
        "like_per_1k": 140.7,
        "amplification": 0.1224,
        "est_watch_proxy": 1.6709
      }
    }
  ],
  "has_more": false,
  "next_cursor": 20
}
GET /api/profile/{username}/best-time

Samples a creator's recent videos and groups their engagement rate by weekday/hour (UTC), plus a duration-bucket curve.

Path: username. Query: limit (4–20, default 12).

curl -s "$BASE/api/profile/charlidamelio/best-time?limit=12"
{
  "username": "charlidamelio",
  "sampled": 12,
  "best_times": [
    { "weekday": 2, "hour": 18, "videos": 3, "avg_engagement_rate": 0.1421 },
    { "weekday": 5, "hour": 20, "videos": 2, "avg_engagement_rate": 0.1318 }
  ],
  "duration_curve": [
    { "bucket": "0-15s", "videos": 4, "avg_engagement_rate": 0.1611 },
    { "bucket": "15-30s", "videos": 5, "avg_engagement_rate": 0.1204 },
    { "bucket": "30-60s", "videos": 3, "avg_engagement_rate": 0.0982 },
    { "bucket": "60s+", "videos": 0, "avg_engagement_rate": null }
  ]
}
GET /api/benchmark

Compare up to four creators side by side. Each creator's recent videos are sampled to produce comparable rows.

Query: users — comma-separated handles; videos (3–12, default 4) sampled per creator.

curl -s "$BASE/api/benchmark?users=charlidamelio,khaby.lame&videos=4"
{
  "rows": [
    {
      "username": "khaby.lame",
      "nickname": "Khabane lame",
      "followers": 162800000,
      "following": 48,
      "likes_total": 2400000000,
      "videos_total": 1100,
      "sampled_videos": 4,
      "avg_engagement_rate": 0.1564,
      "avg_plays": 4200000,
      "plays_per_follower": 0.0258,
      "posts_last_30d": 2,
      "avg_days_between_posts": 4.2
    },
    {
      "username": "charlidamelio",
      "nickname": "charli d'amelio",
      "followers": 155400000,
      "following": 1200,
      "likes_total": 11500000000,
      "videos_total": 2400,
      "sampled_videos": 4,
      "avg_engagement_rate": 0.121,
      "avg_plays": 1800000,
      "plays_per_follower": 0.0116,
      "posts_last_30d": 3,
      "avg_days_between_posts": 2.7
    }
  ]
}
GET /api/video/transcript

Native description and subtitle translation for a video.

Query: url or id (+ optional author), lang (default es).

curl -s "$BASE/api/video/transcript?url=https://www.tiktok.com/@charlidamelio/video/7300000000000000000&lang=es"
{
  "id": "7300000000000000000",
  "target": "es",
  "description": "día en la vida #fyp",
  "subtitle": "hola chicos\nbienvenidos de nuevo a mi canal"
}

Followers

GET /api/followers/{username}

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
}
GET /api/followers/{username}/export

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
...

Videos

GET /api/video

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"]
}
GET /api/video/reflow

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"]
}
GET /api/video/subtitles

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"
}
GET /api/download

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>
GET /api/feed

Recommended videos for a creator, served from the embed feed endpoint. Use sec_uid when you already have it, otherwise pass a username to resolve it first.

Query: username or sec_uid, cursor (int, default 0), count (1–50, default 20).

curl -s "$BASE/api/feed?username=charlidamelio&count=20"
{
  "items": [
    {
      "id": "7300000000000000000",
      "desc": "day in the life #fyp",
      "author": "charlidamelio",
      "plays": 2500000,
      "likes": 340000,
      "comments": 4200,
      "cover": "https://p16-sign.tiktokcdn-us.com/..."
    }
  ],
  "has_more": true,
  "next_cursor": 20
}

Comments

GET /api/video/comments

One page of top-level comments on a video, newest first, with cursor paging.

Query: url or id, cursor (int, default 0), count (1–50, default 20).

curl -s "$BASE/api/video/comments?url=https://www.tiktok.com/@charlidamelio/video/7300000000000000000&cursor=0&count=20"
{
  "comments": [
    {
      "id": "7300000000000000001",
      "text": "first!",
      "create_time": 1699000100,
      "likes": 1200,
      "reply_count": 14,
      "username": "somefan",
      "nickname": "Some Fan",
      "sec_uid": "MS4wLjABAAAA...",
      "avatar": "https://p16-sign.tiktokcdn-us.com/..."
    }
  ],
  "total": 4200,
  "has_more": true,
  "next_cursor": 20
}
GET /api/video/comments/export

Stream all comments for a video as CSV, paging upstream until the limit is reached.

Query: url or id, limit (1–5000, default 500).

curl -s "$BASE/api/video/comments/export?url=https://www.tiktok.com/@charlidamelio/video/7300000000000000000&limit=500" -o comments.csv
id,username,nickname,text,likes,reply_count,create_time
7300000000000000001,somefan,Some Fan,first!,1200,14,1699000100
7300000000000000002,anotherfan,Another Fan,love this,340,2,1699000200
...
GET /api/comment/replies

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).

curl -s "$BASE/api/comment/replies?item_id=7300000000000000000&comment_id=7300000000000000001"
{
  "comments": [
    {
      "id": "7300000000000000009",
      "text": "ikr!",
      "create_time": 1699000300,
      "likes": 12,
      "reply_count": 0,
      "username": "replyguy",
      "nickname": "Reply Guy",
      "sec_uid": "MS4wLjABAAAA..."
    }
  ],
  "total": 14,
  "has_more": false,
  "next_cursor": 14
}
GET /api/comments/translate

Fetch the top comments on a video and batch-translate them. Each comment gains a text_translated field; the original text is preserved.

Query: url or id (+ optional author), target (language code, default en), limit (1–40, default 20).

curl -s "$BASE/api/comments/translate?id=7300000000000000000&target=en&limit=20"
{
  "target": "en",
  "count": 2,
  "comments": [
    {
      "id": "7300000000000000001",
      "text": "素敵な動画ですね",
      "text_translated": "What a lovely video",
      "create_time": 1699000100,
      "likes": 1200,
      "reply_count": 14,
      "username": "somefan",
      "nickname": "Some Fan",
      "sec_uid": "MS4wLjABAAAA...",
      "language": "ja",
      "translatable": true,
      "avatar": "https://p16-sign.tiktokcdn-us.com/..."
    },
    {
      "id": "7300000000000000002",
      "text": "j'adore !",
      "text_translated": "I love it!",
      "create_time": 1699000200,
      "likes": 340,
      "reply_count": 2,
      "username": "anotherfan",
      "nickname": "Another Fan",
      "sec_uid": "MS4wLjABAAAA...",
      "language": "fr",
      "translatable": true,
      "avatar": "https://p16-sign.tiktokcdn-us.com/..."
    }
  ]
}

Discovery

GET /api/hashtag/{tag}

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
}
GET /api/hashtag/{name}/videos new

A page of top videos for a hashtag/challenge, with derived metrics.

Path: name — with or without a leading #. Query: cursor (int, default 0), count (1–50, default 20).

curl -s "$BASE/api/hashtag/fyp/videos?cursor=0&count=20"
{
  "query": "fyp",
  "videos": [
    {
      "id": "7300000000000000000",
      "desc": "wait for it… #fyp",
      "create_time": 1699000000,
      "duration": 18,
      "author": {
        "username": "viraluser",
        "nickname": "Viral User",
        "sec_uid": "MS4wLjABAAAA..."
      },
      "stats": {
        "plays": 3100000,
        "likes": 480000,
        "comments": 5200,
        "shares": 26000,
        "collects": 14000,
        "reposts": 900
      },
      "metrics": {
        "engagement_rate": 0.1649,
        "save_rate": 0.0045,
        "share_rate": 0.0084,
        "comment_rate": 0.0017,
        "like_rate": 0.1548,
        "like_per_1k": 154.8,
        "amplification": 0.0833,
        "est_watch_proxy": 2.9682
      }
    }
  ],
  "has_more": true,
  "next_cursor": 20
}
GET /api/sound/{music_id}

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
}
GET /api/discover

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" }
  ]
}
GET /api/douyin/challenge/{ch_id}

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
}
GET /api/douyin/profile/{sec_uid}

Douyin (抖音) creator profile by sec_uid, via the public iesdouyin web API.

Path: sec_uid — the Douyin security id.

curl -s "$BASE/api/douyin/profile/MS4wLjABAAAA..."
{
  "sec_uid": "MS4wLjABAAAA...",
  "username": "dy_user",
  "nickname": "抖音用户",
  "bio": "记录美好生活",
  "avatar": "https://p3-sign.douyinpic.com/...",
  "followers": 1234000,
  "following": 321,
  "likes": 8900000,
  "videos": 210,
  "verified": false
}
GET /api/live/{username}

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"
  }
}
GET /api/suggest

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"
  ]
}
GET /api/oembed

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/..."
}

Media, reports, topics & more

Added in the current release: direct media/audio, PDF+CSV reports, embeddable cards, Douyin topic videos and deep live rooms.

GET/api/video/media

Direct media URLs from the reflow endpoint: video play/download and the sound audio track.

Query: url or id.

curl -s "$BASE/api/video/media?id=7540092833810730270"
{
  "id": "7540092833810730270",
  "play_url": "https://v16-webapp.../video/tos/...",
  "download_url": "https://v16-webapp.../video/tos/...",
  "music": "original sound",
  "music_url": "https://...",
  "music_duration": 24,
  "subtitles": [{ "lang": "eng-US", "format": "webvtt", "url": "..." }]
}
GET/api/report/profile/{username}

Download a one-page report for a creator. format=csv (default) or format=pdf.

curl -s -o alice.pdf "$BASE/api/report/profile/alice?format=pdf"
GET/api/report/video

Download a report for a video (stats + engagement metrics).

curl -s -o video.csv "$BASE/api/report/video?url=https://www.tiktok.com/@user/video/123&format=csv"
GET/api/profile/{username}/card

oEmbed-style profile card for embedding. A ready-made widget also lives at /embed?user=username.

{ "type": "rich", "provider_name": "TikTokScope", "title": "@alice on TikTok", "thumbnail_url": "...", "html": "<blockquote…>" }
GET/api/douyin/topic-videos

Videos for a Douyin hot topic. Query: word (use a hot-search word), offset, count.

curl -s "$BASE/api/douyin/topic-videos?word=%E7%83%AD%E9%97%A8&count=12"
GET/api/live/room/{room_id}

Deep live-room info with FLV / HLS / RTMP pull URLs, viewer count and cover.

{ "room_id": "...", "is_live": true, "viewers": 1617, "streams": { "flv": {...}, "hls": "...m3u8", "rtmp": "rtmp://..." } }

Metrics

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.

MetricFormulaMeaningRange
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
Ratios use plays as the denominator when it is present. A missing or zero denominator yields null — never an invented number — while a missing numerator counts as zero. Rounded to 4 decimals.

Rate limits & caching

  • Response cache. Successful responses are cached in SQLite and served until their TTL expires — profile 600s, followers 300s, video/reflow 1800s, comments 600s, feed/sound/trending/discover 600s, suggestions 3600s.
  • Upstream pacing. At most 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.
  • Page sizes. Paged endpoints accept count from 1 to 50 (default 20). Exports accept limit up to 5000 (default 500).
  • Errors. Malformed input returns 400; an upstream scrape failure returns 502 with {"detail": "…"}. Empty upstream payloads usually mean throttling — back off and retry.
  • No API key. There is no authentication; rate limiting is applied at the upstream and through the built-in pacing above, so please cache and batch where you can.

Responsible use

  • This API exposes read-only public data. It does not authenticate or access private accounts.
  • Respect TikTok's and Douyin's Terms of Service and all applicable local law.
  • Do not use it for bulk surveillance, harassment, doxxing, or resale of personal data.
  • The built-in pacing exists to stay polite to the upstream service — do not bypass or parallelize it aggressively.
  • Prefer aggregated metrics and cached responses over repeatedly hammering individual profiles.