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

# Lead-Qualified Viewer → Personalized Follow-up

> Identify high-intent Wistia viewers who watched 80%+ of your videos and generate personalized follow-up emails using Mavera Generate

### Scenario

A viewer who watches more than 80% of your product video is a high-intent lead — they've invested 3+ minutes evaluating your solution. This job pulls viewer-level stats from Wistia, filters for viewers who completed 80%+ of any video, matches them to CRM data by email, then uses Mavera Generate to create personalized follow-up emails for each viewer based on which video they watched, how far they got, and their CRM context. The result is a batch of ready-to-send sales emails triggered by viewing behavior — the most intent-rich signal your marketing funnel produces.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Wistia GET /v1/stats/medias/{id}.json"] --> B["Filter 80%+ watched"] --> C[Match email to CRM] --> D["POST /generate (personalized follow-up)"] --> E[Ready-to-send email batch]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  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"}

  TARGET_MEDIA_IDS = ["abc123def4", "xyz789ghi0", "mno456pqr1"]
  WATCH_THRESHOLD = 0.80

  # Simulated CRM data (replace with your CRM API call)
  CRM_DATA = {
      "jane@acme.com": {"name": "Jane Chen", "company": "Acme Corp", "role": "VP Marketing", "deal_stage": "Evaluation", "industry": "SaaS"},
      "mike@globex.com": {"name": "Mike Ross", "company": "Globex Inc", "role": "Director of Growth", "deal_stage": "Awareness", "industry": "Fintech"},
      "sarah@initech.com": {"name": "Sarah Kim", "company": "Initech", "role": "CMO", "deal_stage": "Consideration", "industry": "Healthcare"},
  }

  qualified_viewers = []

  for media_id in TARGET_MEDIA_IDS:
      # 1. Get media details
      media = requests.get(
          f"{WS_BASE}/v1/medias/{media_id}.json", headers=WS_H
      ).json()
      media_name = media.get("name", media_id)
      media_duration = media.get("duration", 0)

      # 2. Get visitor-level stats for this media
      visitors_resp = requests.get(
          f"{WS_BASE}/v1/stats/medias/{media_id}/visitors.json",
          headers=WS_H, params={"per_page": 100},
      ).json()

      for visitor in visitors_resp if isinstance(visitors_resp, list) else []:
          percent_watched = visitor.get("percent_viewed", 0)
          email = visitor.get("visitor_identity", {}).get("email", "")

          if percent_watched < WATCH_THRESHOLD or not email:
              continue

          crm = CRM_DATA.get(email, {})
          qualified_viewers.append({
              "email": email,
              "name": crm.get("name", email.split("@")[0].title()),
              "company": crm.get("company", "Unknown"),
              "role": crm.get("role", "Unknown"),
              "deal_stage": crm.get("deal_stage", "Unknown"),
              "industry": crm.get("industry", "Unknown"),
              "video_watched": media_name,
              "percent_watched": round(percent_watched * 100, 1),
              "video_duration": media_duration,
              "last_viewed": visitor.get("last_event_at", ""),
          })
      time.sleep(0.3)

  print(f"Qualified viewers (>{WATCH_THRESHOLD*100:.0f}% watched): {len(qualified_viewers)}")

  # 3. Generate personalized follow-up for each viewer
  emails = []
  for viewer in qualified_viewers:
      follow_up = requests.post(f"{MV_BASE}/generate", headers=MV_H, json={
          "prompt": f"""Write a personalized sales follow-up email for this viewer.

  VIEWER CONTEXT:
  - Name: {viewer['name']}
  - Company: {viewer['company']}
  - Role: {viewer['role']}
  - Industry: {viewer['industry']}
  - Deal Stage: {viewer['deal_stage']}
  - Video Watched: "{viewer['video_watched']}" ({viewer['percent_watched']}% completed)
  - Video Duration: {viewer['video_duration']}s

  RULES:
  1. Reference the specific video they watched — show you know what they saw
  2. Tailor the value proposition to their role and industry
  3. Match the follow-up intensity to their deal stage:
     - Awareness: Light touch, offer more content
     - Consideration: Share case study from their industry
     - Evaluation: Offer a personalized demo or pilot
  4. Keep it under 150 words
  5. One clear CTA — not multiple asks
  6. Sound human, not automated. No "I noticed you watched..." (creepy)
  7. Subject line should create curiosity, not summarize the email""",
      }).json()

      emails.append({
          **viewer,
          "email_content": follow_up.get("content", ""),
      })
      time.sleep(0.5)

  # 4. Output
  print("\nLEAD-QUALIFIED VIEWER → PERSONALIZED FOLLOW-UP")
  print("=" * 60)
  for e in emails:
      print(f"\n{'─' * 60}")
      print(f"TO: {e['name']} <{e['email']}> | {e['role']} @ {e['company']}")
      print(f"SIGNAL: Watched \"{e['video_watched']}\" ({e['percent_watched']}%) | Stage: {e['deal_stage']}")
      print(f"\n{e['email_content'][:600]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const WS = process.env.WISTIA_API_TOKEN;
  const MV = process.env.MAVERA_API_KEY;
  const WS_BASE = "https://api.wistia.com";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const WS_H = { Authorization: `Bearer ${WS}`, Accept: "application/json" };
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const TARGET_MEDIA_IDS = ["abc123def4", "xyz789ghi0", "mno456pqr1"];
  const WATCH_THRESHOLD = 0.80;

  const CRM_DATA = {
    "jane@acme.com": { name: "Jane Chen", company: "Acme Corp", role: "VP Marketing", dealStage: "Evaluation", industry: "SaaS" },
    "mike@globex.com": { name: "Mike Ross", company: "Globex Inc", role: "Director of Growth", dealStage: "Awareness", industry: "Fintech" },
    "sarah@initech.com": { name: "Sarah Kim", company: "Initech", role: "CMO", dealStage: "Consideration", industry: "Healthcare" },
  };

  const qualifiedViewers = [];

  for (const mediaId of TARGET_MEDIA_IDS) {
    // 1. Media details
    const media = await fetch(
      `${WS_BASE}/v1/medias/${mediaId}.json`, { headers: WS_H }
    ).then(r => r.json());

    // 2. Visitor stats
    const visitorsResp = await fetch(
      `${WS_BASE}/v1/stats/medias/${mediaId}/visitors.json?per_page=100`, { headers: WS_H }
    ).then(r => r.json());

    for (const visitor of Array.isArray(visitorsResp) ? visitorsResp : []) {
      const pctWatched = visitor.percent_viewed || 0;
      const email = visitor.visitor_identity?.email || "";
      if (pctWatched < WATCH_THRESHOLD || !email) continue;

      const crm = CRM_DATA[email] || {};
      qualifiedViewers.push({
        email, name: crm.name || email.split("@")[0],
        company: crm.company || "Unknown", role: crm.role || "Unknown",
        dealStage: crm.dealStage || "Unknown", industry: crm.industry || "Unknown",
        videoWatched: media.name || mediaId,
        percentWatched: Math.round(pctWatched * 1000) / 10,
        videoDuration: media.duration || 0,
        lastViewed: visitor.last_event_at || "",
      });
    }
    await new Promise(r => setTimeout(r, 300));
  }

  console.log(`Qualified viewers (>${WATCH_THRESHOLD * 100}%): ${qualifiedViewers.length}`);

  // 3. Generate follow-ups
  const emails = [];
  for (const viewer of qualifiedViewers) {
    const followUp = await fetch(`${MV_BASE}/generate`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        prompt: `Personalized follow-up email.\n\nVIEWER:\n- ${viewer.name}, ${viewer.role} @ ${viewer.company} (${viewer.industry})\n- Stage: ${viewer.dealStage}\n- Watched: "${viewer.videoWatched}" (${viewer.percentWatched}% of ${viewer.videoDuration}s)\n\nRULES:\n1. Reference the specific video\n2. Tailor to role + industry\n3. Match intensity to deal stage (Awareness=light, Consideration=case study, Evaluation=demo offer)\n4. Under 150 words, one CTA\n5. Sound human, not automated\n6. Curiosity-driven subject line`,
      }),
    }).then(r => r.json());

    emails.push({ ...viewer, emailContent: followUp.content || "" });
    await new Promise(r => setTimeout(r, 500));
  }

  // 4. Output
  console.log("\nLEAD-QUALIFIED VIEWER → PERSONALIZED FOLLOW-UP");
  console.log("=".repeat(60));
  for (const e of emails) {
    console.log(`\n${"─".repeat(60)}`);
    console.log(`TO: ${e.name} <${e.email}> | ${e.role} @ ${e.company}`);
    console.log(`SIGNAL: "${e.videoWatched}" (${e.percentWatched}%) | Stage: ${e.dealStage}`);
    console.log(`\n${e.emailContent.slice(0, 600)}`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Qualified viewers (>80% watched): 3

LEAD-QUALIFIED VIEWER → PERSONALIZED FOLLOW-UP
============================================================

──────────────────────────────────────────────────────────────
TO: Jane Chen <jane@acme.com> | VP Marketing @ Acme Corp
SIGNAL: Watched "Product Demo — Enterprise Dashboard" (94.2%) | Stage: Evaluation

Subject: The reporting gap your team is probably fighting right now

Hi Jane,

Your marketing team at Acme is probably spending Monday mornings the same
way most SaaS marketing teams do — pulling data from 6 dashboards into a
slide deck that's outdated by Tuesday.

The enterprise dashboard you saw handles the consolidation automatically,
but the real unlock for a VP-level view is the anomaly alerts. Instead of
your team flagging a campaign underperforming on Thursday, the system
catches it Monday at 6 AM.

I can set up a 20-minute walkthrough using your actual campaign data —
no slides, just your numbers in the dashboard.

Worth a look?

──────────────────────────────────────────────────────────────
TO: Mike Ross <mike@globex.com> | Director of Growth @ Globex Inc
SIGNAL: Watched "How-To: Getting Started" (82.1%) | Stage: Awareness

Subject: A fintech growth playbook you haven't seen yet

Hey Mike,

Most growth teams in fintech are optimizing the same three channels.
The teams pulling ahead are the ones who figured out that reporting
speed IS a growth lever — when you catch a winning experiment 3 days
earlier, you compound faster.

We put together a case study from a fintech company your size that
cut their experiment cycle from 14 days to 5. Happy to send it over
if that's relevant to what you're building at Globex.

Just reply "send it" and I'll drop it in your inbox.

──────────────────────────────────────────────────────────────
TO: Sarah Kim <sarah@initech.com> | CMO @ Initech
SIGNAL: Watched "ROI Calculator Walkthrough" (88.7%) | Stage: Consideration

Subject: Your Initech numbers in the calculator

Sarah,

The ROI calculator gives you a general estimate, but the healthcare
benchmarks are different — compliance reporting alone shifts the savings
by 30-40% for most health-tech companies.

I can run the calculator with Initech's actual data (team size, reporting
frequency, compliance requirements) and send you a one-page summary your
CFO would actually read.

Takes 15 minutes. Want me to set it up?
```

### Error Handling

<AccordionGroup>
  <Accordion title="Visitor identity requires Turnstile">Email-based viewer matching depends on Wistia's [Turnstile](https://wistia.com/support/player/turnstile) email gate being enabled. Without it, viewers are anonymous. Enable Turnstile on high-intent videos (demos, pricing, case studies) to capture emails.</Accordion>
  <Accordion title="CRM matching">The example uses a static CRM lookup. In production, replace `CRM_DATA` with your CRM's API (Salesforce, HubSpot, Pipedrive) to enrich viewer data with deal stage, company size, and activity history.</Accordion>
  <Accordion title="Privacy and consent">Sending emails triggered by video viewing behavior may require GDPR/CCPA consent. Ensure your Turnstile email gate includes a consent checkbox and your CRM flags opted-in contacts. Never email viewers who haven't opted in.</Accordion>
</AccordionGroup>

***

## What's Next

<CardGroup cols={2}>
  <Card title="Wistia Integration" icon="circle-play" href="/integrations/wistia">
    Back to Wistia integration overview
  </Card>

  <Card title="Wistia Embeds → Brand Voice Source" icon="microphone" href="/integrations/wistia/spoken-brand-voice">
    Build a brand voice profile from spoken content
  </Card>

  <Card title="Viewer-Level Persona Mapping" icon="users" href="/integrations/wistia/viewer-persona-mapping">
    Map viewers to psychographic personas
  </Card>

  <Card title="Generate API" icon="wand-magic-sparkles" href="/api-reference/generate">
    Full reference for POST /api/v1/generate
  </Card>
</CardGroup>
