Skip to main content

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.

Scenario

Wistia captures individual viewer data that most platforms aggregate away — email addresses, geographic location, percent of video watched, number of visits, and viewing history. This job pulls your visitor-level stats, clusters viewers by their behavior patterns (binge-watchers, skimmers, one-time visitors, repeat engagers), then maps each cluster to a Mavera persona with psychographic depth. The result is not just analytics segments but fully realized personas that explain why viewers behave the way they do — so you can tailor content and outreach to each group.

Architecture

Code

import os, requests, time
from collections import defaultdict

WS = os.environ["WISTIA_API_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
WS_BASE = "https://api.wistia.com"
MV_BASE = "https://app.mavera.io/api/v1"
WS_H = {"Authorization": f"Bearer {WS}", "Accept": "application/json"}
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

# 1. Fetch all visitors with engagement data (paginated)
all_visitors = []
page = 1
while True:
    resp = requests.get(f"{WS_BASE}/v1/stats/visitors.json", headers=WS_H, params={
        "per_page": 100, "page": page,
    })
    if resp.status_code == 429:
        time.sleep(2)
        continue
    resp.raise_for_status()
    visitors = resp.json()
    if not visitors:
        break
    all_visitors.extend(visitors)
    page += 1
    time.sleep(0.2)

print(f"Total visitors: {len(all_visitors)}")

# 2. Enrich each visitor with per-event details
enriched = []
for visitor in all_visitors[:200]:
    visitor_key = visitor.get("visitor_key", "")
    events = visitor.get("events", [])

    total_watched = 0
    total_available = 0
    video_count = 0
    locations = set()

    for event in events:
        pct = event.get("percent_viewed", 0)
        total_watched += pct
        total_available += 1
        video_count += 1

    avg_percent = total_watched / max(video_count, 1)

    enriched.append({
        "visitor_key": visitor_key,
        "email": visitor.get("visitor_identity", {}).get("email", "anonymous"),
        "name": visitor.get("visitor_identity", {}).get("name", ""),
        "location": visitor.get("visitor_identity", {}).get("location", "unknown"),
        "video_count": video_count,
        "avg_percent_watched": round(avg_percent, 1),
        "total_visits": visitor.get("total_visits", 0),
        "last_active": visitor.get("last_active_at", ""),
    })

# 3. Cluster viewers by behavior
clusters = defaultdict(list)
for v in enriched:
    if v["video_count"] >= 5 and v["avg_percent_watched"] >= 75:
        clusters["binge_watchers"].append(v)
    elif v["video_count"] >= 3 and v["avg_percent_watched"] >= 50:
        clusters["engaged_explorers"].append(v)
    elif v["video_count"] == 1 and v["avg_percent_watched"] < 30:
        clusters["bouncers"].append(v)
    elif v["video_count"] >= 2 and v["avg_percent_watched"] < 50:
        clusters["skimmers"].append(v)
    else:
        clusters["casual_viewers"].append(v)

print(f"\nClusters:")
for name, members in clusters.items():
    print(f"  {name}: {len(members)} viewers")

# 4. Create Mavera personas for each cluster
persona_map = {}
cluster_descriptions = {
    "binge_watchers": "Watches 5+ videos, completes 75%+ of each. Deeply engaged, likely in active evaluation or already a customer seeking mastery. High intent signal.",
    "engaged_explorers": "Watches 3-4 videos at 50-75% completion. Browsing with purpose — comparing options or building understanding. Mid-funnel prospect.",
    "bouncers": "Watched 1 video, left before 30%. Either the content missed their intent, or they arrived by accident. Lowest engagement tier.",
    "skimmers": "Watches 2+ videos but under 50% each. Scanning for specific information — not consuming narrative content. Wants answers, not stories.",
    "casual_viewers": "Moderate engagement that doesn't fit other clusters. May be returning after time away or following a specific recommendation.",
}

for cluster_name, description in cluster_descriptions.items():
    if cluster_name not in clusters:
        continue
    sample = clusters[cluster_name][:5]
    sample_text = "\n".join(
        f"  - {s['email']}: {s['video_count']} videos, {s['avg_percent_watched']}% avg, {s['total_visits']} visits"
        for s in sample
    )

    persona = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
        "name": cluster_name.replace("_", " ").title(),
        "description": f"{description}\n\nSample viewers:\n{sample_text}",
    }).json()
    persona_map[cluster_name] = persona["id"]
    time.sleep(0.3)

# 5. Analyze via Mave with persona context
cluster_block = "\n\n".join(
    f"CLUSTER: {name.replace('_', ' ').title()} ({len(members)} viewers)\n"
    f"  Avg videos watched: {sum(m['video_count'] for m in members) / len(members):.1f}\n"
    f"  Avg completion: {sum(m['avg_percent_watched'] for m in members) / len(members):.1f}%\n"
    f"  Avg total visits: {sum(m['total_visits'] for m in members) / len(members):.1f}\n"
    f"  Identified (with email): {sum(1 for m in members if m['email'] != 'anonymous')}"
    for name, members in clusters.items() if members
)

analysis = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
    "message": f"""Map these viewer clusters to marketing personas and suggest outreach strategies.

VIEWER CLUSTERS:
{cluster_block}

Total viewers analyzed: {len(enriched)}

For each cluster:
1. **Persona Profile**: Name, motivation, likely job title/role, what they're looking for
2. **Content Affinity**: What video types/topics would resonate most with this persona?
3. **Outreach Strategy**: Best channel (email, retargeting, sales call) and message angle
4. **Conversion Probability**: Estimate likelihood this cluster converts to customer (high/medium/low)
5. **Content Gaps**: What video content are we missing that this persona would need?

End with: Which cluster represents the highest-value opportunity and what single action would convert them?""",
}).json()

print("\nVIEWER-LEVEL PERSONA MAPPING")
print("=" * 60)
for name, members in clusters.items():
    avg_pct = sum(m["avg_percent_watched"] for m in members) / len(members) if members else 0
    identified = sum(1 for m in members if m["email"] != "anonymous")
    print(f"  {name.replace('_', ' ').title():<22} {len(members):>4} viewers  "
          f"Avg:{avg_pct:>5.1f}%  Identified:{identified:>3}")
print("\n" + analysis.get("content", "")[:2000])

Example Output

Total visitors: 1,247

Clusters:
  binge_watchers: 89 viewers
  engaged_explorers: 234 viewers
  bouncers: 412 viewers
  skimmers: 187 viewers
  casual_viewers: 325 viewers

VIEWER-LEVEL PERSONA MAPPING
============================================================
  Binge Watchers            89 viewers  Avg: 82.4%  Identified: 67
  Engaged Explorers        234 viewers  Avg: 61.8%  Identified:142
  Bouncers                 412 viewers  Avg: 18.3%  Identified: 51
  Skimmers                 187 viewers  Avg: 34.7%  Identified: 89
  Casual Viewers           325 viewers  Avg: 45.2%  Identified:118

## Persona Profiles

### Binge Watchers → "The Evaluator"
Motivation: Actively comparing solutions before a purchase decision.
Likely role: Director or VP-level, delegated research phase. Watches
everything to build an internal recommendation document.
Content affinity: Case studies, ROI calculators, integration demos.
Outreach: Direct sales outreach within 48 hours. Message: "I noticed
you've been exploring our platform — want a personalized walkthrough?"
Conversion: HIGH (67% identified = already in your CRM)

### Engaged Explorers → "The Researcher"
Motivation: Building understanding before engaging sales. Wants to
self-serve their way to confidence.
Likely role: Manager or senior IC responsible for evaluation criteria.
Content affinity: Comparison guides, technical deep-dives, FAQ videos.
Outreach: Nurture email sequence with "resources you haven't seen yet."
Conversion: MEDIUM-HIGH (needs one more push)

### Bouncers → "The Drive-By"
Motivation: Arrived from a specific link (ad, social, email) but the
landing video didn't match their expectation.
Content affinity: Short, specific, problem-focused. Under 90 seconds.
Outreach: Retargeting with a different video format. Don't email — you
don't have their address (only 12% identified).
Conversion: LOW unless re-engaged with better-matched content

### HIGHEST-VALUE OPPORTUNITY
Binge Watchers (89 viewers, 75% identified). Single action: Have sales
call every identified Binge Watcher within 48 hours of their last
viewing session. These viewers have already sold themselves — they need
a human to say "let's do this."

Error Handling

Wistia’s email-level visitor tracking requires Turnstile (email gate) to be enabled on your videos. Without it, visitors are anonymous and identified only by visitor_key (a cookie-based ID).
The /v1/stats/visitors.json endpoint returns max 100 visitors per page. For accounts with 10,000+ visitors, pagination can take 100+ requests. Stay well within the 600 req/min limit by adding 200ms delays.
The events array within each visitor contains per-viewing data including percent_viewed, received_at, and media_id. For deeper analysis, join events by media_id to see which specific videos each cluster prefers.

What’s Next

Wistia Integration

Back to Wistia integration overview

Heatmap-Informed Creative Optimization

Diagnose drop-off points with specific edit recommendations

Personas API

Full reference for POST /api/v1/personas

Mave Agent

Full reference for POST /api/v1/mave/chat