Skip to main content

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.

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")

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

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.
Running 3 focus groups sequentially takes 3-5 minutes. For parallel execution, use asyncio (Python) or Promise.all (JavaScript) — but monitor Mavera rate limits.
Detractor tiers often have fewer responses. Even 10 NPS responses per tier provide enough context for meaningful persona creation.