StoriesFly logo

Tracking9 min read

How to Get a TikTok Followers and Following List in Node.js

Page through a TikTok account's followers and following lists in Node.js, dedupe them, diff snapshots for new followers and unfollows, and handle errors.

StoriesFly TeamPublished
On this page
  1. Install and get a key
  2. One page of followers
  3. Walk the whole list
  4. Detect new followers and unfollows
  5. Hidden lists and private accounts
  6. Error handling
  7. What it costs
  8. Reposts
  9. The same thing from Claude or ChatGPT, no code
  10. FAQ

You want the list of accounts that follow a TikTok account, or the accounts it follows, as data you can loop over. The official TikTok API does not give you that for an arbitrary public account, so most people end up searching for "tiktok following list api" and finding nothing usable.

Updated October 2026.

This post shows a working approach in Node.js with the storiesfly npm client: install, page through a list, dedupe it, save a snapshot, and diff it on the next run to see who followed and who left. It also covers hidden lists, private accounts, error codes and what it costs.

Everything below was type-checked against storiesfly 1.1.0. It is ESM and needs Node 18 or newer (it uses the global fetch) and has no runtime dependencies.

Install and get a key

npm install storiesfly

Sign in with Telegram at storiesfly.com/developers/dashboard and create a key. A new account gets 100 free requests, so you can run everything here before paying. Keep the key on the server and read it from the environment:

export STORIESFLY_API_KEY="your_key"

You do not log in to TikTok and you do not handle cookies. The key is the only credential.

One page of followers

A list call returns one page. Here is the shape:

import { StoriesFly } from "storiesfly";

const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });

const page = await client.tiktok.user.followers({ username: "khaby.lame" });

console.log(page.users.length, page.has_more, page.next_cursor, page.hidden);
console.log(page.users[0]);

The output looks like this (values are illustrative):

{
  "user": { "username": "khaby.lame" },
  "users": [
    {
      "id": "6812345678901234567",
      "sec_uid": "MS4wLjABAAAA...",
      "username": "some.follower",
      "full_name": "Some Follower",
      "profile_pic_url": "https://...",
      "verified": false,
      "is_private": false,
      "followers": 120,
      "following": 310,
      "likes": 4500,
      "videos": 12
    }
  ],
  "total": null,
  "hidden": false,
  "next_cursor": "...",
  "has_more": true
}

A few things to know about the fields. Every number can be null when it is not exact, so do not treat null as zero. username can also be null in a list entry, which matters for dedupe. total is the size of the whole list when it is known, and it is often not.

Walk the whole list

client.paginate calls the method, yields the page, then calls it again with the previous next_cursor until has_more is false. Set maxPages unless you really mean to read everything, because each page is a billed request.

import { StoriesFly, type TikTokUserShort } from "storiesfly";

const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });

export async function fetchList(
  method: typeof client.tiktok.user.followers | typeof client.tiktok.user.following,
  username: string,
  maxPages: number,
): Promise<{ users: TikTokUserShort[]; hidden: boolean; complete: boolean }> {
  const byId = new Map<string, TikTokUserShort>();
  let hidden = false;
  let complete = true;

  for await (const page of client.paginate(method, { username }, { maxPages })) {
    if (page.hidden) {
      hidden = true;
      break;
    }
    for (const u of page.users) {
      const key = u.id ?? u.sec_uid ?? u.username;
      if (key) byId.set(key, u);
    }
    complete = !page.has_more;
  }

  return { users: [...byId.values()], hidden, complete };
}

const followers = await fetchList(client.tiktok.user.followers, "khaby.lame", 10);
console.log(followers.users.length, followers.complete ? "whole list" : "stopped at maxPages");

Two details in there are worth explaining.

Dedupe. Keep a Map keyed by id, falling back to sec_uid and then username. Lists are read while accounts follow and unfollow, so an entry can appear on two pages, and a plain array would count it twice.

complete. The loop ends either because the list ended (has_more is false) or because it hit maxPages. Those are different outcomes. If you diff a truncated list against a full one, every account past the cut looks like an unfollow. Track whether you got the whole list and refuse to diff when you did not.

The same function reads the other direction: pass client.tiktok.user.following instead of followers. Lists are paged and how deep they go varies by account, so check complete rather than assuming.

Detect new followers and unfollows

Save the ids from one run, load them on the next, and compare. A JSON file is enough to start with.

import { readFile, writeFile } from "node:fs/promises";
import { StoriesFly } from "storiesfly";

const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });
const username = "khaby.lame";
const file = `./followers-${username}.json`;

async function loadSeen(): Promise<Record<string, string> | null> {
  try {
    return JSON.parse(await readFile(file, "utf8")) as Record<string, string>;
  } catch {
    return null; // first run
  }
}

const current: Record<string, string> = {};
let complete = true;
for await (const page of client.paginate(client.tiktok.user.followers, { username }, { maxPages: 20 })) {
  if (page.hidden) throw new Error("This account hides its followers list");
  for (const u of page.users) {
    const key = u.id ?? u.sec_uid ?? u.username;
    if (key) current[key] = u.username ?? "";
  }
  complete = !page.has_more;
}

const previous = await loadSeen();
if (previous && complete) {
  const joined = Object.keys(current).filter((id) => !(id in previous));
  const left = Object.keys(previous).filter((id) => !(id in current));
  console.log(`new: ${joined.length}, gone: ${left.length}`);
  for (const id of joined) console.log("+", current[id] || id);
  for (const id of left) console.log("-", previous[id] || id);
}

if (complete) await writeFile(file, JSON.stringify(current));
else console.warn("List was cut at maxPages. Snapshot not saved.");

Output on the second run:

new: 2, gone: 1
+ some.follower
+ another.one
- someone.who.left

Some honest limits of this approach:

  • It sees who is in the list now compared with last time. It cannot tell you when someone followed, only that they were not there on the previous run.
  • Someone who follows and unfollows between two runs is invisible. Shorten the interval if that matters.
  • An account that renames itself keeps its id, which is why the snapshot is keyed by id and stores the username only for display.

Run it from cron, once a day for most accounts.

Hidden lists and private accounts

Some accounts hide their followers or following list. The call still succeeds and returns an empty page with hidden: true. Check that flag before you treat an empty list as "no followers". In the code above, the diff throws instead of saving an empty snapshot, because an empty snapshot would make everyone look like a new follower the day the list becomes visible.

Private accounts are separate. To check one before you spend requests on its list, read the profile first, which is one request:

import { StoriesFly } from "storiesfly";

const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });

const profile = await client.tiktok.user.profile({ username: "khaby.lame" });
console.log(profile.followers, profile.following, profile.is_private);

if (profile.is_private) {
  console.log("Private account, skipping");
}

The profile gives followers and following, which is a good guide to the size of a list, so you can estimate the cost before you paginate. Either number can be null when TikTok does not give an exact one, so treat it as a hint and not as a promise. A call that reaches a private account can also come back as a private_account error, which is charged (see below).

Error handling

Every call that does not return data throws StoriesFlyError. Test code, not message.

import { StoriesFly, StoriesFlyError } from "storiesfly";

const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });

try {
  const page = await client.tiktok.user.followers({ username: "no_such_account_12345" });
  console.log(page.users.length);
} catch (err) {
  if (!(err instanceof StoriesFlyError)) throw err;

  switch (err.code) {
    case "not_found":
      console.error("No such account");
      break;
    case "private_account":
      console.error("Private account");
      break;
    case "credits_exhausted":
      console.error("Top up your balance");
      break;
    case "rate_limited":
      console.error(`Slow down, retry in ${err.retryAfterMs ?? "a few"} ms`);
      break;
    case "upstream_unavailable":
      console.error("Not available right now, retry shortly");
      break;
    default:
      console.error(err.status, err.code, err.requestId);
  }
  console.error("charged:", err.meta?.unitsCharged);
}

What matters in practice:

  • rate_limited is retried for you, twice by default, using the Retry-After the API sends. You only see it if the retries run out.
  • credits_exhausted is never retried. Another call would not change it.
  • upstream_unavailable and internal_error are refunded, so retrying them yourself is safe.
  • not_found and private_account are charged, because the call got an answer. A bad parameter (bad_request), a rate limit and an empty balance cost nothing.
  • Keep err.requestId in your logs. Support can find a call with it.

What it costs

Pricing is from $1.50 per 1,000 requests, prepaid. Every call is one request, and that includes each page of a list. So the cost of reading a list is the number of pages, not the number of followers.

requests = ceil(list size / page size)
cost     = requests x $0.0015

Page size follows TikTok and varies, so measure it with one real call before you plan a budget. As an illustration only: if a page held 50 accounts, a 10,000-follower list would be 200 requests, about $0.30. A daily snapshot of that account for a month is about $9. The profile call above usually gives you the list size (when it is not null), so you can do this arithmetic per account before you commit.

To see exactly what a call charged, pass { withMeta: true } as the last argument and read meta.unitsCharged and meta.balanceRemaining. With paginate, you get { data, meta } for every page.

Reposts

The videos an account reposted come through the same paging pattern:

import { StoriesFly } from "storiesfly";

const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });

for await (const page of client.paginate(client.tiktok.user.reposts, { username: "khaby.lame" }, { maxPages: 2 })) {
  for (const v of page.videos) {
    console.log(v.author.username, v.url, v.views, v.likes);
  }
}

Each item has the video's author, caption, cover, views and likes. comments and shares are null here, and taken_at is when the author posted the video, not when it was reposted. An empty list means the account has no reposts or hides them.

The same thing from Claude or ChatGPT, no code

If you only need an answer once, you can skip the script. The same data is available to AI assistants over MCP:

claude mcp add --transport http storiesfly https://storiesfly.com/mcp

ChatGPT and Claude connect to the same address and sign in with Telegram. Then ask in plain language, for example "list the first pages of followers of this TikTok account and tell me which ones are verified". Details are at storiesfly.com/developers/mcp. It is a good way to try a question before you turn it into a script.

FAQ

Do I need a TikTok login or cookies?

No. You send an API key and nothing else.

Can I get the full followers list of any account?

Lists are paged and depth varies by account. Check has_more to see whether you reached the end, and hidden to see whether the account hides the list.

What happens with a private account?

Check is_private on the profile first. A call that reaches a private account may answer private_account, and that answer is charged.

How do I know who unfollowed?

Compare two complete snapshots, as above. The API returns the current list, not a history. For more details, see our guide on view stories anonymously.

How do I stay inside a budget?

Always set maxPages, check the profile counts first, and skip the diff when complete is false.

Is the free tier real?

A new account gets 100 free requests, once. After that you prepay a balance.

Full method list and response types: storiesfly.com/developers/docs. The client's source and README are in the npm package.