Scenario
Your NPS survey segments respondents into promoters (9-10), passives (7-8), and detractors (0-6). Each tier has different messaging needs — promoters want to deepen their relationship, passives need a nudge, detractors need recovery. This job pulls NPS scores with their qualitative comments, groups them by tier, then runs a separate Focus Group per tier asking: “Which message improves your experience?” The result is tier-specific messaging validated by personas that match each NPS segment’s actual mindset. Flow: Typeform NPS responses → Split by promoter/passive/detractor → Create persona per tier →POST /api/v1/focus-groups per tier with candidate messages → “Which message improves your experience?” → Tier-optimized messaging
Architecture
Code
import os, requests, time
TF = os.environ["TYPEFORM_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
TF_BASE = "https://api.typeform.com"
MB = "https://app.mavera.io/api/v1"
TF_H = {"Authorization": f"Bearer {TF}"}
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
FORM_ID = os.environ.get("NPS_FORM_ID", "your_nps_form_id")
# 1. Get form structure — find NPS field and comment field
form = requests.get(f"{TF_BASE}/forms/{FORM_ID}", headers=TF_H).json()
nps_field = None
comment_field = None
for f in form.get("fields", []):
if f.get("type") == "opinion_scale" or f.get("type") == "rating":
nps_field = f["id"]
elif f.get("type") in ("long_text", "short_text") and not comment_field:
comment_field = f["id"]
# 2. Pull responses
responses = []
params = {"page_size": 1000}
while True:
r = requests.get(f"{TF_BASE}/forms/{FORM_ID}/responses",
headers=TF_H, params=params)
if r.status_code == 429:
time.sleep(1); continue
r.raise_for_status()
data = r.json()
responses.extend(data.get("items", []))
if len(data.get("items", [])) < 1000: break
params["before"] = data["items"][-1]["token"]
time.sleep(0.6)
# 3. Split by NPS tier
tiers = {"promoter": [], "passive": [], "detractor": []}
for resp in responses:
score = None
comment = ""
for ans in resp.get("answers", []):
fid = ans.get("field", {}).get("id")
if fid == nps_field:
score = ans.get("number", ans.get("rating"))
elif fid == comment_field:
comment = ans.get("text", "")
if score is None:
continue
entry = {"score": score, "comment": comment}
if score >= 9:
tiers["promoter"].append(entry)
elif score >= 7:
tiers["passive"].append(entry)
else:
tiers["detractor"].append(entry)
for tier, entries in tiers.items():
avg = sum(e["score"] for e in entries) / max(len(entries), 1)
print(f"{tier.title()}: {len(entries)} responses (avg: {avg:.1f})")
# 4. Create persona per tier
TIER_PROFILES = {
"promoter": {
"desc": "NPS 9-10. Enthusiastic advocates. Want deeper engagement and referral programs.",
"mindset": "Loyal, engaged, willing to advocate",
},
"passive": {
"desc": "NPS 7-8. Satisfied but not enthusiastic. Could switch if competitor offers more.",
"mindset": "Content but uncommitted, comparing alternatives",
},
"detractor": {
"desc": "NPS 0-6. Frustrated or disappointed. At risk of churn and negative word-of-mouth.",
"mindset": "Frustrated, feeling unheard, considering alternatives",
},
}
MESSAGE_CANDIDATES = [
"We just launched our biggest update ever — 3 features your team has been asking for.",
"Your feedback shaped our roadmap. Here's what we built because of you.",
"We know we can do better. Here's our improvement plan for the next 90 days.",
"Unlock premium features at no extra cost — as a thank you for being with us.",
"Your peers are seeing 40% time savings. Let us help you get there too.",
]
persona_ids = {}
for tier, profile in TIER_PROFILES.items():
entries = tiers[tier]
comments = [e["comment"] for e in entries if e["comment"]][:10]
comment_summary = "; ".join(comments[:5]) if comments else "No comments"
p = requests.post(f"{MB}/personas", headers=MV_H, json={
"name": f"NPS {tier.title()}",
"description": (
f"{profile['desc']} Based on {len(entries)} responses. "
f"Sample feedback: {comment_summary[:300]}"
),
"psychographic": {"mindset": profile["mindset"], "nps_tier": tier},
})
p.raise_for_status()
persona_ids[tier] = p.json()["id"]
time.sleep(0.3)
# 5. Run Focus Group per tier
for tier, pid in persona_ids.items():
entries = tiers[tier]
comments = [e["comment"] for e in entries if e["comment"]][:5]
fg = requests.post(f"{MB}/focus-groups", headers=MV_H, json={
"name": f"NPS {tier.title()} — Message Testing",
"persona_ids": [pid],
"context": (
f"You are a {tier} (NPS {tiers[tier][0]['score'] if tiers[tier] else '?'}). "
f"Recent feedback from this tier: {'; '.join(comments[:3])}"
),
"questions": [
f"Read these 5 messages. Which one would most improve your experience?\n\n" +
"\n".join(f"{i+1}. {m}" for i, m in enumerate(MESSAGE_CANDIDATES)),
"What specifically about your chosen message resonates?",
"Which message would make things worse? Why?",
"Write the message YOU would want to receive right now.",
"What's the #1 thing that would change your NPS score?",
],
"responses_per_persona": 3,
}).json()
for _ in range(20):
time.sleep(5)
result = requests.get(f"{MB}/focus-groups/{fg['id']}",
headers=MV_H).json()
if result.get("status") == "completed":
break
print(f"\n{'='*50}")
print(f"NPS {tier.title()} Focus Group: {fg['id']}")
print(f"{'='*50}")
for resp in result.get("responses", []):
print(f"Q: {resp.get('question', '')[:60]}...")
print(f"A: {resp.get('answer', '')[:200]}\n")
const TF = process.env.TYPEFORM_TOKEN;
const MV = process.env.MAVERA_API_KEY;
const TF_BASE = "https://api.typeform.com";
const MB = "https://app.mavera.io/api/v1";
const tfH = { Authorization: `Bearer ${TF}` };
const mvH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const FORM_ID = process.env.NPS_FORM_ID || "your_nps_form_id";
// 1. Form structure
const form = await fetch(`${TF_BASE}/forms/${FORM_ID}`, { headers: tfH }).then(r => r.json());
let npsField = null, commentField = null;
for (const f of form.fields || []) {
if (["opinion_scale", "rating"].includes(f.type)) npsField = f.id;
else if (["long_text", "short_text"].includes(f.type) && !commentField) commentField = f.id;
}
// 2. Pull responses
const responses = [];
const params = new URLSearchParams({ page_size: "1000" });
while (true) {
let res = await fetch(`${TF_BASE}/forms/${FORM_ID}/responses?${params}`, { headers: tfH });
if (res.status === 429) { await new Promise(r => setTimeout(r, 1000)); continue; }
const data = await res.json();
responses.push(...(data.items || []));
if ((data.items || []).length < 1000) break;
params.set("before", data.items[data.items.length - 1].token);
await new Promise(r => setTimeout(r, 600));
}
// 3. Split tiers
const tiers = { promoter: [], passive: [], detractor: [] };
for (const resp of responses) {
let score = null, comment = "";
for (const ans of resp.answers || []) {
if (ans.field?.id === npsField) score = ans.number ?? ans.rating;
if (ans.field?.id === commentField) comment = ans.text || "";
}
if (score == null) continue;
const entry = { score, comment };
if (score >= 9) tiers.promoter.push(entry);
else if (score >= 7) tiers.passive.push(entry);
else tiers.detractor.push(entry);
}
const MESSAGES = [
"We just launched our biggest update ever — 3 features your team asked for.",
"Your feedback shaped our roadmap. Here's what we built because of you.",
"We know we can do better. Here's our improvement plan for the next 90 days.",
"Unlock premium features at no extra cost — as a thank you.",
"Your peers see 40% time savings. Let us help you get there too.",
];
const PROFILES = {
promoter: { desc: "NPS 9-10. Enthusiastic advocates.", mindset: "Loyal, willing to advocate" },
passive: { desc: "NPS 7-8. Satisfied but not loyal.", mindset: "Content but comparing alternatives" },
detractor: { desc: "NPS 0-6. Frustrated, at risk.", mindset: "Frustrated, considering switching" },
};
// 4. Create personas + focus groups
for (const [tier, profile] of Object.entries(PROFILES)) {
const entries = tiers[tier];
const comments = entries.filter(e => e.comment).map(e => e.comment).slice(0, 5);
const p = await fetch(`${MB}/personas`, { method: "POST", headers: mvH,
body: JSON.stringify({ name: `NPS ${tier.charAt(0).toUpperCase() + tier.slice(1)}`,
description: `${profile.desc} N=${entries.length}. Feedback: ${comments.slice(0, 3).join("; ").slice(0, 300)}`,
psychographic: { mindset: profile.mindset, nps_tier: tier } }),
}).then(r => r.json());
const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: mvH,
body: JSON.stringify({
name: `NPS ${tier.charAt(0).toUpperCase() + tier.slice(1)} — Messages`,
persona_ids: [p.id],
context: `You are a ${tier}. Recent feedback: ${comments.slice(0, 3).join("; ")}`,
questions: [
`Read these 5 messages. Which improves your experience most?\n${MESSAGES.map((m, i) => `${i + 1}. ${m}`).join("\n")}`,
"What about your chosen message resonates?",
"Which message would make things worse? Why?",
"Write the message YOU would want to receive.",
"What's the #1 thing that would change your NPS score?",
],
responses_per_persona: 3,
}),
}).then(r => r.json());
let result;
for (let i = 0; i < 20; i++) {
await new Promise(r => setTimeout(r, 5000));
result = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: mvH }).then(r => r.json());
if (result.status === "completed") break;
}
console.log(`\n${"=".repeat(50)}\nNPS ${tier.toUpperCase()}: ${fg.id}\n${"=".repeat(50)}`);
for (const resp of result.responses || [])
console.log(`Q: ${(resp.question || "").slice(0, 60)}...\nA: ${(resp.answer || "").slice(0, 200)}\n`);
}
Example Output
Promoter: 142 responses (avg: 9.4)
Passive: 98 responses (avg: 7.6)
Detractor: 67 responses (avg: 4.2)
==================================================
NPS PROMOTER Focus Group
==================================================
Q: Read these 5 messages. Which one would most improve your ex...
A: Message 2 — "Your feedback shaped our roadmap." This validates
that my input matters. I already love the product; what keeps me
engaged is feeling like a co-creator, not just a customer.
Q: Write the message YOU would want to receive right now.
A: "You're one of our top 50 power users. We're building a beta
program — want early access and a direct line to our PM?"
==================================================
NPS DETRACTOR Focus Group
==================================================
Q: Read these 5 messages. Which one would most improve your ex...
A: Message 3 — "We know we can do better." The others feel tone-deaf
when I'm already frustrated. Acknowledging the problem first is
the only credible starting point.
Q: Which message would make things worse? Why?
A: Message 1 — "Our biggest update ever." I don't care about new
features when the existing ones don't work reliably. Ship features
before you celebrate them.
Error Handling
NPS field detection
NPS field detection
The code looks for
opinion_scale or rating fields. If your NPS uses a different Typeform field type (e.g., number), adjust the detection logic.Tier-specific focus groups are sequential
Tier-specific focus groups are sequential
Running 3 focus groups sequentially takes 3-5 minutes. For parallel execution, use
asyncio (Python) or Promise.all (JavaScript) — but monitor Mavera rate limits.Small tier sizes
Small tier sizes
Detractor tiers often have fewer responses. Even 10 NPS responses per tier provide enough context for meaningful persona creation.