# Audience demographics (/docs/recipes/audience-demographics)



Audience demographics [#audience-demographics]

Build the audience slide of an influencer-vetting or media-planning report: where a creator's audience lives, and the approximate age and gender mix of the people engaging with a specific video.

How do you get audience demographics with an API? [#how-do-you-get-audience-demographics-with-an-api]

Platforms only show viewer demographics (age, gender, location of the people who watched) to the account owner, inside TikTok Analytics, YouTube Studio, and Meta Insights. That data never reaches any public surface, so no third-party API can return it for arbitrary public videos. Vendors that advertise it are estimating. SocialCrawl gives you two building blocks that estimate it honestly: [`/v1/tiktok/user/audience`](/platforms/tiktok/user-audience) returns a TikTok creator's audience country breakdown natively, and [`/v1/utility/age-gender`](/platforms/utility/age-gender) turns a sample of a video's commenters into an age and gender estimate on any platform with a comments endpoint.

The problem [#the-problem]

You are shortlisting creators for a campaign that targets women aged 18 to 30 in the US and Mexico. Every creator's media kit claims their audience matches. The platforms will not tell you: viewer demographics are locked to the account owner's own dashboard, and screenshots of those dashboards are easy to fake and impossible to refresh.

The solution [#the-solution]

Two endpoints, composable with any comments endpoint:

* `GET /v1/tiktok/user/audience` (5 credits) — the creator-level answer. Returns `audienceLocations`: countries ranked by share of a sample of roughly 1,000 audience accounts, with counts and percentages.
* `GET /v1/utility/age-gender` (10 credits) — the per-video building block. Takes any social profile URL and returns an estimated age range and gender (with confidence) from the profile's avatar.
* `GET /v1/tiktok/post/comments`, [`/v1/youtube/video/comments`](/platforms/youtube/video-comments), [`/v1/facebook/post/comments`](/platforms/facebook/post-comments) (1 credit each) — the sampling frame: real accounts that engaged with the specific video.

Chain them: pull a video's commenters, run age-gender detection on each commenter's profile, aggregate. The result is a demographic estimate of the video's *active* audience.

```typescript
// recipe-audience-demographics.ts
// Audience geography for a creator + sampled age/gender for one of their videos.
// Run with: SOCIALCRAWL_KEY=sc_... npx tsx recipe-audience-demographics.ts

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

const BASE = "https://www.socialcrawl.dev/v1";
const SAMPLE_SIZE = 10;

async function get<T>(path: string, params: Record<string, string>): Promise<T> {
  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 T;
}

// 1. Which countries is the creator's audience in? (5 credits)
type AudienceRes = {
  data?: {
    audienceLocations: Array<{
      country: string;
      countryCode: string;
      count: number;
      percentage: string;
    }>;
  };
};
const audience = await get<AudienceRes>("tiktok/user/audience", {
  handle: "charlidamelio",
});
console.table(audience.data?.audienceLocations.slice(0, 5));

// 2. Who engages with this specific video? Sample its commenters. (1 credit)
type CommentsRes = {
  data?: { items: Array<{ author: { username: string | null } }> };
};
const comments = await get<CommentsRes>("tiktok/post/comments", {
  url: "https://www.tiktok.com/@charlidamelio/video/7667257618544495886",
});
const usernames = (comments.data?.items ?? [])
  .map((c) => c.author.username)
  .filter((u): u is string => Boolean(u))
  .slice(0, SAMPLE_SIZE);

// 3. Estimate age + gender per commenter from their profile avatar. (10 credits each)
type AgeGenderRes = {
  data?: {
    ageRange: { low: number; high: number };
    gender: string;
    confidence: { gender: number };
  };
};
const sampled = await Promise.all(
  usernames.map(async (username) => {
    const r = await get<AgeGenderRes>("utility/age-gender", {
      url: `https://www.tiktok.com/@${username}`,
    });
    return r.data
      ? { username, age: `${r.data.ageRange.low}-${r.data.ageRange.high}`, gender: r.data.gender }
      : null;
  }),
);
const rows = sampled.filter((r) => r !== null);
console.table(rows);

// 4. Aggregate the sample into a demographic estimate.
const genderShare = rows.reduce<Record<string, number>>((acc, r) => {
  acc[r.gender] = (acc[r.gender] ?? 0) + 1;
  return acc;
}, {});
console.log("Gender mix of sampled commenters:", genderShare);
```

For YouTube, swap step 2 for `youtube/video/comments` and build profile URLs as `https://www.youtube.com/@${username}`; for Facebook, use `facebook/post/comments`. The age-gender endpoint accepts profile URLs from any platform.

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

```jsonc
// Step 1 — audienceLocations (top 5 of ~60 countries):
// ┌─────────┬─────────────────┬─────────────┬───────┬────────────┐
// │ (index) │ country         │ countryCode │ count │ percentage │
// ├─────────┼─────────────────┼─────────────┼───────┼────────────┤
// │    0    │ "United States" │ "US"        │  185  │ "19.49%"   │
// │    1    │ "Mexico"        │ "MX"        │  156  │ "16.44%"   │
// │    2    │ "Pakistan"      │ "PK"        │  70   │ "7.38%"    │
// │    3    │ "Argentina"     │ "AR"        │  51   │ "5.37%"    │
// │    4    │ "Colombia"      │ "CO"        │  47   │ "4.95%"    │
// └─────────┴─────────────────┴─────────────┴───────┴────────────┘

// Step 3 — sampled commenters:
// ┌─────────┬──────────────────┬─────────┬──────────┐
// │ (index) │ username         │ age     │ gender   │
// ├─────────┼──────────────────┼─────────┼──────────┤
// │    0    │ "dandefleurette" │ "23-29" │ "Female" │
// │   ...   │                  │         │          │
// └─────────┴──────────────────┴─────────┴──────────┘
```

Know the limits [#know-the-limits]

* **Commenters are not viewers.** The sample describes the video's *active* audience, which skews toward engaged fans. Treat it as a directional signal, not a viewer census.
* **Avatar-based estimates.** Age and gender come from the profile picture. Accounts with logos, pets, or cartoon avatars return low-confidence or missing estimates; filter on `confidence.gender` if you need precision.
* **Sample percentages are directional.** `audienceLocations` is drawn from roughly 1,000 audience accounts. A 19.5% share means "about a fifth", not a two-decimal truth.

Credits cost [#credits-cost]

> **Cost per run:** 106 credits (5 for the creator geography + 1 for comments + 10 commenters × 10 credits). Widen the age/gender sample to 30 commenters for 306 credits per video.

Take it further [#take-it-further]

* Prefer one call per video? [`/v1/prism/video-intel`](/platforms/prism/video-intel) returns detail, stats, transcript, top comments, and a commenter sample for YouTube, TikTok, Rumble, and Instagram in a single metered request.
* Compare two TikTok creators' audiences directly with [`/v1/prism/audience-overlap`](/platforms/prism/audience-overlap): Jaccard similarity plus a shared-fan count.
* Pair with [Creator engagement scoring](/docs/recipes/creator-engagement-scoring) to vet the same shortlist on performance, not just demographics.
* Want what the audience *thinks*, not just who they are? [Sentiment analysis](/docs/recipes/sentiment-analysis) runs the same comment surfaces through an LLM.
