> ## 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 Insight → Persona Mapping

> Pull demographic breakdowns from Meta Insights API, identify top audience segments, and map or create Mavera personas calibrated to your real paid audience

### Scenario

Your Meta ad account accumulates rich demographic data — age, gender, location, device, interest categories — but it lives in aggregate dashboard charts. This job pulls breakdowns from the Insights API, identifies your strongest audience segments, maps them to existing Mavera personas where possible, and creates custom personas for unmapped segments. The result is a persona library that mirrors your actual paid audience.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Meta GET insights (age, gender, country)"] --> B[Identify top segments] --> C["GET /api/v1/personas"] --> D[Map existing] --> E["POST /api/v1/personas (unmapped)"] --> F[Complete persona library]
```

### Code

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

  META = os.environ["META_ACCESS_TOKEN"]
  ACCT = os.environ["META_AD_ACCOUNT_ID"]
  MV = os.environ["MAVERA_API_KEY"]
  GRAPH = "https://graph.facebook.com/v24.0"
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  # 1. Pull audience insights with demographic breakdowns
  insights = requests.get(
      f"{GRAPH}/{ACCT}/insights",
      params={
          "access_token": META,
          "fields": "impressions,clicks,spend,actions",
          "breakdowns": "age,gender",
          "date_preset": "last_30d",
          "limit": 100,
      },
  ).json().get("data", [])

  # 2. Score segments by conversion efficiency
  segments = []
  for row in insights:
      conversions = 0
      for action in row.get("actions", []):
          if action.get("action_type") in ("offsite_conversion", "lead", "purchase"):
              conversions += int(action.get("value", 0))

      segments.append({
          "age": row.get("age", "unknown"),
          "gender": row.get("gender", "unknown"),
          "impressions": int(row.get("impressions", 0)),
          "clicks": int(row.get("clicks", 0)),
          "spend": float(row.get("spend", 0)),
          "conversions": conversions,
          "ctr": int(row.get("clicks", 0)) / max(int(row.get("impressions", 1)), 1),
          "cpa": float(row.get("spend", 0)) / max(conversions, 1),
      })

  top_segments = sorted(segments, key=lambda s: s["conversions"], reverse=True)[:10]

  # 3. Pull location breakdown separately
  geo_insights = requests.get(
      f"{GRAPH}/{ACCT}/insights",
      params={
          "access_token": META,
          "fields": "impressions,clicks,spend",
          "breakdowns": "country",
          "date_preset": "last_30d",
          "limit": 20,
      },
  ).json().get("data", [])
  top_countries = sorted(geo_insights, key=lambda g: int(g.get("clicks", 0)), reverse=True)[:5]
  country_list = [g.get("country", "Unknown") for g in top_countries]

  # 4. Fetch existing Mavera personas
  existing = requests.get(f"{MB}/personas", headers=MH).json()
  existing_names = {p.get("name", "").lower(): p for p in (existing if isinstance(existing, list) else [])}

  # 5. Map or create personas
  created, mapped = [], []
  for seg in top_segments:
      name = f"Meta {seg['gender'].title()} {seg['age']}"
      search_key = name.lower()

      if search_key in existing_names:
          mapped.append({"name": name, "id": existing_names[search_key]["id"], "action": "mapped"})
          continue

      desc = (
          f"Meta Ads audience segment: {seg['gender']}, age {seg['age']}. "
          f"30-day stats: {seg['impressions']:,} impressions, {seg['clicks']:,} clicks, "
          f"CTR {seg['ctr']:.2%}, {seg['conversions']} conversions, CPA ${seg['cpa']:.2f}. "
          f"Top markets: {', '.join(country_list[:3])}."
      )

      r = requests.post(f"{MB}/personas", headers=MH, json={
          "name": name,
          "description": desc,
          "demographic": {
              "age_range": seg["age"],
              "gender": seg["gender"],
              "countries": country_list[:3],
          },
          "psychographic": {
              "conversion_propensity": "high" if seg["conversions"] > 10 else "medium",
              "engagement_level": "high" if seg["ctr"] > 0.02 else "moderate",
          },
      })
      r.raise_for_status()
      created.append({"name": name, "id": r.json()["id"], "action": "created"})
      time.sleep(0.3)

  print(f"Mapped: {len(mapped)} existing | Created: {len(created)} new")
  for p in mapped + created:
      print(f"  [{p['action'].upper()}] {p['name']} → {p['id']}")
  ```

  ```javascript JavaScript theme={"dark"}
  const META = process.env.META_ACCESS_TOKEN;
  const ACCT = process.env.META_AD_ACCOUNT_ID;
  const MV = process.env.MAVERA_API_KEY;
  const GRAPH = "https://graph.facebook.com/v24.0";
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  // 1. Pull audience insights
  const insights = await fetch(
    `${GRAPH}/${ACCT}/insights?access_token=${META}&fields=impressions,clicks,spend,actions&breakdowns=age,gender&date_preset=last_30d&limit=100`
  ).then(r => r.json()).then(d => d.data || []);

  // 2. Score segments
  const segments = insights.map(row => {
    const conversions = (row.actions || [])
      .filter(a => ["offsite_conversion", "lead", "purchase"].includes(a.action_type))
      .reduce((sum, a) => sum + parseInt(a.value || "0"), 0);
    const impressions = parseInt(row.impressions || "0");
    const clicks = parseInt(row.clicks || "0");
    const spend = parseFloat(row.spend || "0");
    return {
      age: row.age || "unknown", gender: row.gender || "unknown",
      impressions, clicks, spend, conversions,
      ctr: clicks / Math.max(impressions, 1),
      cpa: spend / Math.max(conversions, 1),
    };
  }).sort((a, b) => b.conversions - a.conversions).slice(0, 10);

  // 3. Location breakdown
  const geoInsights = await fetch(
    `${GRAPH}/${ACCT}/insights?access_token=${META}&fields=impressions,clicks,spend&breakdowns=country&date_preset=last_30d&limit=20`
  ).then(r => r.json()).then(d => d.data || []);
  const topCountries = geoInsights
    .sort((a, b) => parseInt(b.clicks || "0") - parseInt(a.clicks || "0"))
    .slice(0, 5).map(g => g.country || "Unknown");

  // 4. Existing personas
  const existing = await fetch(`${MB}/personas`, { headers: MH }).then(r => r.json());
  const existingNames = new Map(
    (Array.isArray(existing) ? existing : []).map(p => [(p.name || "").toLowerCase(), p])
  );

  // 5. Map or create
  const created = [], mapped = [];
  for (const seg of segments) {
    const name = `Meta ${seg.gender.charAt(0).toUpperCase() + seg.gender.slice(1)} ${seg.age}`;
    if (existingNames.has(name.toLowerCase())) {
      mapped.push({ name, id: existingNames.get(name.toLowerCase()).id, action: "mapped" });
      continue;
    }

    const res = await fetch(`${MB}/personas`, {
      method: "POST", headers: MH,
      body: JSON.stringify({
        name,
        description: `Meta Ads: ${seg.gender}, age ${seg.age}. ${seg.impressions.toLocaleString()} imp, CTR ${(seg.ctr * 100).toFixed(2)}%, ${seg.conversions} conversions, CPA $${seg.cpa.toFixed(2)}.`,
        demographic: { age_range: seg.age, gender: seg.gender, countries: topCountries.slice(0, 3) },
        psychographic: {
          conversion_propensity: seg.conversions > 10 ? "high" : "medium",
          engagement_level: seg.ctr > 0.02 ? "high" : "moderate",
        },
      }),
    }).then(r => r.json());
    created.push({ name, id: res.id, action: "created" });
    await new Promise(r => setTimeout(r, 300));
  }

  console.log(`Mapped: ${mapped.length} | Created: ${created.length}`);
  [...mapped, ...created].forEach(p => console.log(`  [${p.action.toUpperCase()}] ${p.name} → ${p.id}`));
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "mapped": 3,
  "created": 7,
  "personas": [
    { "action": "mapped", "name": "Meta Female 25-34", "id": "per_existing_1" },
    { "action": "created", "name": "Meta Male 35-44", "id": "per_meta_m35_2",
      "stats": { "impressions": 245000, "ctr": "3.2%", "conversions": 89, "cpa": "$12.40" } },
    { "action": "created", "name": "Meta Female 18-24", "id": "per_meta_f18_3",
      "stats": { "impressions": 180000, "ctr": "4.1%", "conversions": 67, "cpa": "$9.80" } }
  ],
  "top_countries": ["US", "UK", "CA", "AU", "DE"]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="Insights returns empty data">Insights require at least one active campaign in the date range. Use `date_preset=last_90d` for broader coverage. Verify the ad account has had spend.</Accordion>
  <Accordion title="Breakdowns not combinable">Some breakdowns can't be combined (e.g., `age,gender` works but `age,placement` may not). Check [Meta's breakdown matrix](https://developers.facebook.com/docs/marketing-api/insights/breakdowns).</Accordion>
  <Accordion title="Actions array varies">The `actions` field contains different action types per objective. Filter for your conversion action (e.g., `purchase`, `lead`, `offsite_conversion`).</Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Meta Ads Integration" icon="meta" href="/integrations/meta-ads">
    All Meta Ads jobs
  </Card>

  <Card title="Personas" icon="user" href="/features/personas">
    Creating and managing personas
  </Card>
</CardGroup>
