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

# Lead Gen Form Response → Persona Enrichment

> Pull LinkedIn Lead Gen Form responses, aggregate by title/industry/company size, and create enriched Mavera personas from real lead data.

### Scenario

LinkedIn Lead Gen Forms capture high-quality B2B leads with job title, company size, and industry — but this data sits in Campaign Manager unused. This job pulls form responses, aggregates them by title cluster, company size tier, and industry vertical, then feeds each segment into Mavera's persona discovery engine. The result is a set of Custom Personas grounded in who actually fills out your forms, not who you think your audience is.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["LinkedIn GET /leadFormResponses"] --> B["Aggregate by title/industry/company size"]
    B --> C["Mavera POST /personas (persona discovery)"]
    C --> D["Custom 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"}

  SPONSORED_ACCOUNT_ID = "508000001"

  # 1. Pull lead gen form responses (paginated)
  responses, start = [], 0
  while True:
      r = requests.get(f"{LI_BASE}/leadFormResponses",
          headers=LI_H,
          params={
              "q": "owner",
              "owner": f"urn:li:sponsoredAccount:{SPONSORED_ACCOUNT_ID}",
              "start": start, "count": 100,
          })
      if r.status_code == 429:
          time.sleep(int(r.headers.get("Retry-After", 60)))
          continue
      r.raise_for_status()
      data = r.json()
      elements = data.get("elements", [])
      responses.extend(elements)
      if len(elements) < 100:
          break
      start += 100
      time.sleep(0.5)

  # 2. Extract and aggregate fields
  segments = defaultdict(list)
  for resp in responses:
      answers = {a.get("fieldName", ""): a.get("fieldValue", "") for a in resp.get("answers", [])}
      title = answers.get("jobTitle", "Unknown")
      company_size = answers.get("companySize", "Unknown")
      industry = answers.get("industry", "Unknown")

      title_cluster = title.split(",")[0].strip()
      segments[(title_cluster, industry, company_size)].append(answers)

  # 3. Create personas from top segments
  SIZE_MAP = {
      "1-10": "Startup", "11-50": "Small Business", "51-200": "Mid-Market",
      "201-500": "Mid-Market", "501-1000": "Enterprise", "1001+": "Enterprise",
  }

  created = []
  for (title, industry, size), leads in sorted(segments.items(), key=lambda x: -len(x[1]))[:8]:
      if len(leads) < 2:
          continue
      tier = SIZE_MAP.get(size, "Mid-Market")
      emails = [l.get("email", "") for l in leads if l.get("email")]

      p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
          "name": f"LinkedIn Lead: {title} ({tier})",
          "description": (
              f"Derived from {len(leads)} LinkedIn lead gen responses. "
              f"Role: {title}. Industry: {industry}. Company size: {size} ({tier}). "
              f"These leads actively filled out a form — high intent."
          ),
          "demographic": {
              "job_titles": [title],
              "industries": [industry],
              "company_size": tier,
          },
          "psychographic": {
              "intent_level": "high",
              "source": "linkedin_lead_gen_form",
          },
      }).json()
      created.append({"persona_id": p["id"], "title": title, "industry": industry, "tier": tier, "n": len(leads)})
      time.sleep(0.3)

  print(f"Created {len(created)} personas from {len(responses)} lead gen responses")
  for c in created:
      print(f"  {c['title']} | {c['industry']} | {c['tier']} | N={c['n']} | {c['persona_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 SPONSORED_ACCOUNT_ID = "508000001";

  // 1. Pull lead gen responses (paginated)
  const responses = [];
  let start = 0;
  while (true) {
    const res = await fetch(
      `${LI_BASE}/leadFormResponses?q=owner&owner=urn:li:sponsoredAccount:${SPONSORED_ACCOUNT_ID}&start=${start}&count=100`,
      { headers: LI_H }
    );
    if (res.status === 429) {
      await new Promise(r => setTimeout(r, parseInt(res.headers.get("Retry-After") || "60", 10) * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`LinkedIn ${res.status}`);
    const data = await res.json();
    const elements = data.elements || [];
    responses.push(...elements);
    if (elements.length < 100) break;
    start += 100;
    await new Promise(r => setTimeout(r, 500));
  }

  // 2. Aggregate by segment
  const segments = {};
  for (const resp of responses) {
    const answers = Object.fromEntries(
      (resp.answers || []).map(a => [a.fieldName || "", a.fieldValue || ""])
    );
    const title = (answers.jobTitle || "Unknown").split(",")[0].trim();
    const industry = answers.industry || "Unknown";
    const size = answers.companySize || "Unknown";
    const key = `${title}|${industry}|${size}`;
    (segments[key] ??= []).push(answers);
  }

  // 3. Create personas
  const SIZE_MAP = {
    "1-10": "Startup", "11-50": "Small Business", "51-200": "Mid-Market",
    "201-500": "Mid-Market", "501-1000": "Enterprise", "1001+": "Enterprise",
  };

  const created = [];
  const sorted = Object.entries(segments).sort(([, a], [, b]) => b.length - a.length).slice(0, 8);

  for (const [key, leads] of sorted) {
    if (leads.length < 2) continue;
    const [title, industry, size] = key.split("|");
    const tier = SIZE_MAP[size] || "Mid-Market";

    const p = await fetch(`${MV_BASE}/personas`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        name: `LinkedIn Lead: ${title} (${tier})`,
        description: `Derived from ${leads.length} LinkedIn lead gen responses. Role: ${title}. Industry: ${industry}. Size: ${size} (${tier}). High intent — form fills.`,
        demographic: { job_titles: [title], industries: [industry], company_size: tier },
        psychographic: { intent_level: "high", source: "linkedin_lead_gen_form" },
      }),
    }).then(r => r.json());
    created.push({ persona_id: p.id, title, industry, tier, n: leads.length });
    await new Promise(r => setTimeout(r, 300));
  }

  console.log(`Created ${created.length} personas from ${responses.length} responses`);
  created.forEach(c => console.log(`  ${c.title} | ${c.industry} | ${c.tier} | N=${c.n} | ${c.persona_id}`));
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "total_responses": 847,
  "personas_created": 6,
  "segments": [
    { "title": "VP of Marketing", "industry": "Technology", "tier": "Enterprise", "n": 142, "persona_id": "per_li_vpm_01" },
    { "title": "Head of Growth", "industry": "SaaS", "tier": "Mid-Market", "n": 98, "persona_id": "per_li_hog_02" },
    { "title": "Marketing Manager", "industry": "Financial Services", "tier": "Enterprise", "n": 87, "persona_id": "per_li_mm_03" },
    { "title": "Director of Demand Gen", "industry": "Technology", "tier": "Mid-Market", "n": 64, "persona_id": "per_li_ddg_04" },
    { "title": "CMO", "industry": "Healthcare", "tier": "Enterprise", "n": 41, "persona_id": "per_li_cmo_05" },
    { "title": "Content Strategist", "industry": "Technology", "tier": "Small Business", "n": 29, "persona_id": "per_li_cs_06" }
  ]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="r_ads_leadgen_automation scope required">Lead gen form responses need the `r_ads_leadgen_automation` scope, which requires LinkedIn partner approval. Without it, the endpoint returns 403.</Accordion>
  <Accordion title="Field name variations">LinkedIn lead gen form field names are defined per form. Common names: `jobTitle`, `companySize`, `industry`, `email`. Inspect your form configuration if fields return empty.</Accordion>
  <Accordion title="Deduplication">Leads can submit the same form multiple times. Deduplicate by email before aggregation for accurate persona counts.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="All LinkedIn Marketing jobs" icon="linkedin" href="/integrations/linkedin-marketing" />

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