# TikTok analytics dashboard (/docs/recipes/tiktok-analytics-dashboard)



TikTok analytics dashboard [#tiktok-analytics-dashboard]

Build the data layer for a TikTok analytics dashboard — account KPIs at the top, a per-video performance table in the middle, and the current public comment feed of the top video at the bottom. Repeated comment requests may use the 5-minute endpoint cache; cache hits cost 0 credits, and `Cache-Control: no-cache` requests a billed refresh.

How do you get TikTok analytics data from an API? [#how-do-you-get-tiktok-analytics-data-from-an-api]

Three standard-tier calls cover a full dashboard refresh: `GET /v1/tiktok/profile` returns follower count plus pre-computed `engagement_rate` and `estimated_reach`, `GET /v1/tiktok/profile/videos` returns recent videos with views, likes, comments, and shares, and `GET /v1/tiktok/post/comments` returns the comment feed for any video URL.

The problem [#the-problem]

TikTok's official analytics only cover accounts you own, and the Research API is gated to academics. If you're building a creator tool, an agency report, or a brand dashboard over accounts you *don't* own, you need public-data endpoints — and you need engagement math that's consistent enough to chart over time.

The solution [#the-solution]

Three standard-tier endpoints (1 credit each):

* `GET /v1/tiktok/profile` — profile stats (params: `handle` or `user_id`) with `computed.engagement_rate` and `computed.estimated_reach`
* `GET /v1/tiktok/profile/videos` — recent videos (param: `handle`; optional `sort_by=latest|popular`, paginate with `max_cursor`)
* `GET /v1/tiktok/post/comments` — comments on a video (param: `url`; paginate with `cursor`)

Need audience demographics (age / gender split)? `GET /v1/tiktok/user/audience` is available at the advanced tier (5 credits).

```typescript
// recipe-tiktok-dashboard.ts
// One full dashboard refresh for a TikTok account you don't own.
// Run with: SOCIALCRAWL_KEY=sc_... npx tsx recipe-tiktok-dashboard.ts

const KEY = process.env.SOCIALCRAWL_KEY;
if (!KEY) throw new Error("Set SOCIALCRAWL_KEY");

const BASE = "https://www.socialcrawl.dev/v1";
const handle = "charlidamelio";

async function get(path: string, params: Record<string, string>) {
  const url = new URL(`${BASE}/${path}`);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  const res = await fetch(url, { headers: { "x-api-key": KEY! } });
  return (await res.json()) as {
    success: boolean;
    data?: Record<string, any>;
    error?: { message: string };
  };
}

// ── Panel 1: Account KPIs ──────────────────────────────────────────────────
const profile = await get("tiktok/profile", { handle });
if (!profile.success) throw new Error(profile.error?.message);

const kpis = {
  followers: profile.data!.author.followers,
  engagement_rate: profile.data!.computed.engagement_rate, // optional [0, 1] canonical metric
  estimated_reach: profile.data!.computed.estimated_reach,
};
console.log("account KPIs:", kpis);

// ── Panel 2: Per-video performance table ──────────────────────────────────
const videos = await get("tiktok/profile/videos", {
  handle,
  sort_by: "latest",
});

const rows = (videos.data?.items ?? []).map(
  (item: {
    post: {
      url: string;
      text: string | null;
      created_at: string | null;
      engagement: {
        views: number | null;
        likes: number | null;
        comments: number | null;
        shares: number | null;
      };
    };
  }) => ({
    caption: item.post.text?.slice(0, 40) ?? "",
    views: item.post.engagement.views,
    likes: item.post.engagement.likes,
    comments: item.post.engagement.comments,
    shares: item.post.engagement.shares,
    url: item.post.url,
  }),
);
console.table(rows.slice(0, 10));

// ── Panel 3: Comment feed of the current top video ─────────────────────────
const top = [...rows].sort((a, b) => (b.views ?? 0) - (a.views ?? 0))[0];
if (top) {
  const comments = await get("tiktok/post/comments", { url: top.url });
  for (const c of (comments.data?.items ?? []).slice(0, 5)) {
    console.log(`💬 ${c.comment?.text ?? c.text}`);
  }
}
```

Schedule this against a list of handles and write each refresh to a table keyed by `(handle, date)` — time-series charts of followers and engagement rate fall out for free.

What you get back [#what-you-get-back]

```jsonc
// Panel 1 — profile with computed fields:
{
  "account KPIs": {
    "followers": 155300000,
    "engagement_rate": 0.0731, // <-- pre-computed, clamped to [0, 1]
    "estimated_reach": 11352430, // <-- pre-computed
  },
}

// Panel 2 — console.table of the latest videos:
// ┌─────────┬──────────────────────────┬──────────┬─────────┬──────────┬────────┐
// │ (index) │         caption          │  views   │  likes  │ comments │ shares │
// ├─────────┼──────────────────────────┼──────────┼─────────┼──────────┼────────┤
// │    0    │ "new dance w/ @..."      │ 12400000 │ 2100000 │  18400   │ 41000  │
// │    1    │ "grwm for the show"      │  8100000 │ 1400000 │  12100   │ 22000  │
// └─────────┴──────────────────────────┴──────────┴─────────┴──────────┴────────┘
```

Credits cost [#credits-cost]

> **Cost per run:** 3 credits per dashboard refresh (profile + videos + one comment fetch, 1 credit each). Refreshing 50 accounts daily costs 4,500 credits/month. Adding audience demographics (`/v1/tiktok/user/audience`) costs 5 credits more per account.

Take it further [#take-it-further]

* See [Computed fields](/docs/computed-fields) for the exact `engagement_rate` formula and `[0, 1]` clamping rules — and [Pagination](/docs/pagination) to walk past the first page of videos or comments.
* Compare the account against its Instagram and YouTube with [Creator engagement scoring](/docs/recipes/creator-engagement-scoring) — same `computed` fields, same formula.
* Track a competitor's TikTok with this exact loop, or all their platforms with [Competitor tracking](/docs/recipes/competitor-tracking).
* Full endpoint list (search, hashtags, trending, transcripts, followers): [TikTok API](/platforms/tiktok). New here? [Quickstart](/docs/quickstart).
