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
Cohort user IDs
Cohort user IDs
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.
Rate limiting per user lookup
Rate limiting per user lookup
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.Focus Group polling duration
Focus Group polling duration
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