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")
const DEV_TOKEN = process.env.GOOGLE_ADS_DEVELOPER_TOKEN;
const ACCESS_TOKEN = process.env.GOOGLE_ADS_ACCESS_TOKEN;
const CUSTOMER_ID = process.env.GOOGLE_ADS_CUSTOMER_ID;
const MV = process.env.MAVERA_API_KEY;
const MB = "https://app.mavera.io/api/v1";
const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const gaql = `
SELECT
ad_group_ad.ad.responsive_search_ad.headlines,
ad_group_ad.ad.responsive_search_ad.descriptions,
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`;
const gaRes = await fetch(
`https://googleads.googleapis.com/v23/customers/${CUSTOMER_ID}/googleAds:searchStream`,
{
method: "POST",
headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, "developer-token": DEV_TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({ query: gaql }),
}
).then((r) => r.json());
const ads = (gaRes[0]?.results || []).map((row) => ({
headlines: (row.adGroupAd.ad.responsiveSearchAd.headlines || []).map((h) => h.text),
descriptions: (row.adGroupAd.ad.responsiveSearchAd.descriptions || []).map((d) => d.text),
impressions: parseInt(row.metrics.impressions),
clicks: parseInt(row.metrics.clicks),
conversions: parseFloat(row.metrics.conversions),
ctr: parseFloat(row.metrics.ctr),
}));
const personaIds = [];
for (const [name, desc] of [
["SaaS CMO", "CMO at 200-person SaaS. Budget-conscious, ROI-driven."],
["Growth Marketer", "Mid-level growth marketer. Hands-on, cares about speed."],
["Enterprise IT Buyer", "IT Director. Security, compliance, integration."],
]) {
const p = await fetch(`${MB}/personas`, {
method: "POST", headers: MH,
body: JSON.stringify({ name, description: desc }),
}).then((r) => r.json());
personaIds.push(p.id);
await new Promise((r) => setTimeout(r, 300));
}
const adContext = ads.slice(0, 5).map((a, i) =>
`Ad ${i + 1} (CTR: ${(a.ctr * 100).toFixed(1)}%, Conv: ${a.conversions.toFixed(0)}):\nHeadlines: ${a.headlines.slice(0, 5).join(" | ")}\nDescriptions: ${a.descriptions.slice(0, 3).join(" | ")}`
).join("\n\n");
const fg = await fetch(`${MB}/focus-groups`, {
method: "POST", headers: MH,
body: JSON.stringify({
name: "Google Ads Copy Validation",
persona_ids: personaIds,
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.",
],
context: `Top-performing Google Ads:\n\n${adContext}`,
responses_per_persona: 2,
}),
}).then((r) => r.json());
let data;
for (let i = 0; i < 20; i++) {
await new Promise((r) => setTimeout(r, 5000));
data = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: MH }).then((r) => r.json());
if (data.status === "completed") break;
}
for (const resp of data.responses || []) {
console.log(`[${resp.persona_id}] ${(resp.question || "").slice(0, 50)}`);
console.log(` → ${(resp.answer || "").slice(0, 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
RSA assets missing
RSA assets missing
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.Focus Group polling timeout
Focus Group polling timeout
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