> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Audience Demographic → Gen Z Focus Group

> Pull TikTok audience demographics, map age brackets to generational personas, and run Focus Group testing on ad concepts.

### Scenario

TikTok's audience skews young, but your ads target different generational segments. This job pulls audience demographic data from your campaigns, maps age brackets to Gen Z, Millennial, and Gen Alpha personas with platform-native behaviors, then runs a Focus Group testing your ad concepts before you commit production budget. You get generational feedback calibrated to TikTok's unique content culture.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["TikTok audience demographics"] --> B["Map to generational personas"]
    B --> C["Mavera POST /personas"]
    C --> D["POST /focus-groups with ad concepts"]
    D --> E["Generational feedback"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  TT = os.environ["TIKTOK_ACCESS_TOKEN"]
  ADV = os.environ["TIKTOK_ADVERTISER_ID"]
  MV = os.environ["MAVERA_API_KEY"]
  TT_BASE = "https://business-api.tiktok.com/open_api/v1.3"
  MV_BASE = "https://app.mavera.io/api/v1"
  TT_H = {"Access-Token": TT}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  # 1. Pull audience demographics
  demo = requests.post(f"{TT_BASE}/reports/integrated/get/",
      headers=TT_H,
      json={
          "advertiser_id": ADV,
          "report_type": "AUDIENCE",
          "data_level": "AUCTION_ADVERTISER",
          "dimensions": ["age", "gender"],
          "metrics": ["impressions", "clicks", "spend", "conversion"],
          "start_date": "2025-01-01", "end_date": "2025-12-31",
          "page_size": 50,
      }).json()

  if demo.get("code") != 0:
      raise SystemExit(f"TikTok: {demo.get('message')}")

  age_data = {}
  for row in demo.get("data", {}).get("list", []):
      age = row.get("dimensions", {}).get("age", "Unknown")
      m = row.get("metrics", {})
      existing = age_data.get(age, {"impressions": 0, "clicks": 0, "spend": 0})
      age_data[age] = {
          "impressions": existing["impressions"] + int(m.get("impressions", 0)),
          "clicks": existing["clicks"] + int(m.get("clicks", 0)),
          "spend": existing["spend"] + float(m.get("spend", 0)),
      }

  # 2. Map age brackets to generational personas
  GEN_MAP = {
      "AGE_13_17": {"gen": "Gen Alpha/Z", "desc": "13-17. Digital native. Discovers brands through TikTok, values authenticity and humor. Skeptical of polished ads. Influenced by creators, not brands."},
      "AGE_18_24": {"gen": "Gen Z", "desc": "18-24. Core TikTok demographic. Values transparency, social impact, and UGC. Short attention span. Responds to trends, not traditional marketing."},
      "AGE_25_34": {"gen": "Millennial", "desc": "25-34. Established spending power. Values convenience and brand story. Uses TikTok for discovery. Responds to aspirational + practical content."},
      "AGE_35_44": {"gen": "Elder Millennial", "desc": "35-44. Decision-makers and parents. Uses TikTok as entertainment. Responds to relatable, informational content. Less trend-driven."},
      "AGE_45_54": {"gen": "Gen X", "desc": "45-54. Fastest-growing TikTok segment. Values expertise and straightforward messaging. Skeptical but engaged when content is relevant."},
  }

  persona_ids = []
  for age_bracket, gen_info in GEN_MAP.items():
      stats = age_data.get(age_bracket, {})
      if stats.get("impressions", 0) < 100:
          continue
      ctr = (stats["clicks"] / stats["impressions"] * 100) if stats["impressions"] else 0

      p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
          "name": f"TikTok: {gen_info['gen']}",
          "description": f"{gen_info['desc']} Campaign data: {stats['impressions']:,} impressions, {ctr:.2f}% CTR, ${stats['spend']:,.0f} spend.",
          "demographic": {"age_range": age_bracket, "platform": "TikTok"},
          "psychographic": {"content_preferences": "short-form video", "generation": gen_info["gen"]},
      }).json()
      persona_ids.append({"id": p["id"], "gen": gen_info["gen"], "ctr": ctr})
      time.sleep(0.3)

  # 3. Run Focus Group with ad concepts
  AD_CONCEPTS = """
  CONCEPT A — "Day in My Life" UGC:
  Creator shows morning routine using your product. Lo-fi aesthetic, trending audio, text overlays.

  CONCEPT B — "3 Things You Didn't Know":
  Founder on camera, fast cuts, educational hook. "Stop scrolling if you..." opener.

  CONCEPT C — "Transition Reveal":
  Before/after transformation. Product reveal at the cut. Trending transition effect.

  CONCEPT D — "Stitch Response":
  Stitches a popular creator's question. Authentic, conversational, no script.
  """

  fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
      "name": "TikTok Ad Concept — Generational Testing",
      "persona_ids": [p["id"] for p in persona_ids],
      "questions": [
          f"Review these TikTok ad concepts:\n\n{AD_CONCEPTS}\n\nWhich concept would stop your scroll? Why?",
          "Which concept feels most authentic to TikTok? Which feels like an ad?",
          "Would you share any of these with friends? Which one and why?",
          "What trending format or audio would make you more likely to engage?",
      ],
      "responses_per_persona": 2,
  }).json()

  for _ in range(20):
      time.sleep(5)
      data = requests.get(f"{MV_BASE}/focus-groups/{fg['id']}", headers=MV_H).json()
      if data.get("status") == "completed": break

  for resp in data.get("responses", []):
      gen = next((p["gen"] for p in persona_ids if p["id"] == resp.get("persona_id")), "?")
      print(f"[{gen}] {resp.get('question','')[:60]}...")
      print(f"  → {resp.get('answer','')[:250]}\n")
  ```

  ```javascript JavaScript theme={"dark"}
  const TT = process.env.TIKTOK_ACCESS_TOKEN;
  const ADV = process.env.TIKTOK_ADVERTISER_ID;
  const MV = process.env.MAVERA_API_KEY;
  const TT_BASE = "https://business-api.tiktok.com/open_api/v1.3";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const TT_H = { "Access-Token": TT, "Content-Type": "application/json" };
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  // 1. Audience demographics
  const demo = await fetch(`${TT_BASE}/reports/integrated/get/`, {
    method: "POST", headers: TT_H,
    body: JSON.stringify({
      advertiser_id: ADV, report_type: "AUDIENCE", data_level: "AUCTION_ADVERTISER",
      dimensions: ["age", "gender"],
      metrics: ["impressions", "clicks", "spend", "conversion"],
      start_date: "2025-01-01", end_date: "2025-12-31", page_size: 50,
    }),
  }).then(r => r.json());
  if (demo.code !== 0) throw new Error(`TikTok: ${demo.message}`);

  const ageData = {};
  for (const row of demo.data?.list || []) {
    const age = row.dimensions?.age || "Unknown";
    const m = row.metrics || {};
    const e = ageData[age] || { impressions: 0, clicks: 0, spend: 0 };
    ageData[age] = {
      impressions: e.impressions + parseInt(m.impressions || "0", 10),
      clicks: e.clicks + parseInt(m.clicks || "0", 10),
      spend: e.spend + parseFloat(m.spend || "0"),
    };
  }

  // 2. Generational personas
  const GEN_MAP = {
    AGE_13_17: { gen: "Gen Alpha/Z", desc: "13-17. Digital native. Values authenticity, humor. Influenced by creators." },
    AGE_18_24: { gen: "Gen Z", desc: "18-24. Core TikTok demo. Values transparency, UGC, trends." },
    AGE_25_34: { gen: "Millennial", desc: "25-34. Spending power. Values convenience and story." },
    AGE_35_44: { gen: "Elder Millennial", desc: "35-44. Uses TikTok for entertainment. Practical content." },
    AGE_45_54: { gen: "Gen X", desc: "45-54. Fastest-growing segment. Values expertise." },
  };

  const personaIds = [];
  for (const [bracket, info] of Object.entries(GEN_MAP)) {
    const stats = ageData[bracket] || {};
    if ((stats.impressions || 0) < 100) continue;
    const ctr = stats.impressions ? ((stats.clicks / stats.impressions) * 100).toFixed(2) : "0";

    const p = await fetch(`${MV_BASE}/personas`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        name: `TikTok: ${info.gen}`,
        description: `${info.desc} Data: ${stats.impressions.toLocaleString()} imp, ${ctr}% CTR, $${stats.spend.toLocaleString()} spend.`,
        demographic: { age_range: bracket, platform: "TikTok" },
        psychographic: { content_preferences: "short-form video", generation: info.gen },
      }),
    }).then(r => r.json());
    personaIds.push({ id: p.id, gen: info.gen, ctr: parseFloat(ctr) });
    await new Promise(r => setTimeout(r, 300));
  }

  // 3. Focus Group
  const AD_CONCEPTS = `CONCEPT A — "Day in My Life" UGC: Creator + product, lo-fi, trending audio.
  CONCEPT B — "3 Things You Didn't Know": Founder on camera, fast cuts, educational hook.
  CONCEPT C — "Transition Reveal": Before/after with product reveal.
  CONCEPT D — "Stitch Response": Stitches popular creator question, conversational.`;

  const fg = await fetch(`${MV_BASE}/focus-groups`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "TikTok Ad Concept — Generational Testing",
      persona_ids: personaIds.map(p => p.id),
      questions: [
        `Review:\n\n${AD_CONCEPTS}\n\nWhich concept would stop your scroll? Why?`,
        "Which feels most authentic to TikTok? Which feels like an ad?",
        "Would you share any with friends? Which and why?",
        "What trending format or audio would make you engage?",
      ],
      responses_per_persona: 2,
    }),
  }).then(r => r.json());

  let data;
  for (let i = 0; i < 20; i++) {
    await new Promise(r => setTimeout(r, 5000));
    data = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then(r => r.json());
    if (data.status === "completed") break;
  }

  for (const resp of data.responses || []) {
    const gen = personaIds.find(p => p.id === resp.persona_id)?.gen || "?";
    console.log(`[${gen}] ${(resp.question || "").slice(0, 60)}...`);
    console.log(`  → ${(resp.answer || "").slice(0, 250)}\n`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
[Gen Z] Which concept would stop your scroll? Why?...
  → Concept D — Stitch Response. It feels like actual TikTok content, not an ad.
    The conversational format matches how I discover products. If the stitched
    creator is someone I follow, I'd definitely watch to the end.

[Millennial] Which feels most authentic to TikTok?...
  → Concept A feels most native. The "Day in My Life" format is what I watch
    already. Concept B feels like a YouTube ad someone repurposed for TikTok —
    I'd scroll past. Concept C depends entirely on the transition quality.

[Gen X] Would you share any with friends?...
  → I'd share Concept B if the information was genuinely surprising. I don't
    share UGC-style content but I do share "did you know" posts. Make the
    facts verifiable and I'll forward it.

[Gen Alpha/Z] What trending format would make you engage?...
  → Green screen format with a controversial take. Or the "POV" format where
    you role-play a situation. Also, use a trending sound — not the original,
    the remix version that's actually trending.
```

### Error Handling

<AccordionGroup>
  <Accordion title="Age bracket availability">TikTok may not report all age brackets if your campaigns exclude certain ages. Check your targeting settings if brackets return zero data.</Accordion>
  <Accordion title="Audience report permissions">Audience-level reports require the `Audience Management` permission scope. Without it, the endpoint returns `code: 40100`.</Accordion>
  <Accordion title="Small sample sizes">Age brackets with fewer than 100 impressions are filtered out to avoid unreliable CTR calculations. Extend the date range for newer campaigns.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="All TikTok jobs" icon="tiktok" href="/integrations/tiktok" />

  <Card title="Focus Groups" icon="users" href="/features/focus-groups" />
</CardGroup>
