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

# Audience Demographics → Persona Creation

> Pull GA4 demographic intersections (age, gender, country, device) with conversion metrics and create data-backed Mavera Custom Personas for your actual converting audience

### Scenario

Your GA4 property has thousands of converting visitors segmented by age, gender, country, and device — but your personas are based on gut feel. You pull a RunReport crossing `userAgeBracket`, `userGender`, `country`, and `deviceCategory` with conversion metrics, identify the top-performing demographic intersections, and create a Mavera Custom Persona for each. The result is a persona library calibrated to your actual converting audience.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["GA4 RunReport (age × gender × country × device)"] --> B[Rank intersections by conversion volume] --> C["POST /api/v1/personas"] --> D[Data-backed persona library]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time
  from google.analytics.data_v1beta import BetaAnalyticsDataClient
  from google.analytics.data_v1beta.types import (
      RunReportRequest, Dimension, Metric, DateRange, OrderBy,
  )

  PROPERTY_ID = os.environ["GA4_PROPERTY_ID"]
  MV = os.environ["MAVERA_API_KEY"]
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  client = BetaAnalyticsDataClient()

  report = client.run_report(RunReportRequest(
      property=f"properties/{PROPERTY_ID}",
      dimensions=[
          Dimension(name="userAgeBracket"),
          Dimension(name="userGender"),
          Dimension(name="country"),
          Dimension(name="deviceCategory"),
      ],
      metrics=[
          Metric(name="totalUsers"),
          Metric(name="conversions"),
          Metric(name="sessions"),
          Metric(name="engagementRate"),
          Metric(name="averageSessionDuration"),
      ],
      date_ranges=[DateRange(start_date="30daysAgo", end_date="today")],
      order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="conversions"), desc=True)],
      limit=200,
  ))

  intersections = []
  for row in report.rows:
      age = row.dimension_values[0].value
      gender = row.dimension_values[1].value
      country = row.dimension_values[2].value
      device = row.dimension_values[3].value
      users = int(row.metric_values[0].value)
      conversions = int(row.metric_values[1].value)
      sessions = int(row.metric_values[2].value)
      engagement = float(row.metric_values[3].value)
      avg_duration = float(row.metric_values[4].value)

      if conversions < 5 or age == "(not set)" or gender == "(not set)":
          continue

      intersections.append({
          "age": age, "gender": gender, "country": country, "device": device,
          "users": users, "conversions": conversions, "sessions": sessions,
          "engagement": engagement, "avg_duration": avg_duration,
          "conv_rate": conversions / max(sessions, 1),
      })

  intersections.sort(key=lambda x: x["conversions"], reverse=True)
  top = intersections[:10]

  total_conv = sum(i["conversions"] for i in intersections) or 1
  print(f"Total intersections: {len(intersections)} | Creating personas for top {len(top)}")

  created = []
  for seg in top:
      conv_share = seg["conversions"] / total_conv
      name = f"GA4: {seg['gender'].title()} {seg['age']} — {seg['country']} ({seg['device']})"
      desc = (
          f"Data-backed persona from GA4 (last 30 days). "
          f"Age: {seg['age']}, Gender: {seg['gender']}, Country: {seg['country']}, Device: {seg['device']}. "
          f"Conversions: {seg['conversions']} ({conv_share:.0%} of total). "
          f"Engagement rate: {seg['engagement']:.1%}. Avg session: {seg['avg_duration']:.0f}s. "
          f"Conv rate: {seg['conv_rate']:.2%}."
      )

      r = requests.post(f"{MB}/personas", headers=MH, json={
          "name": name,
          "description": desc,
          "demographic": {
              "age_range": seg["age"],
              "gender": seg["gender"],
              "countries": [seg["country"]],
          },
          "psychographic": {
              "device_preference": seg["device"],
              "engagement_level": "high" if seg["engagement"] > 0.6 else "medium",
              "conversion_share": f"{conv_share:.1%}",
          },
      })
      r.raise_for_status()
      persona = r.json()
      created.append({"name": name, "id": persona["id"], "conversions": seg["conversions"]})
      print(f"  {name} → {persona['id']} ({seg['conversions']} conv, {conv_share:.0%})")
      time.sleep(0.3)

  print(f"\nCreated {len(created)} demographic personas from GA4 data")
  ```

  ```javascript JavaScript theme={"dark"}
  const MV = process.env.MAVERA_API_KEY;
  const PROPERTY_ID = process.env.GA4_PROPERTY_ID;
  const KEY_FILE = JSON.parse(require("fs").readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, "utf8"));
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const { GoogleAuth } = require("google-auth-library");
  const auth = new GoogleAuth({
    credentials: KEY_FILE,
    scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
  });
  const accessToken = await auth.getAccessToken();

  const gaRes = await fetch(
    `https://analyticsdata.googleapis.com/v1beta/properties/${PROPERTY_ID}:runReport`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        dimensions: [
          { name: "userAgeBracket" }, { name: "userGender" },
          { name: "country" }, { name: "deviceCategory" },
        ],
        metrics: [
          { name: "totalUsers" }, { name: "conversions" },
          { name: "sessions" }, { name: "engagementRate" },
          { name: "averageSessionDuration" },
        ],
        dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
        orderBys: [{ metric: { metricName: "conversions" }, desc: true }],
        limit: 200,
      }),
    }
  ).then((r) => r.json());

  const intersections = (gaRes.rows || [])
    .map((row) => ({
      age: row.dimensionValues[0].value,
      gender: row.dimensionValues[1].value,
      country: row.dimensionValues[2].value,
      device: row.dimensionValues[3].value,
      users: parseInt(row.metricValues[0].value),
      conversions: parseInt(row.metricValues[1].value),
      sessions: parseInt(row.metricValues[2].value),
      engagement: parseFloat(row.metricValues[3].value),
      avgDuration: parseFloat(row.metricValues[4].value),
    }))
    .filter((i) => i.conversions >= 5 && i.age !== "(not set)" && i.gender !== "(not set)")
    .sort((a, b) => b.conversions - a.conversions);

  const totalConv = intersections.reduce((s, i) => s + i.conversions, 0) || 1;
  const top = intersections.slice(0, 10);

  const created = [];
  for (const seg of top) {
    const convShare = seg.conversions / totalConv;
    const name = `GA4: ${seg.gender} ${seg.age} — ${seg.country} (${seg.device})`;

    const p = await fetch(`${MB}/personas`, {
      method: "POST", headers: MH,
      body: JSON.stringify({
        name,
        description:
          `Data-backed persona (30d). Age: ${seg.age}, Gender: ${seg.gender}, ` +
          `Country: ${seg.country}, Device: ${seg.device}. ` +
          `Conv: ${seg.conversions} (${(convShare * 100).toFixed(0)}%). ` +
          `Engagement: ${(seg.engagement * 100).toFixed(1)}%. Session: ${seg.avgDuration.toFixed(0)}s.`,
        demographic: { age_range: seg.age, gender: seg.gender, countries: [seg.country] },
        psychographic: {
          device_preference: seg.device,
          engagement_level: seg.engagement > 0.6 ? "high" : "medium",
        },
      }),
    }).then((r) => r.json());

    created.push({ name, id: p.id, conversions: seg.conversions });
    console.log(`  ${name} → ${p.id} (${seg.conversions} conv, ${(convShare * 100).toFixed(0)}%)`);
    await new Promise((r) => setTimeout(r, 300));
  }

  console.log(`\nCreated ${created.length} demographic personas`);
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "total_intersections": 142,
  "created": 10,
  "personas": [
    { "name": "GA4: male 25-34 — United States (desktop)", "id": "per_ga4_01", "conversions": 312, "share": "18%" },
    { "name": "GA4: female 25-34 — United States (mobile)", "id": "per_ga4_02", "conversions": 245, "share": "14%" },
    { "name": "GA4: male 35-44 — United Kingdom (desktop)", "id": "per_ga4_03", "conversions": 189, "share": "11%" },
    { "name": "GA4: female 35-44 — United States (desktop)", "id": "per_ga4_04", "conversions": 156, "share": "9%" },
    { "name": "GA4: male 25-34 — Canada (mobile)", "id": "per_ga4_05", "conversions": 98, "share": "6%" }
  ]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="(not set) dimensions">GA4 can't classify all users by age/gender — especially those without Google sign-in. The code filters these out. Expect 20–40% of traffic to be unclassified.</Accordion>
  <Accordion title="Thresholding / data redaction">GA4 applies [thresholding](https://support.google.com/analytics/answer/9383630) to protect user privacy. Small segments may be hidden. Widen your date range or reduce dimension cardinality to get more data.</Accordion>
  <Accordion title="Quota exhaustion (429)">50,000 requests/day is generous, but bursts of 10+ concurrent requests hit the per-property limit. Add 300ms delays between calls and retry on 429 with exponential backoff.</Accordion>
  <Accordion title="Service account permissions">If you get `403 PERMISSION_DENIED`, verify the service account email is added as a Viewer (or higher) in GA4 → Admin → Property Access Management.</Accordion>
</AccordionGroup>

***

## What's Next

<CardGroup cols={2}>
  <Card title="GA4 Integration" icon="chart-line" href="/integrations/ga4">
    Back to GA4 integration overview
  </Card>

  <Card title="Interest Category → Content Strategy" icon="lightbulb" href="/integrations/ga4/interest-content-strategy">
    Build a content plan from audience interests
  </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>
