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

Google assembles RSA headlines and descriptions dynamically, but you only see aggregate performance — not which combinations resonate with which audiences. You pull individual RSA asset performance ratings, extract the top 5 headline/description combos, then run them through a Mavera Focus Group. The result tells you which combinations to pin and which to replace, broken down by persona.

Architecture

Code

import os, requests, time
from google.ads.googleads.client import GoogleAdsClient

MV = os.environ["MAVERA_API_KEY"]
CUSTOMER_ID = os.environ["GOOGLE_ADS_CUSTOMER_ID"]
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

client = GoogleAdsClient.load_from_env()
ga_service = client.get_service("GoogleAdsService")

query = """
    SELECT
        ad_group_ad.ad.id,
        ad_group_ad.ad.responsive_search_ad.headlines,
        ad_group_ad.ad.responsive_search_ad.descriptions,
        metrics.impressions,
        metrics.conversions,
        metrics.ctr
    FROM ad_group_ad
    WHERE ad_group_ad.ad.type = RESPONSIVE_SEARCH_AD
        AND segments.date DURING LAST_30_DAYS
        AND metrics.impressions > 500
    ORDER BY metrics.impressions DESC
    LIMIT 5
"""

response = ga_service.search(customer_id=CUSTOMER_ID, query=query)

combos = []
for row in response:
    rsa = row.ad_group_ad.ad.responsive_search_ad
    best_headlines = [
        a.text for a in rsa.headlines
        if a.performance_label and a.performance_label.name in ("BEST", "GOOD")
    ][:3] or [a.text for a in rsa.headlines[:3]]

    best_descs = [
        a.text for a in rsa.descriptions
        if a.performance_label and a.performance_label.name in ("BEST", "GOOD")
    ][:2] or [a.text for a in rsa.descriptions[:2]]

    combos.append({
        "ad_id": row.ad_group_ad.ad.id,
        "headlines": best_headlines,
        "descriptions": best_descs,
        "impressions": row.metrics.impressions,
        "ctr": row.metrics.ctr,
        "conversions": row.metrics.conversions,
        "all_headlines": [{"text": a.text, "perf": a.performance_label.name if a.performance_label else "UNKNOWN"} for a in rsa.headlines],
    })

PERSONA_IDS = os.environ.get("RSA_PERSONA_IDS", "").split(",")
if not PERSONA_IDS[0]:
    for name, desc in [
        ("Startup Founder", "CEO/Founder of a 10-50 person startup. Time-poor, skeptical of marketing claims."),
        ("Marketing Director", "Director of Marketing at mid-market B2B. Manages team of 5-10. Accountable for pipeline."),
        ("Procurement Manager", "Procurement lead at enterprise. Risk-averse, process-oriented, price-sensitive."),
    ]:
        p = requests.post(f"{MB}/personas", headers=MH, json={"name": name, "description": desc}).json()
        PERSONA_IDS.append(p["id"])
        time.sleep(0.3)

combo_block = "\n\n".join(
    f"Combo {i+1} (CTR: {c['ctr']:.2%}, Conv: {c['conversions']:.0f}):\n"
    f"  Headlines: {' | '.join(c['headlines'])}\n"
    f"  Descriptions: {' | '.join(c['descriptions'])}"
    for i, c in enumerate(combos)
)

fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
    "name": "RSA Headline/Description Testing",
    "persona_ids": [pid for pid in PERSONA_IDS if pid],
    "questions": [
        f"Here are 5 ad combinations:\n\n{combo_block}\n\nRank them 1-5 and explain your top pick.",
        "Which headline would stop your scroll? What makes it stand out?",
        "Which description would convince you to click? Why?",
        "Rewrite the weakest headline to make it compelling for someone like you.",
    ],
    "context": "These are Google Search ads for a B2B marketing platform.",
    "responses_per_persona": 2,
}).json()

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

print(f"Focus Group: {data.get('id')}")
for resp in data.get("responses", []):
    print(f"\n[{resp.get('persona_id','?')}] {resp.get('question','')[:60]}")
    print(f"  → {resp.get('answer','')[:300]}")

print("\n--- Asset Performance Summary ---")
for c in combos:
    for h in c["all_headlines"]:
        print(f"  [{h['perf']:8s}] {h['text']}")

Example Output

[Startup Founder] Rank these ad combos 1-5
  → #1: "Launch in 15 Minutes — No Demo Required" — I don't have time for
    demos. Self-serve or I'm gone. #5: "Enterprise-Grade Marketing Platform"
    — I'm not enterprise. That language pushes me away.

[Procurement Manager] Which description convinces you to click?
  → "SOC 2 certified. No annual commitment." — Addresses my two biggest
    concerns in 6 words. The ones with percentage claims feel unverifiable.

--- Asset Performance ---
  [BEST    ] Launch in 15 Minutes
  [GOOD    ] Cut Reporting Time 60%
  [LOW     ] Best Marketing Platform
  [UNKNOWN ] Enterprise-Grade Solution

Error Handling

Asset performance labels (BEST, GOOD, LOW) require ~5,000 impressions per ad. New ads show UNKNOWN or LEARNING. Filter for ads with sufficient volume.
Pinned headlines always appear in position 1/2/3, inflating their performance. Note pinning status (pinned_field) when interpreting results.

All Google Ads jobs

View all 7 Google Ads integration jobs

Focus Groups API

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