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

# Ad Copy Performance × Persona Validation

> Pull top-performing Google Ads copy and validate it through a Mavera Focus Group to learn which messages resonate with each persona.

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

```mermaid theme={"dark"}
flowchart LR
    A["Google Ads GAQL (ad_group_ad)"] --> B["Top ads by conversions/CTR"] --> C["POST /api/v1/focus-groups"] --> D[Persona-scored copy insights]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  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")
  ```

  ```javascript JavaScript theme={"dark"}
  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`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
[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

<AccordionGroup>
  <Accordion title="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.</Accordion>
  <Accordion title="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.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="All Google Ads jobs" icon="rectangle-history" href="/integrations/google-ads">
    View all 7 Google Ads integration jobs
  </Card>

  <Card title="Focus Groups API" icon="comments" href="/api-reference/focus-groups">
    Full reference for POST /api/v1/focus-groups
  </Card>
</CardGroup>
