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
Code
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)
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));
}
Example Output
=== 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
600 req/min rate limit
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.Stats API date range
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.Browser stats availability
Browser stats availability
The
/browsers/stats endpoint may return empty data if link tracking is disabled. Enable click tracking in Settings → Tracking.