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

# Engagement-Weighted Persona Refinement

> Pull HubSpot engagement data, score and tier contacts, then update Mavera personas so they evolve with your audience

## Scenario

Static personas go stale. Your HubSpot contacts have live engagement data — opens, clicks, page views — that reveal which segments are active. You pull engagement metrics, weight persona attributes by behavior, then update Mavera personas so they evolve with your audience.

## Architecture

```mermaid theme={"dark"}
flowchart LR
    A[HubSpot Contacts] --> B[Engagement scoring] --> C["Tier (high/medium/low)"] --> D[Extract traits] --> E["PATCH /personas/{id}"] --> F[Updated personas]
```

## Code

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

  HS = os.environ["HUBSPOT_ACCESS_TOKEN"]
  MV = os.environ["MAVERA_API_KEY"]
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  ENG_PROPS = ["hs_email_open_count", "hs_email_click_count",
               "hs_analytics_num_page_views", "num_conversion_events",
               "jobtitle", "industry", "lifecyclestage"]

  # 1. Pull engaged contacts
  contacts, after = [], None
  while len(contacts) < 300:
      payload = {
          "filterGroups": [{"filters": [{"propertyName": "hs_email_open_count", "operator": "GT", "value": "0"}]}],
          "properties": ENG_PROPS,
          "sorts": [{"propertyName": "hs_email_open_count", "direction": "DESCENDING"}],
          "limit": 100,
      }
      if after: payload["after"] = after
      r = requests.post("https://api.hubapi.com/crm/v3/objects/contacts/search",
          headers={"Authorization": f"Bearer {HS}"}, json=payload)
      if r.status_code == 429: time.sleep(1); continue
      r.raise_for_status()
      data = r.json()
      contacts.extend(data.get("results", []))
      after = data.get("paging", {}).get("next", {}).get("after")
      if not after: break

  # 2. Score and tier
  def score(c):
      p = c.get("properties", {})
      return int(p.get("hs_email_open_count") or 0) + int(p.get("hs_email_click_count") or 0)*3 + int(p.get("hs_analytics_num_page_views") or 0)*2 + int(p.get("num_conversion_events") or 0)*10

  scored = sorted([(c, score(c)) for c in contacts], key=lambda x: -x[1])
  third = len(scored) // 3
  tiers = {"High": scored[:third], "Medium": scored[third:2*third], "Low": scored[2*third:]}

  def profile(tier):
      titles, inds = defaultdict(int), defaultdict(int)
      for c, _ in tier:
          p = c.get("properties", {})
          if p.get("jobtitle"): titles[p["jobtitle"]] += 1
          if p.get("industry"): inds[p["industry"]] += 1
      return {"n": len(tier), "avg": sum(s for _,s in tier)/max(len(tier),1),
          "titles": sorted(titles, key=titles.get, reverse=True)[:5],
          "industries": sorted(inds, key=inds.get, reverse=True)[:3]}

  # 3. Get existing personas
  existing = requests.get(f"{MB}/personas", headers=MH).json()
  hs_personas = {p["name"]: p for p in (existing if isinstance(existing, list) else []) if "HubSpot" in p.get("name","")}

  # 4. Update or create
  for tier_name, tier_data in tiers.items():
      prof = profile(tier_data)
      name = f"HubSpot {tier_name} Engagement"
      desc = f"{tier_name} engagement. Avg: {prof['avg']:.1f}. N={prof['n']}. Titles: {', '.join(prof['titles'][:3])}."
      payload = {"name": name, "description": desc, "demographic": {"job_titles": prof["titles"], "industries": prof["industries"]},
          "psychographic": {"engagement_level": tier_name.lower(), "avg_score": prof["avg"]}}
      if name in hs_personas:
          r = requests.patch(f"{MB}/personas/{hs_personas[name]['id']}", headers=MH, json=payload)
          r.raise_for_status()
          print(f"Updated: {name} ({hs_personas[name]['id']})")
      else:
          r = requests.post(f"{MB}/personas", headers=MH, json=payload)
          r.raise_for_status()
          print(f"Created: {name} ({r.json()['id']})")
      time.sleep(0.3)
  ```

  ```javascript JavaScript theme={"dark"}
  const HS = process.env.HUBSPOT_ACCESS_TOKEN;
  const MV = process.env.MAVERA_API_KEY;
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const ENG_PROPS = ["hs_email_open_count", "hs_email_click_count",
    "hs_analytics_num_page_views", "num_conversion_events", "jobtitle", "industry", "lifecyclestage"];

  // 1. Pull engaged contacts
  const contacts = [];
  let after = null;
  while (contacts.length < 300) {
    const payload = {
      filterGroups: [{ filters: [{ propertyName: "hs_email_open_count", operator: "GT", value: "0" }] }],
      properties: ENG_PROPS,
      sorts: [{ propertyName: "hs_email_open_count", direction: "DESCENDING" }],
      limit: 100,
    };
    if (after) payload.after = after;
    const res = await fetch("https://api.hubapi.com/crm/v3/objects/contacts/search", {
      method: "POST", headers: { Authorization: `Bearer ${HS}`, "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    if (res.status === 429) { await new Promise(r => setTimeout(r, 1000)); continue; }
    const data = await res.json();
    contacts.push(...(data.results || []));
    after = data.paging?.next?.after; if (!after) break;
  }

  // 2. Score + tier
  const score = (c) => { const p = c.properties || {};
    return parseInt(p.hs_email_open_count||"0") + parseInt(p.hs_email_click_count||"0")*3
      + parseInt(p.hs_analytics_num_page_views||"0")*2 + parseInt(p.num_conversion_events||"0")*10; };
  const scored = contacts.map(c => ({ c, s: score(c) })).sort((a,b) => b.s - a.s);
  const third = Math.floor(scored.length / 3);
  const tiers = { High: scored.slice(0, third), Medium: scored.slice(third, third*2), Low: scored.slice(third*2) };
  const profile = (tier) => {
    const titles = {}, inds = {};
    tier.forEach(({ c }) => { const p = c.properties || {};
      if (p.jobtitle) titles[p.jobtitle] = (titles[p.jobtitle]||0)+1;
      if (p.industry) inds[p.industry] = (inds[p.industry]||0)+1; });
    const sort = o => Object.keys(o).sort((a,b) => o[b]-o[a]);
    return { n: tier.length, avg: tier.reduce((s,t)=>s+t.s,0)/(tier.length||1),
      titles: sort(titles).slice(0,5), industries: sort(inds).slice(0,3) };
  };
  // 3. Existing personas
  const existing = await fetch(`${MB}/personas`, { headers: MH }).then(r => r.json());
  const hsPersonas = Object.fromEntries(
    (Array.isArray(existing) ? existing : []).filter(p => (p.name||"").includes("HubSpot")).map(p => [p.name, p])
  );

  // 4. Update or create
  for (const [tierName, tierData] of Object.entries(tiers)) {
    const prof = profile(tierData);
    const name = `HubSpot ${tierName} Engagement`;
    const payload = {
      name, description: `${tierName} engagement. Avg: ${prof.avg.toFixed(1)}. N=${prof.n}. Titles: ${prof.titles.slice(0,3).join(", ")}.`,
      demographic: { job_titles: prof.titles, industries: prof.industries },
      psychographic: { engagement_level: tierName.toLowerCase(), avg_score: prof.avg },
    };

    if (hsPersonas[name]) {
      await fetch(`${MB}/personas/${hsPersonas[name].id}`, { method: "PATCH", headers: MH, body: JSON.stringify(payload) });
      console.log(`Updated: ${name} (${hsPersonas[name].id})`);
    } else {
      const p = await fetch(`${MB}/personas`, { method: "POST", headers: MH, body: JSON.stringify(payload) }).then(r => r.json());
      console.log(`Created: ${name} (${p.id})`);
    }
    await new Promise(r => setTimeout(r, 300));
  }
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
HIGH (100 contacts, avg: 142.3) — VP Marketing, Head of Growth | Tech, SaaS
MEDIUM (100, avg: 38.7) — Marketing Manager, Content Strategist | Tech, Healthcare
LOW (100, avg: 5.2) — Analyst, Coordinator | Tech, Education

Updated: HubSpot High Engagement (per_eng_high_1)
Updated: HubSpot Medium Engagement (per_eng_med_2)
Created: HubSpot Low Engagement (per_eng_low_3)
```

## Error Handling

<AccordionGroup>
  <Accordion title="Missing engagement properties">`hs_email_open_count` requires Marketing Hub with email tracking. Verify with `GET /crm/v3/properties/contacts`.</Accordion>
  <Accordion title="PATCH not supported">If Mavera doesn't support PATCH, delete and recreate: `DELETE /api/v1/personas/{id}` then `POST /api/v1/personas`.</Accordion>
  <Accordion title="Score weighting">Default weights (opens: 1×, clicks: 3×, views: 2×, conversions: 10×) are starting points. Adjust per your business.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="HubSpot Integration" icon="arrow-left" href="/integrations/hubspot" />

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