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: G2GET /survey-responses → Group by reviewer role → Mavera POST /personas → POST /generations (role-targeted content)
Architecture
Code
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)
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));
}
Example Output
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
G2 auth format
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.Comment answer structure
Comment answer structure
Role normalization
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.
comment_answersfield structure varies by survey version. Some are flat strings, others are{text, id}objects. The code handles both formats.