Skip to main content

Scenario

You’ve identified two behavioral cohorts in Amplitude — users who onboarded within 24 hours versus those who took more than 7 days. These groups have radically different retention curves, but you don’t know why the experience differs. You map each cohort to Mavera personas and run a Focus Group exploring how fast-onboarders and slow-onboarders experience the product differently — what drives urgency, what causes delay, and what each group needs.

Architecture

Code

import os, requests, time
from collections import Counter

AMP_KEY = os.environ["AMPLITUDE_API_KEY"]
AMP_SECRET = os.environ["AMPLITUDE_SECRET_KEY"]
MV = os.environ["MAVERA_API_KEY"]
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
amp_auth = (AMP_KEY, AMP_SECRET)

FAST_IDS = os.environ.get("FAST_ONBOARD_IDS", "").split(",")
SLOW_IDS = os.environ.get("SLOW_ONBOARD_IDS", "").split(",")

def fetch_activity(user_id):
    r = requests.get(
        "https://amplitude.com/api/2/useractivity",
        auth=amp_auth,
        params={"user": user_id},
    )
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", 10)))
        return fetch_activity(user_id)
    if r.status_code != 200:
        return {"events": [], "properties": {}}
    data = r.json().get("userData", {})
    return {"events": data.get("events", []), "properties": data.get("userProperties", {})}

def profile_cohort(user_ids, label):
    events_all = Counter()
    platforms = Counter()
    countries = Counter()
    event_counts = []

    for uid in user_ids[:20]:
        data = fetch_activity(uid)
        user_events = [e.get("event_type", "") for e in data["events"] if not e.get("event_type", "").startswith("$")]
        events_all.update(user_events)
        event_counts.append(len(user_events))
        props = data["properties"]
        if props.get("platform"):
            platforms[props["platform"]] += 1
        if props.get("country"):
            countries[props["country"]] += 1
        time.sleep(1)

    return {
        "label": label,
        "n": len(user_ids),
        "sampled": min(len(user_ids), 20),
        "avg_events": sum(event_counts) / max(len(event_counts), 1),
        "top_events": events_all.most_common(8),
        "platforms": platforms.most_common(3),
        "countries": countries.most_common(5),
    }

fast_profile = profile_cohort(FAST_IDS, "Fast Onboarders (<24h)")
slow_profile = profile_cohort(SLOW_IDS, "Slow Onboarders (>7d)")

fast_persona = requests.post(f"{MB}/personas", headers=MH, json={
    "name": "Amplitude: Fast Onboarder (<24h)",
    "description": (
        f"Users who completed onboarding within 24 hours. "
        f"Avg events: {fast_profile['avg_events']:.0f}. "
        f"Top actions: {', '.join(e for e, _ in fast_profile['top_events'][:5])}. "
        f"Platforms: {', '.join(p for p, _ in fast_profile['platforms'])}."
    ),
    "psychographic": {
        "onboarding_speed": "fast",
        "avg_events": fast_profile["avg_events"],
        "top_actions": [e for e, _ in fast_profile["top_events"][:5]],
    },
}).json()
time.sleep(0.3)

slow_persona = requests.post(f"{MB}/personas", headers=MH, json={
    "name": "Amplitude: Slow Onboarder (>7d)",
    "description": (
        f"Users who took 7+ days to complete onboarding. "
        f"Avg events: {slow_profile['avg_events']:.0f}. "
        f"Top actions: {', '.join(e for e, _ in slow_profile['top_events'][:5])}. "
        f"Platforms: {', '.join(p for p, _ in slow_profile['platforms'])}."
    ),
    "psychographic": {
        "onboarding_speed": "slow",
        "avg_events": slow_profile["avg_events"],
        "top_actions": [e for e, _ in slow_profile["top_events"][:5]],
    },
}).json()

def format_profile(prof):
    events = ", ".join(f"{e} ({c})" for e, c in prof["top_events"][:6])
    return f"  N={prof['n']}, sampled={prof['sampled']}, avg events={prof['avg_events']:.0f}\n  Top: {events}"

context = f"""Two behavioral cohorts from Amplitude onboarding data:

FAST ONBOARDERS (completed onboarding in <24 hours):
{format_profile(fast_profile)}

SLOW ONBOARDERS (took >7 days to complete onboarding):
{format_profile(slow_profile)}

Fast onboarders retain at 68% (30d). Slow onboarders retain at 22% (30d)."""

fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
    "name": "Amplitude: Onboarding Speed Cohort Study",
    "persona_ids": [fast_persona["id"], slow_persona["id"]],
    "questions": [
        "Walk me through your first day with a new software tool. What makes you complete setup quickly versus putting it off?",
        "When you signed up, what was your immediate goal? Were you trying to solve a specific problem or just exploring?",
        "What would have made you complete onboarding faster? Be specific — was something confusing, unnecessary, or missing?",
        "If a tool required you to invite teammates during onboarding, would that speed you up or slow you down? Why?",
        "Rank what matters most in your first session: (1) Seeing sample data, (2) Completing a real task, (3) Customizing settings, (4) Reading documentation, (5) Watching a tutorial. Explain your #1.",
    ],
    "context": context,
    "responses_per_persona": 2,
}).json()

for _ in range(30):
    time.sleep(5)
    data = requests.get(f"{MB}/focus-groups/{fg['id']}", headers=MH).json()
    if data.get("status") == "completed":
        break

print(f"Focus Group: {data.get('id')}{data.get('status')}\n")
for resp in data.get("responses", []):
    print(f"[{resp.get('persona_id','?')}] {resp.get('question','')[:80]}")
    print(f"  → {resp.get('answer','')[:300]}\n")
const AMP_KEY = process.env.AMPLITUDE_API_KEY;
const AMP_SECRET = process.env.AMPLITUDE_SECRET_KEY;
const MV = process.env.MAVERA_API_KEY;
const MB = "https://app.mavera.io/api/v1";
const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const ampAuth = "Basic " + Buffer.from(`${AMP_KEY}:${AMP_SECRET}`).toString("base64");

const FAST_IDS = (process.env.FAST_ONBOARD_IDS || "user_101,user_102,user_103").split(",");
const SLOW_IDS = (process.env.SLOW_ONBOARD_IDS || "user_201,user_202,user_203").split(",");

async function fetchActivity(userId) {
  const res = await fetch(
    `https://amplitude.com/api/2/useractivity?user=${encodeURIComponent(userId)}`,
    { headers: { Authorization: ampAuth } }
  );
  if (res.status === 429) {
    await new Promise((r) => setTimeout(r, parseInt(res.headers.get("Retry-After") || "10") * 1000));
    return fetchActivity(userId);
  }
  if (!res.ok) return { events: [], properties: {} };
  const data = (await res.json()).userData || {};
  return { events: data.events || [], properties: data.userProperties || {} };
}

async function profileCohort(ids, label) {
  const eventsAll = {}, platforms = {}, eventCounts = [];
  for (const uid of ids.slice(0, 20)) {
    const data = await fetchActivity(uid);
    const userEvents = data.events.map((e) => e.event_type || "").filter((n) => !n.startsWith("$"));
    userEvents.forEach((e) => { eventsAll[e] = (eventsAll[e] || 0) + 1; });
    eventCounts.push(userEvents.length);
    if (data.properties.platform) platforms[data.properties.platform] = (platforms[data.properties.platform] || 0) + 1;
    await new Promise((r) => setTimeout(r, 1000));
  }
  const topN = (o, n) => Object.entries(o).sort(([, a], [, b]) => b - a).slice(0, n);
  return {
    label, n: ids.length, sampled: Math.min(ids.length, 20),
    avgEvents: eventCounts.reduce((s, c) => s + c, 0) / (eventCounts.length || 1),
    topEvents: topN(eventsAll, 8), platforms: topN(platforms, 3),
  };
}

const fastProf = await profileCohort(FAST_IDS, "Fast (<24h)");
const slowProf = await profileCohort(SLOW_IDS, "Slow (>7d)");

const fastPersona = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
  body: JSON.stringify({
    name: "Amplitude: Fast Onboarder (<24h)",
    description: `Completed onboarding <24h. Avg events: ${fastProf.avgEvents.toFixed(0)}. Top: ${fastProf.topEvents.slice(0, 5).map(([e]) => e).join(", ")}.`,
    psychographic: { onboarding_speed: "fast", avg_events: fastProf.avgEvents },
  }),
}).then((r) => r.json());
await new Promise((r) => setTimeout(r, 300));

const slowPersona = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
  body: JSON.stringify({
    name: "Amplitude: Slow Onboarder (>7d)",
    description: `Took 7+ days to onboard. Avg events: ${slowProf.avgEvents.toFixed(0)}. Top: ${slowProf.topEvents.slice(0, 5).map(([e]) => e).join(", ")}.`,
    psychographic: { onboarding_speed: "slow", avg_events: slowProf.avgEvents },
  }),
}).then((r) => r.json());

const context = `Cohorts:\nFAST (<24h): N=${fastProf.n}, avg events=${fastProf.avgEvents.toFixed(0)}, top: ${fastProf.topEvents.slice(0, 5).map(([e, c]) => `${e}(${c})`).join(", ")}\nSLOW (>7d): N=${slowProf.n}, avg events=${slowProf.avgEvents.toFixed(0)}, top: ${slowProf.topEvents.slice(0, 5).map(([e, c]) => `${e}(${c})`).join(", ")}\n\nFast retain 68% (30d). Slow retain 22% (30d).`;

const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: MH,
  body: JSON.stringify({
    name: "Amplitude: Onboarding Speed Study",
    persona_ids: [fastPersona.id, slowPersona.id],
    questions: [
      "Walk me through your first day with new software. What makes you complete setup quickly vs putting it off?",
      "When you signed up, what was your immediate goal — solving a problem or exploring?",
      "What would have made you finish onboarding faster? Something confusing, unnecessary, or missing?",
      "If a tool required teammate invites during onboarding, speed you up or slow you down?",
      "Rank first-session priorities: (1) Sample data, (2) Real task, (3) Settings, (4) Docs, (5) Tutorial. Explain #1.",
    ],
    context,
    responses_per_persona: 2,
  }),
}).then((r) => r.json());

let data;
for (let i = 0; i < 30; i++) {
  await new Promise((r) => setTimeout(r, 5000));
  data = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: MH }).then((r) => r.json());
  if (data.status === "completed") break;
}

console.log(`Focus Group: ${data.id}${data.status}\n`);
for (const resp of data.responses || []) {
  console.log(`[${resp.persona_id}] ${(resp.question || "").slice(0, 80)}`);
  console.log(`  → ${(resp.answer || "").slice(0, 300)}\n`);
}

Example Output

Focus Group: fg_amp_cohort_9k2a — completed

[Fast Onboarder] Walk me through your first day with new software
  → I have a problem to solve TODAY. I sign up, skip the welcome tour,
    go straight to the feature I need. If I can't do something useful in
    10 minutes, I'll try the next tool. Setup wizards that force me
    through 8 steps before I can do anything are the fastest way to
    lose me — let me jump to the thing I came for.

[Slow Onboarder] Walk me through your first day with new software
  → I usually sign up during a meeting or after reading a blog post,
    then forget about it for a few days. When I come back, I've lost
    context. I need the tool to remind me why I signed up and pick up
    where I left off, not start from scratch.

[Fast Onboarder] If a tool required teammate invites during onboarding
  → Speed up. If I can invite my team early, I look like the person
    who found the solution. But ONLY if the invite step takes <30
    seconds and I can add them by email without configuring permissions.

[Slow Onboarder] Rank first-session priorities
  → #1: Seeing sample data. I don't have my own data ready on day 1.
    If the product shows me what it looks like with real content, I
    can evaluate it without doing any work. Empty states kill momentum.

Error Handling

You must supply user IDs for each cohort. Export these from Amplitude cohorts (Amplitude → Cohorts → Export CSV) or query via the Behavioral Cohorts API. The User Activity endpoint does not accept cohort IDs directly.
Each /api/2/useractivity call counts against the 360/hour limit. For 20 users per cohort (40 total), this uses 40 of your 360 hourly queries. Add 1-second delays between calls.
2 personas × 5 questions × 2 responses = 20 total responses. Allow 90–150s for completion. The loop provides ~150s.

What’s Next

Amplitude Integration

Back to Amplitude integration overview

Pricing Focus Group

Validate willingness-to-pay with revenue-driven personas

Focus Groups API

Full reference for POST /api/v1/focus-groups

Personas API

Full reference for POST /api/v1/personas