Scenario
Segment’s identity graph merges user identities across sources — the same person visiting your website, using your mobile app, and opening your emails appears as one unified profile. You pull the merged identity data showing which channels a user engages with, then send it to Mave with the instruction to create a unified persona that accounts for cross-channel behavior differences. The result is a persona that understands the user’s full journey, not just a single-channel slice.Architecture
Code
import os, requests, time
from collections import defaultdict
SEG_TOKEN = os.environ["SEGMENT_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
SH = {"Authorization": f"Bearer {SEG_TOKEN}", "Content-Type": "application/json"}
SPACE_ID = os.environ.get("SEGMENT_SPACE_ID", "spa_xxxxx")
r = requests.get(
f"https://api.segmentapis.com/spaces/{SPACE_ID}/collections/users/profiles",
headers=SH,
params={"limit": 100, "include": "traits,external_ids"},
)
profiles = []
if r.status_code == 200:
profiles = r.json().get("data", {}).get("profiles", [])
if not profiles:
profiles = [
{
"traits": {"email": "j.doe@example.com", "ltv": 890, "plan": "pro", "last_web_visit": "2026-03-15", "last_mobile_open": "2026-03-16", "last_email_click": "2026-03-12", "web_sessions_30d": 14, "mobile_sessions_30d": 22, "emails_opened_30d": 8},
"external_ids": [{"type": "user_id", "id": "usr_001"}, {"type": "ios_id", "id": "ios_abc"}, {"type": "email", "id": "j.doe@example.com"}],
},
{
"traits": {"email": "s.chen@example.com", "ltv": 340, "plan": "starter", "last_web_visit": "2026-03-10", "last_mobile_open": "2026-02-28", "last_email_click": "2026-03-14", "web_sessions_30d": 4, "mobile_sessions_30d": 1, "emails_opened_30d": 12},
"external_ids": [{"type": "user_id", "id": "usr_002"}, {"type": "email", "id": "s.chen@example.com"}],
},
{
"traits": {"email": "m.patel@example.com", "ltv": 1450, "plan": "enterprise", "last_web_visit": "2026-03-17", "last_mobile_open": "2026-03-17", "last_email_click": "2026-03-16", "web_sessions_30d": 28, "mobile_sessions_30d": 35, "emails_opened_30d": 15},
"external_ids": [{"type": "user_id", "id": "usr_003"}, {"type": "ios_id", "id": "ios_xyz"}, {"type": "android_id", "id": "and_789"}, {"type": "email", "id": "m.patel@example.com"}],
},
] * 10
channel_patterns = {"web_dominant": [], "mobile_dominant": [], "email_dominant": [], "omnichannel": []}
for p in profiles:
traits = p.get("traits", {})
ext_ids = p.get("external_ids", [])
id_types = [e.get("type", "") for e in ext_ids]
web = int(traits.get("web_sessions_30d", 0) or 0)
mobile = int(traits.get("mobile_sessions_30d", 0) or 0)
email = int(traits.get("emails_opened_30d", 0) or 0)
total = web + mobile + email
if total == 0:
continue
profile_data = {
"ltv": float(traits.get("ltv", 0) or 0),
"plan": traits.get("plan", "unknown"),
"web": web, "mobile": mobile, "email": email,
"id_count": len(id_types),
"channels": id_types,
}
web_pct = web / total
mobile_pct = mobile / total
email_pct = email / total
if web_pct > 0.5:
channel_patterns["web_dominant"].append(profile_data)
elif mobile_pct > 0.5:
channel_patterns["mobile_dominant"].append(profile_data)
elif email_pct > 0.5:
channel_patterns["email_dominant"].append(profile_data)
else:
channel_patterns["omnichannel"].append(profile_data)
def summarize_pattern(users, label):
if not users:
return ""
avg_ltv = sum(u["ltv"] for u in users) / len(users)
avg_web = sum(u["web"] for u in users) / len(users)
avg_mobile = sum(u["mobile"] for u in users) / len(users)
avg_email = sum(u["email"] for u in users) / len(users)
avg_ids = sum(u["id_count"] for u in users) / len(users)
plans = defaultdict(int)
for u in users:
plans[u["plan"]] += 1
top_plans = sorted(plans, key=plans.get, reverse=True)[:3]
return (
f"**{label}** (N={len(users)})\n"
f" Avg LTV: ${avg_ltv:.0f} | Web sessions: {avg_web:.0f} | Mobile: {avg_mobile:.0f} | Email opens: {avg_email:.0f}\n"
f" Avg identity count: {avg_ids:.1f} | Plans: {', '.join(top_plans)}"
)
pattern_block = "\n\n".join(
summarize_pattern(users, label.replace("_", " ").title())
for label, users in channel_patterns.items()
if users
)
mave = requests.post(
f"{MB}/mave/chat",
headers=MH,
json={"message": f"""Create a unified persona framework that accounts for cross-channel behavior. Users interact with our product via website, mobile app, and email — and the same user behaves differently on each channel.
CROSS-CHANNEL PATTERNS (from Segment identity graph, 30 days):
{pattern_block}
For each channel pattern, provide:
1. A persona name and 2-sentence description capturing their cross-channel identity
2. Why they prefer their dominant channel — what need does it serve?
3. How to reach them on their weaker channels — what message/format would work?
4. Cross-channel journey map — how do they typically move between channels?
5. Content format recommendations per channel
6. Risk of losing them — which channel loss would cause churn?
Then provide an overall cross-channel strategy:
- Where to invest in consistency vs. channel-specific adaptation
- Identity resolution opportunities (matching anonymous to known)
- Moments when users switch channels and what triggers the switch"""},
).json()
print("--- Cross-Channel Unified Persona Framework ---")
print(mave.get("content", "")[:3000])
const SEG_TOKEN = process.env.SEGMENT_TOKEN;
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 SPACE_ID = process.env.SEGMENT_SPACE_ID || "spa_xxxxx";
const profRes = await fetch(
`https://api.segmentapis.com/spaces/${SPACE_ID}/collections/users/profiles?limit=100&include=traits,external_ids`,
{ headers: { Authorization: `Bearer ${SEG_TOKEN}`, "Content-Type": "application/json" } }
).then((r) => r.json());
let profiles = profRes.data?.profiles || [];
if (!profiles.length) {
profiles = Array(10).fill(null).flatMap(() => [
{ traits: { ltv: 890, plan: "pro", web_sessions_30d: 14, mobile_sessions_30d: 22, emails_opened_30d: 8 }, external_ids: [{ type: "user_id" }, { type: "ios_id" }, { type: "email" }] },
{ traits: { ltv: 340, plan: "starter", web_sessions_30d: 4, mobile_sessions_30d: 1, emails_opened_30d: 12 }, external_ids: [{ type: "user_id" }, { type: "email" }] },
{ traits: { ltv: 1450, plan: "enterprise", web_sessions_30d: 28, mobile_sessions_30d: 35, emails_opened_30d: 15 }, external_ids: [{ type: "user_id" }, { type: "ios_id" }, { type: "android_id" }, { type: "email" }] },
]);
}
const patterns = { web_dominant: [], mobile_dominant: [], email_dominant: [], omnichannel: [] };
for (const p of profiles) {
const t = p.traits || {};
const web = parseInt(t.web_sessions_30d || 0);
const mobile = parseInt(t.mobile_sessions_30d || 0);
const email = parseInt(t.emails_opened_30d || 0);
const total = web + mobile + email;
if (!total) continue;
const data = { ltv: parseFloat(t.ltv || 0), plan: t.plan || "unknown", web, mobile, email, idCount: (p.external_ids || []).length };
if (web / total > 0.5) patterns.web_dominant.push(data);
else if (mobile / total > 0.5) patterns.mobile_dominant.push(data);
else if (email / total > 0.5) patterns.email_dominant.push(data);
else patterns.omnichannel.push(data);
}
function summarize(users, label) {
if (!users.length) return "";
const avg = (key) => users.reduce((s, u) => s + u[key], 0) / users.length;
const plans = {};
users.forEach((u) => { plans[u.plan] = (plans[u.plan] || 0) + 1; });
const topPlans = Object.keys(plans).sort((a, b) => plans[b] - plans[a]).slice(0, 3);
return `**${label}** (N=${users.length})\n LTV: $${avg("ltv").toFixed(0)} | Web: ${avg("web").toFixed(0)} | Mobile: ${avg("mobile").toFixed(0)} | Email: ${avg("email").toFixed(0)}\n IDs: ${avg("idCount").toFixed(1)} | Plans: ${topPlans.join(", ")}`;
}
const patternBlock = Object.entries(patterns)
.filter(([, u]) => u.length)
.map(([k, u]) => summarize(u, k.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())))
.join("\n\n");
const mave = await fetch(`${MB}/mave/chat`, { method: "POST", headers: MH,
body: JSON.stringify({
message: `Create unified persona framework for cross-channel behavior (website, mobile, email).\n\nPATTERNS (Segment identity graph, 30d):\n\n${patternBlock}\n\nFor each pattern: 1) Persona name + description 2) Why they prefer dominant channel 3) How to reach on weak channels 4) Cross-channel journey 5) Content format per channel 6) Churn risk by channel loss.\n\nOverall: consistency vs. adaptation strategy, identity resolution opportunities, channel-switch triggers.`,
}),
}).then((r) => r.json());
console.log("--- Cross-Channel Persona Framework ---");
console.log((mave.content || "").slice(0, 3000));
Example Output
--- Cross-Channel Unified Persona Framework ---
## 1. Mobile Dominant — "The On-The-Go Operator"
Power user who runs their business from their phone. 22 mobile sessions
vs. 14 web sessions per month. LTV: $890. Pro plan.
**Why mobile:** They manage in motion — between meetings, on commutes,
during events. Mobile gives them quick-check capability without context-
switching from other work.
**Reach on web:** Desktop email with "Continue on Desktop" CTAs for
complex tasks (reports, settings). They use web for setup, mobile for
monitoring.
**Journey:** Discovers on web → installs mobile → daily mobile check-ins
→ monthly web deep-dive → upgrade decision on desktop.
## 2. Email Dominant — "The Inbox Dweller"
Engages primarily through email. 12 opens vs. 4 web sessions and 1
mobile session. LTV: $340. Starter plan.
**Why email:** They're not heavy product users — they consume insights
passively. Email delivers value without requiring them to log in.
**Risk:** HIGH churn risk if email quality drops. They don't have a
product habit — email IS their product experience. If you stop sending
useful content, they'll forget you exist.
## 3. Omnichannel — "The Power Orchestrator"
Uses all channels heavily. 28 web + 35 mobile + 15 email. LTV: $1,450.
Enterprise plan. 4+ identity graph connections.
**Journey:** Morning email digest → mobile check during commute →
desktop deep work session → mobile monitoring → evening email summary.
This user expects seamless continuity across all channels.
## Cross-Channel Strategy
- **Consistency:** Value proposition, core metrics, notification content
- **Adaptation:** Layout (mobile-optimized cards vs. desktop tables),
depth (email summaries → web details), interaction (mobile gestures
vs. desktop keyboard shortcuts)
- **Identity gaps:** 30% of profiles have only 2 identifiers. Push
mobile app install and email verification to resolve anonymous web
visitors to known profiles.
Error Handling
Identity graph requires Engage
Identity graph requires Engage
The identity resolution features (
external_ids, merged profiles) require Segment Engage. On Connections-only plans, profiles exist per-source without cross-source merging.External ID types
External ID types
Common
external_id types include user_id, email, ios_id, android_id, anonymous_id, and ga_client_id. The code counts ID types to estimate cross-channel coverage — more ID types = more channels connected.Channel activity estimation
Channel activity estimation
The example uses traits like
web_sessions_30d and mobile_sessions_30d. These must be set up as computed traits in Segment Engage. If they don’t exist, compute from raw events using the Events API or set up custom computed traits.What’s Next
Segment Integration
Back to Segment integration overview
Real-Time Persona Triggers
Auto-trigger research from live events
Mave Agent
Full reference for POST /api/v1/mave/chat
All Integrations
50+ API integrations with full code