API tutorial · October 2026

TikTok Data API: Profiles, Videos, Comments and Hashtags as JSON

Pull public TikTok data with one API key: creator profiles, video stats, comment threads and hashtag feeds. Includes the real field names, what each call costs, and the one thing this API can’t do yet.

· 11 minute read

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

EndpointWhat it returnsCredits
/v1/user/by/usernameProfile: bio, verification, followers, following, total likes, video count1
/v1/media/by/id · /v1/media/by/urlOne video: description, author, plays, likes, comments, shares, saves, hashtags, music1
/v1/media/comments/by/idA page of comments on a video, cursor-paginated1 / page
/v1/media/comment/replies/by/idReplies to one comment (comment_id + media_id)1
/v1/hashtag/infoA hashtag’s ID, description, video count and view count1
/v1/hashtag/mediasPopular videos under a hashtag, by hashtag id, cursor-paginated1 / page
/v2/user/medias/by/secUidA creator’s videos, newest first, about 20 per page, with full stats1 / page
/v1/user/playlists/by/usernameA creator’s public playlists1
/v1/media/video/download/by/id · /v1/media/music/download/by/idDownload URLs for a video or its audio1

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 — profile by username
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:

Response — trimmed, values illustrative
{
  "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:

Node.js 18+ — flatten a 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.

Python 3 — video by URL
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", [])])
FieldWhat it is
idThe video ID. Use it with the comments and download endpoints.
descThe caption, including hashtags as typed.
createTimePublish time as a Unix timestamp in seconds. Multiply by 1000 for a JavaScript Date.
author.uniqueIdThe creator’s handle.
stats.playCountPlays. This is the “views” number shown on the video.
stats.diggCountLikes. TikTok calls a like a “digg” internally.
stats.commentCount / shareCount / collectCountComments, shares, and saves (“collects”).
challenges[].titleThe hashtags attached to the video.
music.title / music.authorName / music.originalThe sound, who made it, and whether it is the creator’s original audio.
video.duration / video.coverLength 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.

Node.js — a creator’s recent videos
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:

Node.js — walk a comment thread
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:

Node.js — hashtag totals, then its videos
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 true

Prefer 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:

JobCallsCredits
Check a creator’s profile1 profile1
Stats for 50 video URLs from a campaign50 video lookups50
First 300 comments on a videoabout 10 comment pagesabout 10
Top 90 videos under a hashtag1 info + about 3 pagesabout 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

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 →