TikTok’s official Research API is limited to approved academic researchers, and the Display API only returns data for users who log in to your app. If you need public numbers for creators you don’t control, such as plays on a competitor’s video, a creator’s follower count, or what is trending under a hashtag, neither official route will give them to you.
This guide covers the TikTok endpoints in the GramScraper API. It uses the same key and credit balance as the Instagram endpoints. Every example is a plain HTTP GET, so it works in curl, Node.js, Python or a spreadsheet script.
The endpoints
| Endpoint | What it returns | Credits |
|---|---|---|
/v1/user/by/username | Profile: bio, verification, followers, following, total likes, video count | 1 |
/v1/media/by/id · /v1/media/by/url | One video: description, author, plays, likes, comments, shares, saves, hashtags, music | 1 |
/v1/media/comments/by/id | A page of comments on a video, cursor-paginated | 1 / page |
/v1/media/comment/replies/by/id | Replies to one comment (comment_id + media_id) | 1 |
/v1/hashtag/info | A hashtag’s ID, description, video count and view count | 1 |
/v1/hashtag/medias | Popular videos under a hashtag, by hashtag id, cursor-paginated | 1 / page |
/v2/user/medias/by/secUid | A creator’s videos, newest first, about 20 per page, with full stats | 1 / page |
/v1/user/playlists/by/username | A creator’s public playlists | 1 |
/v1/media/video/download/by/id · /v1/media/music/download/by/id | Download URLs for a video or its audio | 1 |
Base URL: https://gramscraper.com/api/tiktok. Authorise with Authorization: Bearer gs_your_key. A free account comes with 100 credits, and you don’t need a card.
Get a TikTok profile
curl -s -H "Authorization: Bearer gs_your_key" \
"https://gramscraper.com/api/tiktok/v1/user/by/username?username=khaby.lame"The response follows TikTok’s own web format, so the user and the stats come back as maps keyed by the user ID. It isn’t a flat object:
{
"users": {
"127905465618821121": {
"id": "127905465618821121",
"uniqueId": "khaby.lame",
"nickname": "Khabane lame",
"signature": "Se vuoi ridere sei nel posto giusto",
"verified": true,
"privateAccount": false,
"avatarLarger": "https://p16-sign-va.tiktokcdn.com/..."
}
},
"stats": {
"127905465618821121": {
"followerCount": 162400000,
"followingCount": 79,
"heart": 2600000000,
"heartCount": -1694967296,
"videoCount": 1290
}
}
}Read heart, not heartCount. heartCount is stored as a 32-bit integer. Once an account passes about 2.1 billion total likes, it overflows and turns negative, like the value above. heart holds the same total without overflowing. We found this against live data on our own free tools. Any TikTok dashboard that shows a negative like count for a big creator has this bug.
A small Node.js helper that flattens the profile:
const BASE = "https://gramscraper.com/api/tiktok";
const KEY = process.env.GRAMSCRAPER_API_KEY;
async function tiktok(path, params) {
const url = new URL(BASE + path);
for (const [k, v] of Object.entries(params)) if (v != null) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status} on ${path}`);
return res.json();
}
async function getProfile(username) {
const data = await tiktok("/v1/user/by/username", { username });
const user = Object.values(data.users || {})[0];
const stats = Object.values(data.stats || {})[0] || {};
if (!user) return null;
const likes = [stats.heart, stats.heartCount].find(n => typeof n === "number" && n >= 0) ?? null;
return {
username: user.uniqueId,
followers: stats.followerCount,
following: stats.followingCount,
likes,
videos: stats.videoCount,
verified: !!user.verified,
private: !!user.privateAccount
};
}
console.log(await getProfile("khaby.lame"));Get one video’s stats
You can pass the full video URL or its numeric ID. The ID is the long number after /video/ in the URL. Short share links (vm.tiktok.com/...) don’t include it, so send those to the by/url endpoint.
import os, requests
BASE = "https://gramscraper.com/api/tiktok"
HEADERS = {"Authorization": f"Bearer {os.environ['GRAMSCRAPER_API_KEY']}"}
r = requests.get(f"{BASE}/v1/media/by/url", headers=HEADERS,
params={"url": "https://www.tiktok.com/@creator/video/7680560012632018189"})
r.raise_for_status()
v = r.json()
s = v.get("stats", {})
print(v["id"], v.get("desc"))
print("plays", s.get("playCount"), "likes", s.get("diggCount"),
"comments", s.get("commentCount"), "shares", s.get("shareCount"),
"saves", s.get("collectCount"))
print("hashtags", [c["title"] for c in v.get("challenges", [])])| Field | What it is |
|---|---|
id | The video ID. Use it with the comments and download endpoints. |
desc | The caption, including hashtags as typed. |
createTime | Publish time as a Unix timestamp in seconds. Multiply by 1000 for a JavaScript Date. |
author.uniqueId | The creator’s handle. |
stats.playCount | Plays. This is the “views” number shown on the video. |
stats.diggCount | Likes. TikTok calls a like a “digg” internally. |
stats.commentCount / shareCount / collectCount | Comments, shares, and saves (“collects”). |
challenges[].title | The hashtags attached to the video. |
music.title / music.authorName / music.original | The sound, who made it, and whether it is the creator’s original audio. |
video.duration / video.cover | Length in seconds, and a cover image URL (signed, so it expires). |
Unlike Instagram, TikTok shows share and save counts on public videos, so you get them here too. That makes a fuller engagement rate possible: (likes + comments + shares + saves) / plays. Our free TikTok engagement rate calculator runs the same arithmetic if you want to sanity-check your numbers.
List a creator’s videos
To get everything a creator has posted, take secUid from the profile response (users.<id>.secUid) and page through /v2/user/medias/by/secUid. Each page returns about 20 videos, newest first, for 1 credit. Pass the returned max_cursor to get the next page while has_more is 1.
async function creatorVideos(username, maxPages = 3) {
const profile = await tiktok("/v1/user/by/username", { username });
const secUid = Object.values(profile.users || {})[0]?.secUid;
if (!secUid) return [];
const out = [];
let cursor = null;
for (let page = 0; page < maxPages; page++) {
const data = await tiktok("/v2/user/medias/by/secUid", { secUid, max_cursor: cursor });
for (const v of data.aweme_list || []) {
const s = v.statistics || {};
out.push({
id: v.aweme_id,
at: new Date(v.create_time * 1000).toISOString(),
plays: s.play_count, likes: s.digg_count, comments: s.comment_count,
shares: s.share_count, saves: s.collect_count,
pinned: !!v.is_top
});
}
if (!data.has_more) break;
cursor = data.max_cursor;
}
return out;
}This endpoint uses TikTok’s app format rather than the web format: videos are under aweme_list, the ID is aweme_id, stats are snake_case under statistics, and video.duration is in milliseconds. Pinned videos come first and are marked with is_top, so drop them if you want strictly chronological results. Saving the secUid per creator means later refreshes cost 1 credit per page with no profile lookup.
Read the comments
Comments come about 30 per page. Keep requesting while has_more is truthy, and pass the returned cursor back each time:
async function allComments(videoId, maxPages = 10) {
const out = [];
let cursor = null;
for (let page = 0; page < maxPages; page++) {
const data = await tiktok("/v1/media/comments/by/id", { id: videoId, cursor });
for (const c of data.comments || []) {
out.push({
user: c.user?.unique_id,
text: c.text,
likes: c.digg_count,
replies: c.reply_comment_total,
at: new Date(c.create_time * 1000).toISOString()
});
}
if (!data.has_more) break;
cursor = data.cursor;
}
return out;
}Each page costs one credit, so the maxPages cap is your budget. Comment objects use snake_case (digg_count, create_time) while video objects use camelCase (diggCount, createTime). That is how TikTok returns them, and it is an easy way to end up with undefined everywhere. To load the replies under a comment, call /v1/media/comment/replies/by/id with its comment_id and the video’s media_id.
What’s trending under a hashtag
Getting a hashtag feed takes two calls. First, resolve the hashtag name to an ID and read its totals. Then page through its videos by ID:
const info = await tiktok("/v1/hashtag/info", { hashtag: "booktok" });
const tag = info.challengeInfo.challenge;
// statsV2 has exact counts as strings; stats is rounded and often says 0 videos
const exact = info.challengeInfo.statsV2 || {};
console.log(tag.id, tag.title, "videos:", Number(exact.videoCount), "views:", Number(exact.viewCount));
const page = await tiktok("/v1/hashtag/medias", { id: tag.id });
for (const v of page.itemList || []) {
console.log(v.author?.uniqueId, v.stats?.playCount, v.desc?.slice(0, 60));
}
// next page: pass page.cursor while page.hasMore is truePrefer statsV2 over stats for hashtag totals. The older stats object is rounded and often reports zero videos for hashtags with millions. Store the hashtag ID once you have it. After that, each page of videos costs a single credit and you skip the lookup.
What it costs
Every TikTok endpoint costs 1 credit per call. Here are some typical jobs:
| Job | Calls | Credits |
|---|---|---|
| Check a creator’s profile | 1 profile | 1 |
| Stats for 50 video URLs from a campaign | 50 video lookups | 50 |
| First 300 comments on a video | about 10 comment pages | about 10 |
| Top 90 videos under a hashtag | 1 info + about 3 pages | about 4 |
Standard is $29 for 15,400 credits, about $1.88 per 1,000 calls. Business is $300 for 320,000, about $0.94 per 1,000. Both are one-time purchases, and the credits don’t expire. Each response’s _meta block reports credits_consumed and credits_remaining, so you can track spend from the payload. If TikTok rate-limits the request upstream, you aren’t charged. The TikTok rate limits guide covers how to back off.
What this API can’t do
- Public accounts only. Private accounts return the profile shell with
privateAccount: trueand no video access. The private-account guide covers how to check for this before spending credits. - No first-party analytics. Watch time, completion rate, audience demographics and traffic sources are only available to the account owner through TikTok’s own tools. No third-party API can get them.
- Media URLs expire. Cover, avatar and download URLs are signed. If you need the file, download it soon after the call.
- Counts are TikTok’s public counts. Large numbers are what TikTok serves at request time. They can lag the app by a few minutes and may be rounded for very large accounts.
If you would rather ask questions than write code, the same TikTok endpoints are available as tools in Claude and Cursor through our hosted MCP server. Full parameter references are in the API docs.
Frequently asked questions
Is there an official TikTok API for public data?
TikTok's Research API returns public data, but only to approved academic researchers in eligible regions. The Display API only returns data for users who authorise your app. For public data on creators you don't control, developers use a third-party API like this one.
Why is TikTok's heartCount negative?
heartCount is a 32-bit integer and overflows above about 2.1 billion likes. Read the heart field instead, which holds the same total without overflowing.
Can I get share and save counts for TikTok videos?
Yes. TikTok shows them publicly, and they come back as stats.shareCount and stats.collectCount on every video response.
Does the TikTok API use the same key as Instagram?
Yes. One GramScraper key and one credit balance cover both platforms. Instagram is under /api/instagram and TikTok is under /api/tiktok.
Get 100 free API credits
Look up a creator, pull a video’s stats and read its comments before you buy a pack. That takes about three credits.
Create a free account →