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

Code

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

Example Output

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

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

What’s Next

Wistia Integration

Back to Wistia integration overview

Heatmap-Informed Creative Optimization

Diagnose drop-off points with specific edit recommendations

Wistia Embeds → Brand Voice Source

Build a brand voice profile from spoken content

Focus Groups API

Full reference for POST /api/v1/focus-groups