Developer documentation

TikTokScope API

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.

Base URL https://tiktokscope.382972.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.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
Conventions. All endpoints are GET except the three mutation routes under /api/track. Errors are returned as {"detail": "…"} with HTTP 400 (bad input) or 502 (upstream scrape failure).

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

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>

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
}

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

Tracking

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.

POST /api/track

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

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
}
DELETE /api/track

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

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
}
POST /api/track/snapshot

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

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.