DEV Community

mian po
mian po

Posted on

Build a TikTok Hashtag and Creator Research Pipeline in Node.js

A useful trend-research workflow rarely ends with a hashtag count. You usually
need to move from a hashtag to recent public videos, then to the creators behind
those videos—without silently accepting duplicate pages or anti-bot responses
as real data.

This tutorial builds that workflow with four read-only endpoints:

  • /v1/hashtag/info
  • /v1/hashtag/videos
  • /v1/users/info
  • /v1/videos

The examples use the TikTok Public Metadata API on RapidAPI. It has a free BASIC
plan with 150 requests per month, which is enough to run this tutorial and test
the response contract.

This is an independent public-web metadata API, not TikTok's official API.
Use public data lawfully and respect privacy, intellectual-property rights,
TikTok's applicable terms, and RapidAPI's terms.

1. Create a small API client

Store your RapidAPI key in an environment variable. Never commit it.

export RAPIDAPI_KEY="your-key"
Enter fullscreen mode Exit fullscreen mode
const BASE_URL =
  "https://tiktok-public-metadata-api.p.rapidapi.com";

const headers = {
  "X-RapidAPI-Key": process.env.RAPIDAPI_KEY,
  "X-RapidAPI-Host": "tiktok-public-metadata-api.p.rapidapi.com"
};

const sleep = (milliseconds) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

async function apiGet(path, parameters, attempt = 0) {
  const url = new URL(path, BASE_URL);

  for (const [name, value] of Object.entries(parameters)) {
    if (value !== undefined && value !== null) {
      url.searchParams.set(name, String(value));
    }
  }

  const response = await fetch(url, { headers });

  if ((response.status === 429 || response.status === 503) && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delay = Number.isFinite(retryAfter) && retryAfter >= 0
      ? retryAfter * 1000
      : 500 * 2 ** attempt + Math.random() * 250;

    await sleep(delay);
    return apiGet(path, parameters, attempt + 1);
  }

  const body = await response.json();

  if (!response.ok) {
    throw new Error(
      `${response.status} ${body?.data?.error?.code ?? "API_ERROR"} ` +
      `(request_id=${body?.request_id ?? "unknown"})`
    );
  }

  return body;
}
Enter fullscreen mode Exit fullscreen mode

TikTok can return HTTP 200 challenge or anti-bot payloads on public web routes.
The API validates the native status, payload shape, and requested resource
identity; an untrustworthy response becomes a retryable structured 503
instead of a fake empty success.

2. Read exact hashtag metadata

The leading # is optional:

const hashtag = await apiGet("/v1/hashtag/info", {
  tag: "music",
  raw: false
});

console.log({
  requestId: hashtag.request_id,
  title: hashtag.data?.title,
  videoCount: hashtag.data?.statistics?.video_count,
  viewCount: hashtag.data?.statistics?.view_count
});
Enter fullscreen mode Exit fullscreen mode

Keep request_id in your logs. It lets support trace one request without
logging your API key or a full native payload.

3. Fetch videos and preserve the returned cursor

Start with cursor=0. Do not increment, decode, edit, or reconstruct the
cursor returned by a list endpoint.

async function getHashtagPage(tag, cursor = 0, count = 10) {
  const page = await apiGet("/v1/hashtag/videos", {
    tag,
    cursor,
    count,
    raw: false
  });

  if (page.count !== page.data.length) {
    throw new Error("Unexpected page contract");
  }

  const ids = page.data.map((video) => video.id);
  if (new Set(ids).size !== ids.length) {
    throw new Error("Duplicate video IDs in one page");
  }

  return page;
}

const firstPage = await getHashtagPage("music", 0, 10);

console.log({
  count: firstPage.count,
  hasMore: firstPage.has_more,
  nextCursor: firstPage.cursor
});
Enter fullscreen mode Exit fullscreen mode

Continue only when has_more === true, copying the returned cursor unchanged:

const secondPage = firstPage.has_more
  ? await getHashtagPage("music", firstPage.cursor, 10)
  : null;
Enter fullscreen mode Exit fullscreen mode

The cursor is signed and bound to the original hashtag and page state. It can
also preserve unconsumed items when the API merged more than one native TikTok
window to fill a requested page.

4. Turn video results into creator rows

Normalize creator usernames from the returned videos, then fetch each public
profile once. Keep concurrency low so a research script remains friendly to
both the API and the upstream service.

const usernames = [
  ...new Set(
    firstPage.data
      .map((video) => video.author?.unique_id)
      .filter(Boolean)
  )
];

const creators = [];

for (const username of usernames.slice(0, 5)) {
  const profile = await apiGet("/v1/users/info", {
    username,
    raw: false
  });

  creators.push({
    username: profile.data?.unique_id,
    nickname: profile.data?.nickname,
    followers: profile.data?.statistics?.follower_count ?? null,
    videos: profile.data?.statistics?.video_count ?? null
  });
}

console.table(creators);
Enter fullscreen mode Exit fullscreen mode

Unknown values remain null. Do not convert them to false or 0 unless your
own application explicitly defines that meaning.

5. Add one creator's public video feed

Use the same cursor rule for creator videos:

if (creators[0]?.username) {
  const creatorVideos = await apiGet("/v1/videos", {
    username: creators[0].username,
    cursor: 0,
    count: 5,
    raw: false
  });

  console.log(
    creatorVideos.data.map((video) => ({
      id: video.id,
      description: video.description,
      views: video.statistics?.play_count ?? null
    }))
  );
}
Enter fullscreen mode Exit fullscreen mode

At this point the workflow can answer practical questions:

  • Which public videos are associated with a hashtag?
  • Which creators appear in those results?
  • What public audience and posting signals are available for those creators?
  • What should be collected on the next page without duplicating items?

Production notes

  • Retry 429 and temporary 503 responses with backoff and jitter.
  • Do not retry 400, 401, or 404 unchanged.
  • Cache normalized results for your own product when appropriate.
  • Request raw=true only when you need a native TikTok field that is not normalized; native payloads are larger and can change.
  • Preserve request_id for support, but never log the RapidAPI key.

The production monitor currently runs 37 customer-path probes through the
RapidAPI gateway, covering all 16 data endpoints, multiple creators and
hashtags, pagination, raw responses, known regressions, and empty final pages.

Try the workflow

The free BASIC plan includes 150 requests per month:

https://rapidapi.com/pomiandaitumm/api/tiktok-public-metadata-api?utm_source=devto&utm_medium=content&utm_campaign=tiktok_api_hashtag_research_202608

Full documentation, live status, limitations, and OpenAPI:

https://tiktok-public-metadata-api.pomiandaitumm.chatgpt.site

If you build creator analytics or social-listening tools, I would especially
like feedback on which normalized hashtag and creator fields save you the most
integration work.

Top comments (0)