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

# Review Sentiment → Persona-Specific Messaging

> Pull G2 reviews segmented by reviewer role, create Mavera personas, and generate role-targeted marketing content

## Scenario

G2 reviews include the reviewer's role, company size, and industry. A VP of Marketing experiences your product differently than a DevOps engineer. You pull reviews segmented by reviewer role, create Mavera personas grounded in actual reviewer profiles, then generate role-targeted marketing content that speaks to what each persona actually cares about — using their own words.

**Flow:** G2 `GET /survey-responses` → Group by reviewer role → Mavera `POST /personas` → `POST /generations` (role-targeted content)

## Architecture

```mermaid theme={"dark"}
flowchart LR
    A["G2 GET /survey-responses"] --> B["Group by reviewer role/industry"]
    B --> C["POST /api/v1/personas"]
    C --> D["POST /api/v1/generations"]
    D --> E["Per-role marketing content"]
```

## Code

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

  G2 = os.environ["G2_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  G2_BASE = "https://data.g2.com/api/v1"
  MV_BASE = "https://app.mavera.io/api/v1"
  G2_H = {"Authorization": f"Token token={G2}", "Content-Type": "application/vnd.api+json"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  # 1. Pull reviews
  reviews = []
  page = 1
  while len(reviews) < 300:
      r = requests.get(f"{G2_BASE}/survey-responses",
          headers=G2_H,
          params={"page[size]": 50, "page[number]": page})
      if r.status_code == 429:
          time.sleep(1)
          continue
      r.raise_for_status()
      data = r.json().get("data", [])
      if not data:
          break
      reviews.extend(data)
      page += 1
      time.sleep(0.1)

  # 2. Group by reviewer role
  role_groups = defaultdict(list)
  for rev in reviews:
      attrs = rev.get("attributes", {})
      role = attrs.get("title", "Unknown Role")
      industry = attrs.get("industry", "Unknown")
      star = attrs.get("star_rating", 0)
      love = ""
      hate = ""
      for ans in attrs.get("comment_answers", {}).values():
          text = ans if isinstance(ans, str) else ans.get("text", "")
          if "love" in str(ans).lower() or "best" in str(ans).lower():
              love = text[:300]
          elif "dislike" in str(ans).lower() or "hate" in str(ans).lower():
              hate = text[:300]

      role_bucket = role.split(",")[0].strip() if role else "Unknown"
      role_groups[role_bucket].append({
          "star": star, "industry": industry,
          "love": love, "hate": hate,
      })

  # 3. Create personas per role
  persona_map = []
  for role, revs in sorted(role_groups.items(), key=lambda x: -len(x[1]))[:6]:
      if len(revs) < 3:
          continue
      avg_star = sum(r["star"] for r in revs) / len(revs)
      industries = list({r["industry"] for r in revs if r["industry"] != "Unknown"})[:3]
      love_samples = [r["love"] for r in revs if r["love"]][:3]
      hate_samples = [r["hate"] for r in revs if r["hate"]][:3]

      p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
          "name": f"G2 Reviewer: {role}",
          "description": (
              f"G2 reviewer with role '{role}'. N={len(revs)}. Avg rating: {avg_star:.1f}/5. "
              f"Industries: {', '.join(industries)}. "
              f"What they love: {'; '.join(love_samples[:2])}. "
              f"What they dislike: {'; '.join(hate_samples[:2])}."
          ),
          "demographic": {"job_titles": [role], "industries": industries},
          "psychographic": {
              "product_sentiment": "positive" if avg_star >= 4 else "mixed" if avg_star >= 3 else "negative",
              "avg_rating": avg_star,
          },
      }).json()
      persona_map.append({"id": p["id"], "role": role, "n": len(revs), "avg": avg_star})
      print(f"Persona: {p['id']} — {role} ({len(revs)} reviews, avg {avg_star:.1f})")
      time.sleep(0.3)

  # 4. Generate role-targeted content
  for pm in persona_map:
      gen = requests.post(f"{MV_BASE}/generations", headers=MV_H, json={
          "persona_id": pm["id"],
          "prompt": (
              f"Generate a 150-word marketing paragraph targeting {pm['role']}s. "
              f"This segment gave us {pm['avg']:.1f}/5 on G2. "
              f"Use language that resonates with their specific concerns and value drivers. "
              f"Include a CTA appropriate for their role."
          ),
      }).json()
      content = gen.get("output", gen.get("content", gen.get("text", "")))
      print(f"\n--- Content for {pm['role']} ---")
      print(content[:400])
      time.sleep(0.5)
  ```

  ```javascript JavaScript theme={"dark"}
  const G2 = process.env.G2_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const G2_BASE = "https://data.g2.com/api/v1";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const G2_H = { Authorization: `Token token=${G2}`, "Content-Type": "application/vnd.api+json" };
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  // 1. Pull reviews
  const reviews = [];
  let page = 1;
  while (reviews.length < 300) {
    const res = await fetch(`${G2_BASE}/survey-responses?page[size]=50&page[number]=${page}`, { headers: G2_H });
    if (res.status === 429) { await new Promise((r) => setTimeout(r, 1000)); continue; }
    if (!res.ok) throw new Error(`G2 ${res.status}`);
    const data = (await res.json()).data || [];
    if (!data.length) break;
    reviews.push(...data);
    page++;
    await new Promise((r) => setTimeout(r, 100));
  }

  // 2. Group by role
  const roleGroups = {};
  for (const rev of reviews) {
    const attrs = rev.attributes || {};
    const role = (attrs.title || "Unknown").split(",")[0].trim();
    const star = attrs.star_rating || 0;
    let love = "", hate = "";
    for (const [key, val] of Object.entries(attrs.comment_answers || {})) {
      const text = typeof val === "string" ? val : val?.text || "";
      if (key.toLowerCase().includes("love") || key.toLowerCase().includes("best")) love = text.slice(0, 300);
      else if (key.toLowerCase().includes("dislike") || key.toLowerCase().includes("hate")) hate = text.slice(0, 300);
    }
    (roleGroups[role] ??= []).push({ star, industry: attrs.industry || "Unknown", love, hate });
  }

  // 3. Personas
  const personaMap = [];
  const topRoles = Object.entries(roleGroups).sort(([, a], [, b]) => b.length - a.length).slice(0, 6);
  for (const [role, revs] of topRoles) {
    if (revs.length < 3) continue;
    const avgStar = revs.reduce((s, r) => s + r.star, 0) / revs.length;
    const industries = [...new Set(revs.map((r) => r.industry).filter((i) => i !== "Unknown"))].slice(0, 3);
    const loves = revs.map((r) => r.love).filter(Boolean).slice(0, 3);
    const hates = revs.map((r) => r.hate).filter(Boolean).slice(0, 3);

    const p = await fetch(`${MV_BASE}/personas`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        name: `G2 Reviewer: ${role}`,
        description: `G2 reviewer '${role}'. N=${revs.length}. Avg: ${avgStar.toFixed(1)}/5. Love: ${loves.slice(0, 2).join("; ")}. Dislike: ${hates.slice(0, 2).join("; ")}.`,
        demographic: { job_titles: [role], industries },
        psychographic: { avg_rating: avgStar },
      }),
    }).then((r) => r.json());
    personaMap.push({ id: p.id, role, n: revs.length, avg: avgStar });
    await new Promise((r) => setTimeout(r, 300));
  }

  // 4. Generate content
  for (const pm of personaMap) {
    const gen = await fetch(`${MV_BASE}/generations`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        persona_id: pm.id,
        prompt: `Generate 150-word marketing targeting ${pm.role}s. G2 rating: ${pm.avg.toFixed(1)}/5. Use their language. Include role-appropriate CTA.`,
      }),
    }).then((r) => r.json());
    console.log(`\n--- ${pm.role} ---`);
    console.log((gen.output || gen.content || gen.text || "").slice(0, 400));
    await new Promise((r) => setTimeout(r, 500));
  }
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
Persona: per_g2_vp_1 — VP of Marketing (34 reviews, avg 4.6)
Persona: per_g2_dev_2 — Software Engineer (28 reviews, avg 3.9)
Persona: per_g2_pm_3 — Product Manager (22 reviews, avg 4.2)

--- Content for VP of Marketing ---
Your team doesn't need another dashboard — they need decisions. Our platform
turns raw customer data into messaging that converts, tested by synthetic
audiences before you spend a dollar. G2 reviewers in your role call it
"the missing link between data and creative." Start a free pilot and see
your first persona-validated campaign in 48 hours.

--- Content for Software Engineer ---
The API does what the docs say it does. REST endpoints, JSON responses,
sub-200ms latency. No SDK required — but we have one if you want it.
Check out our GitHub examples and have your first integration running
in under an hour. Engineers on G2 gave our API a 4.8/5 for documentation.
```

## Error Handling

<AccordionGroup>
  <Accordion title="G2 auth format">G2 uses `Token token={key}` (not Bearer). Using the wrong format returns `401`. Check the exact header format in your G2 API documentation.</Accordion>
  <Accordion title="Comment answer structure">The `comment_answers` field structure varies by survey version. Some are flat strings, others are `{text, id}` objects. The code handles both formats.</Accordion>
  <Accordion title="Role normalization">Reviewer titles can be verbose ("Vice President of Marketing & Communications"). The code splits on comma and takes the first part. For better grouping, use a role-normalization function.</Accordion>
</AccordionGroup>
