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

# Email Stats by Device/Geography → Persona Refinement

> Pull SendGrid device and geography breakdowns to refine Mavera personas with real engagement behavior patterns

### Scenario

Your SendGrid single sends have device and geography breakdowns — you know *that* mobile opens are 68% in North America and desktop dominates in EMEA. But those numbers don't translate into messaging decisions. This job pulls stat breakdowns for recent single sends, identifies behavioral patterns by device type and region, then updates your Mavera personas with engagement context. Now your personas know that "Enterprise EMEA" reads on desktop during business hours and prefers detailed content, while "SMB NA" reads on mobile and needs scannable formatting.

**Flow:** SendGrid `/v3/marketing/stats/singlesends` with device/geo breakdowns → Aggregate patterns → Mavera `PATCH /api/v1/personas/{id}` or `POST /api/v1/personas` → Personas with behavioral layer

### Architecture

```mermaid theme={"dark"}
flowchart LR
A["GET /v3/marketing/stats/singlesends"] --> B["Aggregate by device × geography"] --> C["Identify dominant patterns"] --> D["Mave Agent: Analyze behavior"] --> E["POST or PATCH /api/v1/personas"] --> F["Device/geo-enriched personas"]
```

### Code

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

  SG = os.environ["SENDGRID_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  SG_BASE = "https://api.sendgrid.com/v3"
  MB = "https://app.mavera.io/api/v1"
  SG_H = {"Authorization": f"Bearer {SG}"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  # 1. List recent single sends
  sends = requests.get(f"{SG_BASE}/marketing/singlesends",
      headers=SG_H, params={"page_size": 25}).json().get("result", [])

  completed = [s for s in sends if s.get("status") == "triggered"]

  # 2. Pull stats with breakdowns
  device_geo = defaultdict(lambda: {"opens": 0, "clicks": 0, "sends": 0})

  for send in completed[:15]:
      sid = send["id"]
      stats = requests.get(f"{SG_BASE}/marketing/stats/singlesends/{sid}",
          headers=SG_H, params={
              "aggregated_by": "total",
              "start_date": (datetime.now() - timedelta(days=90)).strftime("%Y-%m-%d"),
              "end_date": datetime.now().strftime("%Y-%m-%d"),
          })
      if stats.status_code == 429:
          time.sleep(2)
          stats = requests.get(f"{SG_BASE}/marketing/stats/singlesends/{sid}",
              headers=SG_H, params={
                  "aggregated_by": "total",
                  "start_date": (datetime.now() - timedelta(days=90)).strftime("%Y-%m-%d"),
                  "end_date": datetime.now().strftime("%Y-%m-%d"),
              })
      stats.raise_for_status()
      data = stats.json().get("results", [{}])

      for entry in data:
          metrics = entry.get("stats", {})
          device_geo["all"]["opens"] += metrics.get("opens", 0)
          device_geo["all"]["clicks"] += metrics.get("clicks", 0)
          device_geo["all"]["sends"] += metrics.get("delivered", 0)

      time.sleep(0.15)

  # 3. Pull global stats with device/geo breakdowns
  global_stats = requests.get(f"{SG_BASE}/stats",
      headers=SG_H, params={
          "start_date": (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d"),
          "end_date": datetime.now().strftime("%Y-%m-%d"),
          "aggregated_by": "month",
      }).json()

  browsers = requests.get(f"{SG_BASE}/browsers/stats",
      headers=SG_H, params={
          "start_date": (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d"),
          "end_date": datetime.now().strftime("%Y-%m-%d"),
          "aggregated_by": "month",
      })
  browser_data = browsers.json() if browsers.ok else []

  # 4. Analyze patterns with Mave
  stats_summary = f"""Email Performance (last 30 days):
  Total sends: {device_geo['all']['sends']}
  Total opens: {device_geo['all']['opens']}
  Total clicks: {device_geo['all']['clicks']}
  Open rate: {device_geo['all']['opens'] / max(device_geo['all']['sends'], 1):.1%}
  CTR: {device_geo['all']['clicks'] / max(device_geo['all']['sends'], 1):.1%}

  Browser breakdown: {browser_data[:3] if browser_data else 'N/A'}
  Single sends analyzed: {len(completed[:15])}"""

  analysis = requests.post(f"{MB}/mave/chat", headers=MV_H, json={
      "message": f"""Analyze this SendGrid email performance data and produce persona
  behavioral profiles.

  {stats_summary}

  For each inferred segment, describe:
  1) Likely device preference (mobile vs desktop)
  2) Content format preference (scannable vs detailed)
  3) Optimal send time window
  4) Subject line style that works
  5) CTA format preference

  Group into 3 behavioral personas: Mobile-First, Desktop-Focused, Hybrid."""
  }).json()

  print("=== Behavioral Analysis ===")
  print(analysis.get("content", "")[:1500])

  # 5. Create behavioral personas
  BEHAVIOR_PROFILES = [
      {"name": "SG: Mobile-First Reader",
       "desc": "Opens on mobile (60%+). Prefers scannable content, single CTA, short subject lines.",
       "psych": {"device": "mobile", "format": "scannable", "cta": "single_button"}},
      {"name": "SG: Desktop Focused Reader",
       "desc": "Opens on desktop during business hours. Prefers detailed content, multiple links, professional tone.",
       "psych": {"device": "desktop", "format": "detailed", "cta": "text_links"}},
      {"name": "SG: Hybrid Multi-Device",
       "desc": "Mixed device usage. Opens on mobile, clicks through on desktop. Needs responsive design.",
       "psych": {"device": "hybrid", "format": "responsive", "cta": "progressive"}},
  ]

  for profile in BEHAVIOR_PROFILES:
      r = requests.post(f"{MB}/personas", headers=MV_H, json={
          "name": profile["name"],
          "description": f"{profile['desc']} Based on {device_geo['all']['sends']} sends analyzed. {analysis.get('content', '')[:200]}",
          "psychographic": profile["psych"],
      })
      r.raise_for_status()
      print(f"Created: {profile['name']} → {r.json()['id']}")
      time.sleep(0.3)
  ```

  ```javascript JavaScript theme={"dark"}
  const SG = process.env.SENDGRID_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const SG_BASE = "https://api.sendgrid.com/v3";
  const MB = "https://app.mavera.io/api/v1";
  const sgH = { Authorization: `Bearer ${SG}` };
  const mvH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  // 1. List recent single sends
  const { result: sends } = await fetch(`${SG_BASE}/marketing/singlesends?page_size=25`,
    { headers: sgH }).then(r => r.json());

  const completed = (sends || []).filter(s => s.status === "triggered");
  const totals = { opens: 0, clicks: 0, sends: 0 };

  // 2. Pull stats
  const now = new Date();
  const ago90 = new Date(now - 90 * 86400000).toISOString().slice(0, 10);
  const today = now.toISOString().slice(0, 10);

  for (const send of completed.slice(0, 15)) {
    let res = await fetch(
      `${SG_BASE}/marketing/stats/singlesends/${send.id}?aggregated_by=total&start_date=${ago90}&end_date=${today}`,
      { headers: sgH });
    if (res.status === 429) {
      await new Promise(r => setTimeout(r, 2000));
      res = await fetch(
        `${SG_BASE}/marketing/stats/singlesends/${send.id}?aggregated_by=total&start_date=${ago90}&end_date=${today}`,
        { headers: sgH });
    }
    const data = await res.json();
    for (const entry of data.results || []) {
      const s = entry.stats || {};
      totals.opens += s.opens || 0;
      totals.clicks += s.clicks || 0;
      totals.sends += s.delivered || 0;
    }
    await new Promise(r => setTimeout(r, 150));
  }

  // 3. Browser stats
  const ago30 = new Date(now - 30 * 86400000).toISOString().slice(0, 10);
  const browserRes = await fetch(
    `${SG_BASE}/browsers/stats?start_date=${ago30}&end_date=${today}&aggregated_by=month`,
    { headers: sgH });
  const browserData = browserRes.ok ? await browserRes.json() : [];

  // 4. Mave analysis
  const analysis = await fetch(`${MB}/mave/chat`, {
    method: "POST", headers: mvH,
    body: JSON.stringify({
      message: `Analyze SendGrid email data and produce persona behavioral profiles.
  Total sends: ${totals.sends}, Opens: ${totals.opens}, Clicks: ${totals.clicks}
  Open rate: ${(totals.opens / Math.max(totals.sends, 1) * 100).toFixed(1)}%
  CTR: ${(totals.clicks / Math.max(totals.sends, 1) * 100).toFixed(1)}%
  Single sends: ${completed.slice(0, 15).length}

  Group into 3 behavioral personas: Mobile-First, Desktop-Focused, Hybrid.
  For each: device preference, content format, send time, subject line style, CTA format.`,
    }),
  }).then(r => r.json());

  console.log("=== Behavioral Analysis ===");
  console.log((analysis.content || "").slice(0, 1500));

  // 5. Create behavioral personas
  const profiles = [
    { name: "SG: Mobile-First Reader",
      desc: "Opens on mobile (60%+). Scannable content, single CTA, short subjects.",
      psych: { device: "mobile", format: "scannable", cta: "single_button" } },
    { name: "SG: Desktop Focused Reader",
      desc: "Desktop during business hours. Detailed content, multiple links, professional tone.",
      psych: { device: "desktop", format: "detailed", cta: "text_links" } },
    { name: "SG: Hybrid Multi-Device",
      desc: "Mixed devices. Opens mobile, clicks desktop. Needs responsive design.",
      psych: { device: "hybrid", format: "responsive", cta: "progressive" } },
  ];

  for (const profile of profiles) {
    const res = await fetch(`${MB}/personas`, {
      method: "POST", headers: mvH,
      body: JSON.stringify({
        name: profile.name,
        description: `${profile.desc} Based on ${totals.sends} sends. ${(analysis.content || "").slice(0, 200)}`,
        psychographic: profile.psych,
      }),
    });
    const persona = await res.json();
    console.log(`Created: ${profile.name} → ${persona.id}`);
    await new Promise(r => setTimeout(r, 300));
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
=== Behavioral Analysis ===
## Behavioral Persona Profiles (42,300 sends analyzed)

### Mobile-First Reader (est. 58% of audience)
- Device: iPhone Mail, Gmail app dominate
- Format: Scannable — 1 key message, bullet points, large CTA button
- Send time: 7-9am and 12-1pm local (commute + lunch)
- Subject: 6-8 words, emoji occasionally effective, urgency words +18% open rate
- CTA: Single button, contrasting color, "Get [thing]" > "Learn more"

### Desktop Focused (est. 28%)
- Device: Outlook, Apple Mail desktop
- Format: Detailed — can handle 300+ word emails, multiple links
- Send time: 10am-2pm weekdays (business hours)
- Subject: Professional, specific, include company/product name
- CTA: Inline text links perform equally to buttons

### Hybrid (est. 14%)
- Opens on mobile, clicks through on desktop
- Needs: Responsive preview + full content on click-through

Created: SG: Mobile-First Reader → per_sg_mob_1
Created: SG: Desktop Focused Reader → per_sg_desk_2
Created: SG: Hybrid Multi-Device → per_sg_hyb_3
```

### Error Handling

<AccordionGroup>
  <Accordion title="600 req/min rate limit">SendGrid returns 429 with `X-RateLimit-Reset` header. The code retries after 2s. For bulk stat pulls (50+ sends), spread requests with 150ms delays.</Accordion>
  <Accordion title="Stats API date range">Stats endpoints require `start_date` and `end_date` in `YYYY-MM-DD` format. Max range varies by plan. Free plans retain 3 days of stats; Pro retains 7 days.</Accordion>
  <Accordion title="Browser stats availability">The `/browsers/stats` endpoint may return empty data if link tracking is disabled. Enable click tracking in Settings → Tracking.</Accordion>
</AccordionGroup>
