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

# Company Page Analytics → Persona Refinement

> Pull LinkedIn follower demographics, compare against existing Mavera personas, and create or update data-grounded persona profiles

## Scenario

Your Company Page has 15,000 followers, but your personas are based on assumptions from a strategy deck written two years ago. This job pulls real follower demographics — industry verticals, job functions, and seniority levels — from LinkedIn's organizational statistics API. It compares those distributions against your existing Mavera personas, then updates (or creates) Custom Personas with actual audience data. The result: personas that reflect who actually follows you, not who you wish followed you.

## Architecture

```mermaid theme={"dark"}
flowchart LR
    A["LinkedIn GET /organizationalEntityFollowerStatistics"] --> B["Parse industry/function/seniority"]
    B --> C["Mavera GET /personas (existing)"]
    C --> D["Compare distributions"]
    D --> E["POST or PATCH /personas"]
    E --> F["Updated persona set"]
```

## Code

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

  LI = os.environ["LINKEDIN_ACCESS_TOKEN"]
  MV = os.environ["MAVERA_API_KEY"]
  LI_BASE = "https://api.linkedin.com/rest"
  MV_BASE = "https://app.mavera.io/api/v1"
  LI_H = {"Authorization": f"Bearer {LI}", "LinkedIn-Version": "202401", "X-Restli-Protocol-Version": "2.0.0"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  ORG_URN = "urn:li:organization:12345678"

  # 1. Pull follower statistics
  r = requests.get(f"{LI_BASE}/organizationalEntityFollowerStatistics",
      headers=LI_H,
      params={
          "q": "organizationalEntity",
          "organizationalEntity": ORG_URN,
      })
  if r.status_code == 429:
      time.sleep(int(r.headers.get("Retry-After", 60)))
      r = requests.get(f"{LI_BASE}/organizationalEntityFollowerStatistics",
          headers=LI_H,
          params={"q": "organizationalEntity", "organizationalEntity": ORG_URN})
  r.raise_for_status()
  stats = r.json().get("elements", [{}])[0]

  # 2. Parse demographic breakdowns
  def parse_breakdown(data, key):
      result = {}
      for entry in data:
          name = entry.get(key, {}).get("localized", {}).get("en_US", "")
          if not name:
              name = entry.get(key, "Unknown")
          organic = entry.get("followerCounts", {}).get("organicFollowerCount", 0)
          paid = entry.get("followerCounts", {}).get("paidFollowerCount", 0)
          result[name] = organic + paid
      return dict(sorted(result.items(), key=lambda x: -x[1]))

  industries = parse_breakdown(stats.get("followerCountsByIndustry", []), "industry")
  functions = parse_breakdown(stats.get("followerCountsByFunction", []), "function")
  seniorities = parse_breakdown(stats.get("followerCountsBySeniority", []), "seniority")

  total = sum(industries.values()) or 1
  print(f"Total followers: {total}")
  print(f"Top industries: {list(industries.items())[:5]}")
  print(f"Top functions: {list(functions.items())[:5]}")
  print(f"Top seniorities: {list(seniorities.items())[:5]}")

  # 3. Fetch existing Mavera personas
  existing = requests.get(f"{MV_BASE}/personas", headers=MV_H).json()
  li_personas = {p["name"]: p for p in (existing if isinstance(existing, list) else [])
                 if "LinkedIn Follower" in p.get("name", "")}

  # 4. Build persona segments from top combinations
  top_industries = list(industries.keys())[:4]
  top_functions = list(functions.keys())[:3]
  top_seniorities = list(seniorities.keys())[:3]

  created, updated = [], []
  for industry in top_industries:
      ind_pct = round((industries[industry] / total) * 100, 1)
      if ind_pct < 3:
          continue

      top_func = top_functions[0] if top_functions else "General"
      top_sen = top_seniorities[0] if top_seniorities else "Senior"
      name = f"LinkedIn Follower: {industry}"
      desc = (
          f"Derived from Company Page follower analytics. "
          f"Industry: {industry} ({ind_pct}% of followers). "
          f"Top function: {top_func} ({round((functions.get(top_func, 0) / total) * 100, 1)}%). "
          f"Top seniority: {top_sen} ({round((seniorities.get(top_sen, 0) / total) * 100, 1)}%). "
          f"Total follower base: {total:,}."
      )
      payload = {
          "name": name,
          "description": desc,
          "demographic": {
              "industries": [industry],
              "job_titles": [top_func],
              "seniority": top_sen,
          },
          "psychographic": {
              "source": "linkedin_company_page_followers",
              "audience_share_pct": ind_pct,
          },
      }

      if name in li_personas:
          r = requests.patch(f"{MV_BASE}/personas/{li_personas[name]['id']}",
              headers=MV_H, json=payload)
          r.raise_for_status()
          updated.append({"name": name, "id": li_personas[name]["id"], "pct": ind_pct})
      else:
          r = requests.post(f"{MV_BASE}/personas", headers=MV_H, json=payload)
          r.raise_for_status()
          created.append({"name": name, "id": r.json()["id"], "pct": ind_pct})
      time.sleep(0.3)

  print(f"\nCreated {len(created)} | Updated {len(updated)} personas")
  for p in created + updated:
      action = "Created" if p in created else "Updated"
      print(f"  {action}: {p['name']} ({p['pct']}%) → {p['id']}")
  ```

  ```javascript JavaScript theme={"dark"}
  const LI = process.env.LINKEDIN_ACCESS_TOKEN;
  const MV = process.env.MAVERA_API_KEY;
  const LI_BASE = "https://api.linkedin.com/rest";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const LI_H = { Authorization: `Bearer ${LI}`, "LinkedIn-Version": "202401", "X-Restli-Protocol-Version": "2.0.0" };
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const ORG_URN = "urn:li:organization:12345678";

  // 1. Pull follower statistics
  let res = await fetch(
    `${LI_BASE}/organizationalEntityFollowerStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(ORG_URN)}`,
    { headers: LI_H }
  );
  if (res.status === 429) {
    await new Promise(r => setTimeout(r, parseInt(res.headers.get("Retry-After") || "60", 10) * 1000));
    res = await fetch(
      `${LI_BASE}/organizationalEntityFollowerStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(ORG_URN)}`,
      { headers: LI_H }
    );
  }
  if (!res.ok) throw new Error(`LinkedIn ${res.status}: ${await res.text()}`);
  const stats = (await res.json()).elements?.[0] || {};

  // 2. Parse breakdowns
  function parseBreakdown(data, key) {
    const result = {};
    for (const entry of data || []) {
      const name = entry[key]?.localized?.en_US || entry[key] || "Unknown";
      const organic = entry.followerCounts?.organicFollowerCount || 0;
      const paid = entry.followerCounts?.paidFollowerCount || 0;
      result[name] = organic + paid;
    }
    return Object.fromEntries(Object.entries(result).sort(([, a], [, b]) => b - a));
  }

  const industries = parseBreakdown(stats.followerCountsByIndustry, "industry");
  const functions = parseBreakdown(stats.followerCountsByFunction, "function");
  const seniorities = parseBreakdown(stats.followerCountsBySeniority, "seniority");
  const total = Object.values(industries).reduce((s, v) => s + v, 0) || 1;

  console.log(`Total followers: ${total}`);
  console.log(`Top industries:`, Object.entries(industries).slice(0, 5));

  // 3. Fetch existing personas
  const existing = await fetch(`${MV_BASE}/personas`, { headers: MV_H }).then(r => r.json());
  const liPersonas = Object.fromEntries(
    (Array.isArray(existing) ? existing : [])
      .filter(p => (p.name || "").includes("LinkedIn Follower"))
      .map(p => [p.name, p])
  );

  // 4. Build persona segments
  const topIndustries = Object.keys(industries).slice(0, 4);
  const topFunc = Object.keys(functions)[0] || "General";
  const topSen = Object.keys(seniorities)[0] || "Senior";

  const created = [], updated = [];
  for (const industry of topIndustries) {
    const indPct = parseFloat(((industries[industry] / total) * 100).toFixed(1));
    if (indPct < 3) continue;

    const name = `LinkedIn Follower: ${industry}`;
    const funcPct = ((functions[topFunc] || 0) / total * 100).toFixed(1);
    const senPct = ((seniorities[topSen] || 0) / total * 100).toFixed(1);
    const payload = {
      name,
      description: `Derived from Company Page analytics. Industry: ${industry} (${indPct}%). Top function: ${topFunc} (${funcPct}%). Top seniority: ${topSen} (${senPct}%). Base: ${total.toLocaleString()}.`,
      demographic: { industries: [industry], job_titles: [topFunc], seniority: topSen },
      psychographic: { source: "linkedin_company_page_followers", audience_share_pct: indPct },
    };

    if (liPersonas[name]) {
      await fetch(`${MV_BASE}/personas/${liPersonas[name].id}`, {
        method: "PATCH", headers: MV_H, body: JSON.stringify(payload),
      });
      updated.push({ name, id: liPersonas[name].id, pct: indPct });
    } else {
      const p = await fetch(`${MV_BASE}/personas`, {
        method: "POST", headers: MV_H, body: JSON.stringify(payload),
      }).then(r => r.json());
      created.push({ name, id: p.id, pct: indPct });
    }
    await new Promise(r => setTimeout(r, 300));
  }

  console.log(`\nCreated ${created.length} | Updated ${updated.length} personas`);
  [...created, ...updated].forEach(p => {
    const action = created.includes(p) ? "Created" : "Updated";
    console.log(`  ${action}: ${p.name} (${p.pct}%) → ${p.id}`);
  });
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
Total followers: 14,832
Top industries: [('Technology', 4218), ('Financial Services', 2107), ('Marketing & Advertising', 1890), ('Healthcare', 1260), ('Education', 947)]
Top functions: [('Marketing', 3415), ('Business Development', 2890), ('Engineering', 1740)]
Top seniorities: [('Senior', 4120), ('Manager', 3280), ('Director', 2950)]

Created 1 | Updated 3 personas
  Updated: LinkedIn Follower: Technology (28.4%) → per_li_tech_01
  Updated: LinkedIn Follower: Financial Services (14.2%) → per_li_fin_02
  Updated: LinkedIn Follower: Marketing & Advertising (12.7%) → per_li_mktg_03
  Created: LinkedIn Follower: Healthcare (8.5%) → per_li_hc_04
```

## Error Handling

<AccordionGroup>
  <Accordion title="r_organization_followers scope required">Follower statistics need the `r_organization_followers` scope, which requires the Community Management API product approval. Without it, the endpoint returns 403.</Accordion>
  <Accordion title="Organization URN format">The organization URN must be `urn:li:organization:{numericId}`. Find your numeric ID on the Company Page admin URL or via `GET /organizationAcls`.</Accordion>
  <Accordion title="Localized field names">Industry and function names are returned in a `localized` object. The code extracts `en_US` first, falling back to the raw value. Adjust for other locales.</Accordion>
  <Accordion title="Low follower counts">Pages with under 300 followers produce unreliable demographic distributions. Wait for a larger base before running persona refinement.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="LinkedIn Content Integration" icon="arrow-left" href="/integrations/linkedin-content" />

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