> ## 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.

# User Composition → Persona Calibration

> Pull Amplitude demographic composition data and calibrate Mavera personas to reflect your real user base

### Scenario

Your Amplitude project tracks user demographics — country, device, platform, language, and custom properties like plan type or role. You pull the composition endpoint to understand who your users actually are, compare that demographic distribution against your existing Mavera personas, and create or adjust personas so they reflect real product usage instead of assumptions. The result is a persona library calibrated to your actual user base.

### Architecture

```mermaid theme={"dark"}
flowchart LR
A["Amplitude GET /api/2/composition"] --> B["Compare distribution to existing personas"] --> C["POST /api/v1/personas"] --> D["Demographics-calibrated persona set"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time
  from collections import defaultdict

  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)

  r = requests.get(
      "https://amplitude.com/api/2/composition",
      auth=amp_auth,
      params={"start": "20260215", "end": "20260317"},
  )
  if r.status_code == 429:
      retry = int(r.headers.get("Retry-After", 60))
      time.sleep(retry)
      r = requests.get(
          "https://amplitude.com/api/2/composition",
          auth=amp_auth,
          params={"start": "20260215", "end": "20260317"},
      )
  r.raise_for_status()
  composition = r.json()

  series = composition.get("data", {}).get("series", [])
  xvals = composition.get("data", {}).get("xValues", [])

  segments = defaultdict(lambda: {"total": 0, "countries": defaultdict(int),
      "platforms": defaultdict(int), "devices": defaultdict(int)})

  for i, date_label in enumerate(xvals):
      for seg in series:
          name = seg.get("name", "all")
          val = seg.get("value", [0] * len(xvals))
          count = val[i] if i < len(val) else 0
          segments[name]["total"] += count

  existing = requests.get(f"{MB}/personas", headers=MH).json()
  existing_names = {p.get("name", ""): p for p in (existing if isinstance(existing, list) else [])}

  PERSONA_SEGMENTS = {
      "Mobile Power Users": {
          "filter": lambda s: "mobile" in s.lower() or "ios" in s.lower() or "android" in s.lower(),
          "description": "Users primarily accessing via mobile devices. Shorter sessions, gesture-driven navigation, on-the-go usage patterns.",
      },
      "Desktop Evaluators": {
          "filter": lambda s: "desktop" in s.lower() or "windows" in s.lower() or "mac" in s.lower(),
          "description": "Desktop-first users. Longer sessions, deeper feature exploration, likely evaluating the product for team adoption.",
      },
      "International Users": {
          "filter": lambda s: any(c in s.lower() for c in ["uk", "de", "fr", "in", "br", "jp", "au"]),
          "description": "Non-US users who may need localized content, timezone-aware engagement, and region-specific messaging.",
      },
  }

  created = []
  for persona_name, config in PERSONA_SEGMENTS.items():
      matching = {k: v for k, v in segments.items() if config["filter"](k)}
      if not matching:
          continue

      total = sum(v["total"] for v in matching.values())
      segment_names = list(matching.keys())[:5]

      full_name = f"Amplitude: {persona_name}"
      payload = {
          "name": full_name,
          "description": (
              f"{config['description']} "
              f"Amplitude composition data (30d). Total: {total:,} users. "
              f"Segments: {', '.join(segment_names)}."
          ),
          "demographic": {
              "source": "amplitude_composition",
              "segments": segment_names,
              "total_users": total,
          },
      }

      if full_name in existing_names:
          pid = existing_names[full_name]["id"]
          requests.patch(f"{MB}/personas/{pid}", headers=MH, json=payload).raise_for_status()
          created.append({"name": full_name, "id": pid, "action": "updated", "users": total})
          print(f"  Updated: {full_name} ({pid}) — {total:,} users")
      else:
          p = requests.post(f"{MB}/personas", headers=MH, json=payload).json()
          created.append({"name": full_name, "id": p["id"], "action": "created", "users": total})
          print(f"  Created: {full_name} ({p['id']}) — {total:,} users")
      time.sleep(0.3)

  overall_total = sum(s["total"] for s in segments.values())
  print(f"\nCalibrated {len(created)} personas from {overall_total:,} total composition data points")
  ```

  ```javascript JavaScript theme={"dark"}
  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");

  let res = await fetch(
    "https://amplitude.com/api/2/composition?start=20260215&end=20260317",
    { headers: { Authorization: ampAuth } }
  );
  if (res.status === 429) {
    const retry = parseInt(res.headers.get("Retry-After") || "60") * 1000;
    await new Promise((r) => setTimeout(r, retry));
    res = await fetch(
      "https://amplitude.com/api/2/composition?start=20260215&end=20260317",
      { headers: { Authorization: ampAuth } }
    );
  }
  const composition = await res.json();

  const series = composition.data?.series || [];
  const xvals = composition.data?.xValues || [];

  const segments = {};
  for (let i = 0; i < xvals.length; i++) {
    for (const seg of series) {
      const name = seg.name || "all";
      const count = (seg.value || [])[i] || 0;
      segments[name] ??= { total: 0 };
      segments[name].total += count;
    }
  }

  const existing = await fetch(`${MB}/personas`, { headers: MH }).then((r) => r.json());
  const existingNames = Object.fromEntries(
    (Array.isArray(existing) ? existing : []).map((p) => [p.name, p])
  );

  const configs = [
    { persona: "Mobile Power Users", test: (s) => /mobile|ios|android/i.test(s),
      desc: "Mobile-first users. Shorter sessions, gesture-driven, on-the-go." },
    { persona: "Desktop Evaluators", test: (s) => /desktop|windows|mac/i.test(s),
      desc: "Desktop-first. Longer sessions, deeper exploration, evaluating for teams." },
    { persona: "International Users", test: (s) => /uk|de|fr|in|br|jp|au/i.test(s),
      desc: "Non-US users needing localized content and timezone-aware engagement." },
  ];

  const created = [];
  for (const cfg of configs) {
    const matching = Object.entries(segments).filter(([k]) => cfg.test(k));
    if (!matching.length) continue;

    const total = matching.reduce((s, [, v]) => s + v.total, 0);
    const segNames = matching.slice(0, 5).map(([k]) => k);
    const fullName = `Amplitude: ${cfg.persona}`;

    const payload = {
      name: fullName,
      description: `${cfg.desc} Amplitude composition (30d). Total: ${total.toLocaleString()} users. Segments: ${segNames.join(", ")}.`,
      demographic: { source: "amplitude_composition", segments: segNames, total_users: total },
    };

    if (existingNames[fullName]) {
      const pid = existingNames[fullName].id;
      await fetch(`${MB}/personas/${pid}`, { method: "PATCH", headers: MH, body: JSON.stringify(payload) });
      created.push({ name: fullName, id: pid, action: "updated", users: total });
      console.log(`  Updated: ${fullName} (${pid}) — ${total.toLocaleString()} users`);
    } else {
      const p = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
        body: JSON.stringify(payload) }).then((r) => r.json());
      created.push({ name: fullName, id: p.id, action: "created", users: total });
      console.log(`  Created: ${fullName} (${p.id}) — ${total.toLocaleString()} users`);
    }
    await new Promise((r) => setTimeout(r, 300));
  }

  console.log(`\nCalibrated ${created.length} personas from composition data`);
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "composition_total": 24800,
  "personas": [
    { "name": "Amplitude: Mobile Power Users", "id": "per_amp_mob_1", "action": "created", "users": 9200 },
    { "name": "Amplitude: Desktop Evaluators", "id": "per_amp_desk_2", "action": "created", "users": 12400 },
    { "name": "Amplitude: International Users", "id": "per_amp_intl_3", "action": "updated", "users": 6800 }
  ],
  "sample_segments": ["iOS", "Android", "Windows", "Mac OS X", "United Kingdom", "Germany"]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="Composition API date format">Amplitude uses `YYYYMMDD` format (no dashes) for date parameters. Passing `2026-02-15` instead of `20260215` returns a 400 error.</Accordion>
  <Accordion title="Rate limit: 360 queries/hour">The 360/hour limit is shared across all Dashboard REST API endpoints. Each composition query counts as one. On 429, read the `Retry-After` header for the exact wait time in seconds.</Accordion>
  <Accordion title="Empty composition segments">If custom user properties aren't set on most users, composition returns sparse data. Ensure your tracking code sets properties via `identify()` calls before relying on composition breakdowns.</Accordion>
</AccordionGroup>

***

## What's Next

<CardGroup cols={2}>
  <Card title="Amplitude Integration" icon="wave-square" href="/integrations/amplitude">
    Back to Amplitude integration overview
  </Card>

  <Card title="Journey Mapping" icon="route" href="/integrations/amplitude/journey-mapping">
    Map conversion paths from user activity timelines
  </Card>

  <Card title="Personas API" icon="users" href="/api-reference/personas">
    Full reference for POST /api/v1/personas
  </Card>

  <Card title="Mave Agent" icon="brain" href="/api-reference/mave">
    Full reference for POST /api/v1/mave/chat
  </Card>
</CardGroup>
