Scenario
Not all reviewers are the same. 1-star reviewers have different concerns than 3-star reviewers. You segment reviews by star rating, create a persona for each tier (1-2 star detractors, 3-star neutrals, 4-5 star promoters), then run a Focus Group asking the pivotal question: “What would move you to 5 stars?” The answers reveal the exact interventions needed for each segment. Flow: TrustpilotGET /reviews → Group by star tier → Mavera POST /personas (per tier) → POST /focus-groups (“What moves you to 5?”)
Code
import os, requests, time
from collections import defaultdict
TP_KEY = os.environ["TRUSTPILOT_API_KEY"]
MV = os.environ["MAVERA_API_KEY"]
BU_ID = os.environ["TRUSTPILOT_BU_ID"]
TP_BASE = "https://api.trustpilot.com/v1"
MV_BASE = "https://app.mavera.io/api/v1"
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
TIERS = {
"Detractor (1-2★)": [1, 2],
"Neutral (3★)": [3],
"Promoter (4-5★)": [4, 5],
}
# 1. Pull reviews per tier
tier_reviews = {}
for tier_name, star_list in TIERS.items():
all_revs = []
for stars in star_list:
r = requests.get(f"{TP_BASE}/business-units/{BU_ID}/reviews",
params={"apikey": TP_KEY, "stars": stars, "perPage": 50,
"orderBy": "createdat.desc"})
r.raise_for_status()
all_revs.extend(r.json().get("reviews", []))
time.sleep(0.2)
tier_reviews[tier_name] = all_revs
# 2. Extract themes per tier
tier_themes = {}
for tier_name, revs in tier_reviews.items():
texts = [r.get("text", "")[:300] for r in revs if r.get("text")]
themes = defaultdict(int)
keywords = {"shipping": 0, "quality": 0, "support": 0, "price": 0, "easy": 0,
"fast": 0, "broken": 0, "refund": 0, "love": 0, "recommend": 0}
for t in texts:
lower = t.lower()
for kw in keywords:
if kw in lower:
themes[kw] += 1
tier_themes[tier_name] = {"texts": texts, "themes": dict(themes), "count": len(revs)}
# 3. Create personas per tier
persona_ids = []
for tier_name, data in tier_themes.items():
top_themes = sorted(data["themes"].items(), key=lambda x: -x[1])[:5]
theme_str = ", ".join(f"{k} ({v})" for k, v in top_themes)
sample_texts = data["texts"][:3]
p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
"name": f"TP: {tier_name}",
"description": (
f"Trustpilot reviewer in {tier_name} segment. N={data['count']}. "
f"Key themes: {theme_str}. "
f"Sample: \"{sample_texts[0][:100]}...\""
),
"psychographic": {
"satisfaction_tier": tier_name,
"top_themes": [k for k, _ in top_themes],
},
}).json()
persona_ids.append({"id": p["id"], "tier": tier_name, "n": data["count"]})
print(f"Persona: {p['id']} — {tier_name} ({data['count']} reviews)")
time.sleep(0.3)
# 4. Focus Group: "What moves you to 5 stars?"
fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
"name": "TrustScore Improvement Strategy",
"persona_ids": [p["id"] for p in persona_ids],
"questions": [
"What would it take for you to give this company a 5-star review?",
"What's the #1 thing this company does well that they should never change?",
"If you could fix one thing about your experience, what would it be?",
{"type": "likert", "text": "How likely are you to update your review if the company resolves your issue? (1-5)", "scale": 5},
"Describe your ideal post-purchase experience in 2 sentences.",
],
"responses_per_persona": 3,
}).json()
for _ in range(20):
time.sleep(5)
data = requests.get(f"{MV_BASE}/focus-groups/{fg['id']}", headers=MV_H).json()
if data.get("status") == "completed":
break
for resp in data.get("responses", []):
tier = next((p["tier"] for p in persona_ids if p["id"] == resp.get("persona_id")), "?")
print(f"\n[{tier}] {resp.get('question','')[:60]}")
print(f" → {resp.get('answer','')[:250]}")
const TP_KEY = process.env.TRUSTPILOT_API_KEY;
const MV = process.env.MAVERA_API_KEY;
const BU_ID = process.env.TRUSTPILOT_BU_ID;
const TP_BASE = "https://api.trustpilot.com/v1";
const MV_BASE = "https://app.mavera.io/api/v1";
const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const TIERS = { "Detractor (1-2★)": [1, 2], "Neutral (3★)": [3], "Promoter (4-5★)": [4, 5] };
// 1. Pull per tier
const tierReviews = {};
for (const [tierName, starList] of Object.entries(TIERS)) {
const allRevs = [];
for (const stars of starList) {
const res = await fetch(
`${TP_BASE}/business-units/${BU_ID}/reviews?apikey=${TP_KEY}&stars=${stars}&perPage=50&orderBy=createdat.desc`
);
allRevs.push(...((await res.json()).reviews || []));
await new Promise((r) => setTimeout(r, 200));
}
tierReviews[tierName] = allRevs;
}
// 2. Personas
const personaIds = [];
for (const [tierName, revs] of Object.entries(tierReviews)) {
const texts = revs.filter((r) => r.text).map((r) => r.text.slice(0, 300));
const p = await fetch(`${MV_BASE}/personas`, {
method: "POST", headers: MV_H,
body: JSON.stringify({
name: `TP: ${tierName}`,
description: `Trustpilot ${tierName}. N=${revs.length}. Sample: "${(texts[0] || "").slice(0, 100)}..."`,
psychographic: { satisfaction_tier: tierName },
}),
}).then((r) => r.json());
personaIds.push({ id: p.id, tier: tierName, n: revs.length });
await new Promise((r) => setTimeout(r, 300));
}
// 3. Focus Group
const fg = await fetch(`${MV_BASE}/focus-groups`, {
method: "POST", headers: MV_H,
body: JSON.stringify({
name: "TrustScore Improvement",
persona_ids: personaIds.map((p) => p.id),
questions: [
"What would it take for you to give 5 stars?",
"What's the #1 thing they do well?",
"If you could fix one thing, what?",
{ type: "likert", text: "How likely to update review if issue resolved? (1-5)", scale: 5 },
"Describe your ideal post-purchase experience.",
],
responses_per_persona: 3,
}),
}).then((r) => r.json());
let data;
for (let i = 0; i < 20; i++) {
await new Promise((r) => setTimeout(r, 5000));
data = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then((r) => r.json());
if (data.status === "completed") break;
}
for (const resp of data.responses || []) {
const tier = personaIds.find((p) => p.id === resp.persona_id)?.tier || "?";
console.log(`\n[${tier}] ${(resp.question || "").slice(0, 60)}`);
console.log(` → ${(resp.answer || "").slice(0, 250)}`);
}
Example Output
{
"focus_group": "fg_trust_tier_9k2",
"insights": [
{
"tier": "Detractor (1-2★)",
"to_5_stars": "Ship when you say you'll ship. That's it. I ordered with 2-day shipping, waited 9 days. Fix the logistics and I'd give you 5 stars — the product itself is great.",
"update_likelihood": 3.8
},
{
"tier": "Neutral (3★)",
"to_5_stars": "The product does what it says. But the website is clunky, checkout had errors, and I had to email support for a tracking number. Smooth that out.",
"update_likelihood": 4.1
},
{
"tier": "Promoter (4-5★)",
"to_5_stars": "Already gave 5. But proactive shipping updates instead of me checking the tracking page would be nice. And a loyalty program.",
"update_likelihood": 4.7
}
],
"strategy": "Shipping fixes would recover Detractors (highest ROI). UX polish converts Neutrals. Proactive communication retains Promoters."
}
Error Handling
Star-specific pagination
Star-specific pagination
Trustpilot requires separate API calls per star rating. The code loops through stars individually. For large volumes, paginate each star level independently.
Review text language
Review text language
Trustpilot reviews can be in any language. For non-English dominant businesses, filter by
language param or use Mave for translation.