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

# CTA Performance × Focus Group

> Pull Wistia CTA click-through data and test messaging and placement variations with Mavera Focus Groups for optimized calls-to-action

### Scenario

Wistia lets you embed CTAs (calls-to-action) at specific timestamps in your videos — but click-through rates are often disappointing and A/B testing natively is limited. This job pulls CTA click-through data from Wistia's stats, then sends the CTA text, placement, and performance data to a Focus Group testing variations of messaging and timing. The result is a CTA optimization plan with specific wording changes and placement shifts validated by synthetic personas before you commit to changes.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Wistia GET /v1/medias.json + stats"] --> B["Extract CTA placement + click-through"] --> C["POST /focus-groups"] --> D[CTA optimization plan]
```

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

  # 1. Fetch medias with their customizations (CTAs)
  medias = requests.get(f"{WS_BASE}/v1/medias.json", headers=WS_H, params={
      "per_page": 50, "sort_by": "play_count", "sort_direction": 0,
  }).json()

  cta_videos = []
  for media in medias:
      hashed_id = media.get("hashed_id", "")
      customizations = media.get("embed_options", {})
      cta = customizations.get("call_to_action", {}) or customizations.get("postRollCTA", {})

      if not cta:
          continue

      # 2. Get stats for this media
      stats = requests.get(
          f"{WS_BASE}/v1/stats/medias/{hashed_id}.json", headers=WS_H
      ).json()

      play_count = stats.get("play_count", 0)
      cta_clicks = cta.get("clicks", 0)
      cta_ctr = round(cta_clicks / max(play_count, 1) * 100, 2)

      cta_videos.append({
          "name": media.get("name", "Untitled"),
          "hashed_id": hashed_id,
          "duration": media.get("duration", 0),
          "plays": play_count,
          "cta_text": cta.get("text", ""),
          "cta_url": cta.get("url", ""),
          "cta_type": cta.get("type", "text"),
          "cta_time": cta.get("time", "end"),
          "cta_clicks": cta_clicks,
          "cta_ctr": cta_ctr,
      })
      time.sleep(0.2)

  print(f"Found {len(cta_videos)} videos with CTAs")

  # 3. Sort by CTR for analysis
  cta_videos.sort(key=lambda x: x["cta_ctr"])

  # 4. Create Focus Group personas
  persona_ids = []
  for name, desc in [
      ("Impulse Clicker", "Clicks CTAs that create urgency or curiosity. Responds to 'limited time,' 'see inside,' and action verbs. Ignores generic 'learn more' buttons. Mobile-first viewer who needs the CTA to be thumb-friendly."),
      ("Deliberate Researcher", "Only clicks CTAs after watching the full video. Needs the CTA to match the video's promise exactly. Distrusts 'bait-and-switch' where the CTA leads somewhere unrelated. Values specificity over urgency."),
      ("Skeptical Executive", "Senior decision-maker watching to evaluate vendors. Will only click if the CTA offers something they can't get elsewhere — a demo, a calculator, an exclusive report. Ignores 'contact us' and 'subscribe.'"),
      ("Content Binger", "Watches 5+ videos in a session. CTAs interrupt their flow. Will only click if the CTA offers a natural next step in their learning journey, not a sales redirect."),
  ]  :
      p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
          "name": name, "description": desc,
      }).json()
      persona_ids.append(p["id"])
      time.sleep(0.3)

  # 5. Build CTA performance summary
  cta_block = "\n\n".join(
      f"VIDEO: \"{v['name']}\"\n"
      f"  CTA Text: \"{v['cta_text']}\"\n"
      f"  CTA URL: {v['cta_url']}\n"
      f"  Placement: {v['cta_type']} at {v['cta_time']} | Plays: {v['plays']:,} | "
      f"Clicks: {v['cta_clicks']} | CTR: {v['cta_ctr']}%"
      for v in cta_videos[:8]
  )

  # 6. Run Focus Group
  fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
      "name": "CTA Performance Optimization",
      "persona_ids": persona_ids,
      "questions": [
          f"Review these video CTAs and their click-through rates. For each, explain why you would or wouldn't click:\n\n{cta_block}",
          "Rewrite the 3 lowest-performing CTA texts to maximize YOUR click probability. Explain what you changed and why.",
          "Where in the video should the CTA appear? At the end (post-roll)? Mid-video? As an overlay? Why does placement matter to you?",
          "What CTA format would make you most likely to click: text button, image banner, or interactive annotation? Why?",
          "If you've already decided you're interested in the product, what CTA text would make you convert immediately?",
      ],
      "responses_per_persona": 2,
  }).json()

  for _ in range(20):
      time.sleep(5)
      fg_data = requests.get(f"{MV_BASE}/focus-groups/{fg['id']}", headers=MV_H).json()
      if fg_data.get("status") == "completed":
          break

  # 7. Output
  print("\nCTA PERFORMANCE × FOCUS GROUP")
  print("=" * 60)
  print(f"{'Video':<35} {'CTA Text':<25} {'Plays':>7} {'CTR':>7}")
  print("-" * 60)
  for v in cta_videos[:8]:
      print(f"  {v['name'][:33]:<35} {v['cta_text'][:23]:<25} {v['plays']:>7,} {v['cta_ctr']:>6.2f}%")

  print("\nFOCUS GROUP RESPONSES:")
  for resp in fg_data.get("responses", []):
      print(f"\n[{resp.get('persona_name', '?')}] {resp.get('question', '')[:55]}...")
      print(f"  → {resp.get('answer', '')[:400]}")
  ```

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

  // 1. Fetch medias with customizations
  const medias = await fetch(
    `${WS_BASE}/v1/medias.json?per_page=50&sort_by=play_count&sort_direction=0`, { headers: WS_H }
  ).then(r => r.json());

  const ctaVideos = [];
  for (const media of medias) {
    const hashedId = media.hashed_id || "";
    const cta = media.embed_options?.call_to_action || media.embed_options?.postRollCTA;
    if (!cta) continue;

    // 2. Stats
    const stats = await fetch(
      `${WS_BASE}/v1/stats/medias/${hashedId}.json`, { headers: WS_H }
    ).then(r => r.json());

    const playCount = stats.play_count || 0;
    const ctaClicks = cta.clicks || 0;
    ctaVideos.push({
      name: media.name || "Untitled", hashedId,
      duration: media.duration || 0, plays: playCount,
      ctaText: cta.text || "", ctaUrl: cta.url || "",
      ctaType: cta.type || "text", ctaTime: cta.time || "end",
      ctaClicks, ctaCtr: playCount > 0 ? Math.round(ctaClicks / playCount * 10000) / 100 : 0,
    });
    await new Promise(r => setTimeout(r, 200));
  }

  console.log(`Found ${ctaVideos.length} videos with CTAs`);
  ctaVideos.sort((a, b) => a.ctaCtr - b.ctaCtr);

  // 3. Personas
  const personaIds = [];
  for (const [name, desc] of [
    ["Impulse Clicker", "Clicks urgency/curiosity CTAs. Ignores 'learn more.' Mobile-first."],
    ["Deliberate Researcher", "Clicks only after full watch. CTA must match video promise. Values specificity."],
    ["Skeptical Executive", "Senior buyer. Only clicks for demos, calculators, exclusives. Ignores 'contact us.'"],
    ["Content Binger", "Watches 5+ videos. CTAs interrupt flow. Only clicks natural next-step CTAs."],
  ]) {
    const p = await fetch(`${MV_BASE}/personas`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({ name, description: desc }),
    }).then(r => r.json());
    personaIds.push(p.id);
    await new Promise(r => setTimeout(r, 300));
  }

  // 4. CTA summary
  const ctaBlock = ctaVideos.slice(0, 8).map(v =>
    `VIDEO: "${v.name}"\n  CTA: "${v.ctaText}" | ${v.ctaType} at ${v.ctaTime} | ` +
    `Plays: ${v.plays.toLocaleString()} | Clicks: ${v.ctaClicks} | CTR: ${v.ctaCtr}%`
  ).join("\n\n");

  // 5. Focus Group
  const fg = await fetch(`${MV_BASE}/focus-groups`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "CTA Performance Optimization", persona_ids: personaIds,
      questions: [
        `Review CTAs and CTRs. Would you click each? Why/why not?\n\n${ctaBlock}`,
        "Rewrite the 3 worst CTAs to maximize YOUR clicks. Explain changes.",
        "Best CTA placement: end, mid-video, overlay? Why does it matter to you?",
        "Best format: text button, image banner, or interactive annotation?",
        "If already interested, what CTA text converts you immediately?",
      ],
      responses_per_persona: 2,
    }),
  }).then(r => r.json());

  let fgData;
  for (let i = 0; i < 20; i++) {
    await new Promise(r => setTimeout(r, 5000));
    fgData = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then(r => r.json());
    if (fgData.status === "completed") break;
  }

  // 6. Output
  console.log("\nCTA PERFORMANCE × FOCUS GROUP");
  console.log("=".repeat(60));
  ctaVideos.slice(0, 8).forEach(v =>
    console.log(`  ${v.name.slice(0, 33).padEnd(35)} ${v.ctaText.slice(0, 23).padEnd(25)} ` +
      `${String(v.plays.toLocaleString()).padStart(7)} ${(v.ctaCtr + "%").padStart(7)}`)
  );

  console.log("\nFOCUS GROUP:");
  for (const resp of fgData.responses || []) {
    console.log(`\n[${resp.persona_name || "?"}] ${(resp.question || "").slice(0, 55)}...`);
    console.log(`  → ${(resp.answer || "").slice(0, 400)}`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Found 12 videos with CTAs

CTA PERFORMANCE × FOCUS GROUP
============================================================
Video                               CTA Text                   Plays     CTR
------------------------------------------------------------
  Product Demo — Dashboard           Learn More                  4,280   1.20%
  Customer Story — Acme Corp         Contact Sales               2,100   1.85%
  How-To: Getting Started            Start Free Trial            3,700   4.20%
  Pricing Explainer                  See Plans & Pricing         1,800   6.10%
  ROI Calculator Walkthrough         Calculate Your Savings      1,200   8.75%

FOCUS GROUP:

[Impulse Clicker] Review CTAs and CTRs. Would you click each?...
  → "Learn More" — NEVER clicking that. It's the "I surrender" of CTAs.
    It tells me nothing about what I'll learn. "Contact Sales" — only if
    I'm already sold, which I'm not after a demo. "Start Free Trial" —
    yes, because it's action + zero risk. "Calculate Your Savings" — YES,
    this is the best one. It promises a personalized result. I NEED to
    know my number.

[Deliberate Researcher] Rewrite the 3 worst CTAs...
  → 1. "Learn More" → "See the Dashboard in Action (2-min interactive demo)"
       Why: Specificity. I know exactly what happens when I click.
    2. "Contact Sales" → "Get a Custom Demo for [Your Industry]"
       Why: Personalization signals. I don't want generic sales, I want
       someone who understands my context.
    3. "Product Demo" CTA should be → "Try It Free — No Credit Card"
       Why: Removes friction. The word "free" plus removing the objection
       about payment makes this a no-brainer click.

[Content Binger] Best CTA placement...
  → Mid-video annotation at the moment you demonstrate the feature I care
    about. Post-roll CTAs are wasted on me — by the time the video ends,
    I'm already clicking the next video. If you catch me at the moment of
    peak interest (usually the "aha" demo moment), I'll click.
```

### Error Handling

<AccordionGroup>
  <Accordion title="CTA data in embed options">Wistia stores CTA configuration in the `embed_options` field of each media. The key name varies: `call_to_action`, `postRollCTA`, or `midrollLink`. Check all three fields for completeness.</Accordion>
  <Accordion title="Click tracking accuracy">Wistia tracks CTA clicks at the player level. If viewers use ad blockers or privacy browsers, clicks may be underreported by 10-15%. Use CTA CTR as a relative metric (for comparing CTAs) rather than an absolute conversion measure.</Accordion>
  <Accordion title="No CTA configured">Videos without CTAs return empty embed options. Filter these out before analysis. Consider recommending CTA addition for high-play, no-CTA videos as a quick win.</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="Heatmap-Informed Creative Optimization" icon="fire" href="/integrations/wistia/heatmap-optimization">
    Diagnose drop-off points with specific edit recommendations
  </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="Focus Groups API" icon="comments" href="/api-reference/focus-groups">
    Full reference for POST /api/v1/focus-groups
  </Card>
</CardGroup>
