# 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.

- URL: https://storiesfly.com/blog/tiktok-followers-following-list-nodejs
- Published: 2026-10-29
- Topic: Tracking
- Publisher: StoriesFly (https://storiesfly.com)

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

```sh
npm install storiesfly
```

Sign in with Telegram at [storiesfly.com/developers/dashboard](https://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:

```sh
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:

```ts
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):

```json
{
  "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.

```ts
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.

```ts
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:

```ts
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`.

```ts
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:

```ts
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:

```sh
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](https://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.

### 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](https://storiesfly.com/developers/docs). The client's source and README are in the npm package.
