Skip to main content

Scenario

Your winning ads have high CTR and conversions, but you don’t know why they resonate with specific audience segments. You pull top-performing ad copy from Google Ads, then run the winning headlines and descriptions through a Mavera Focus Group with your target personas. The result tells you which messages land with economic buyers vs. end users vs. technical evaluators — so you can scale what works per segment.

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"]
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
MB = "https://app.mavera.io/api/v1"

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

query = """
    SELECT
        ad_group_ad.ad.responsive_search_ad.headlines,
        ad_group_ad.ad.responsive_search_ad.descriptions,
        ad_group_ad.ad.final_urls,
        metrics.impressions,
        metrics.clicks,
        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.conversions > 0
    ORDER BY metrics.conversions DESC
    LIMIT 10
"""

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

ads = []
for row in response:
    rsa = row.ad_group_ad.ad.responsive_search_ad
    headlines = [asset.text for asset in rsa.headlines]
    descriptions = [asset.text for asset in rsa.descriptions]
    ads.append({
        "headlines": headlines,
        "descriptions": descriptions,
        "impressions": row.metrics.impressions,
        "clicks": row.metrics.clicks,
        "conversions": row.metrics.conversions,
        "ctr": row.metrics.ctr,
    })

PERSONA_IDS = os.environ.get("FOCUS_GROUP_PERSONA_IDS", "").split(",")
if not PERSONA_IDS[0]:
    for name, desc in [
        ("SaaS CMO", "Chief Marketing Officer at a 200-person SaaS company. Budget-conscious, ROI-driven."),
        ("Growth Marketer", "Mid-level growth marketer. Hands-on, cares about tooling and workflow speed."),
        ("Enterprise IT Buyer", "IT Director evaluating tools for security, compliance, and integration depth."),
    ]:
        p = requests.post(f"{MB}/personas", headers=MH, json={"name": name, "description": desc}).json()
        PERSONA_IDS.append(p["id"])
        time.sleep(0.3)

ad_context = "\n\n".join(
    f"Ad {i+1} (CTR: {a['ctr']:.2%}, Conv: {a['conversions']:.0f}):\n"
    f"Headlines: {' | '.join(a['headlines'][:5])}\n"
    f"Descriptions: {' | '.join(a['descriptions'][:3])}"
    for i, a in enumerate(ads[:5])
)

fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
    "name": "Google Ads Copy Validation",
    "persona_ids": [pid for pid in PERSONA_IDS if pid],
    "questions": [
        "Which ad headline would make you click? Why?",
        "Which description feels most credible to you?",
        "What's missing from these ads that would make you convert?",
        "Rank the 5 ads from most to least compelling. Explain your #1 and #5.",
    ],
    "context": f"Here are 5 top-performing Google Ads from our account:\n\n{ad_context}",
    "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

for resp in data.get("responses", []):
    print(f"[{resp.get('persona_id','?')}] {resp.get('question','')[:50]}")
    print(f"  → {resp.get('answer','')[:250]}\n")

Example Output

[SaaS CMO] Which ad headline would make you click?
  → "Cut Reporting Time by 60%" — specific, measurable, addresses my #1 pain.
    The vague "Best Marketing Platform" tells me nothing.

[Growth Marketer] What's missing from these ads?
  → None mention a free trial or time-to-value. I need to know I can test
    this in 15 minutes, not sit through a demo.

[Enterprise IT Buyer] Rank the 5 ads
  → #1: "SOC 2 Certified Platform" — security-first language. #5: "Grow
    Revenue Fast" — no enterprise buyer trusts that kind of promise.

Error Handling

Not all ad types have responsive_search_ad. The query filters for ad.type = RESPONSIVE_SEARCH_AD. For expanded text ads (legacy), change the type filter and field paths.
Large groups (3+ personas × 4 questions × 2 responses each) can take 60–120s. The loop allows ~100s. Increase iterations for larger configs.

All Google Ads jobs

View all 7 Google Ads integration jobs

Focus Groups API

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