> ## 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 A/B with Focus Groups

> Pull ad copy variants from Meta, run a Mavera Focus Group with target personas, and get pre-launch headline ratings and click intent signals

### Scenario

You've written 4 headline variants for a new campaign and need to know which one drives clicks before spending budget on live A/B tests. This job pulls existing ad copy variants (or you provide new ones), creates a Focus Group with your target personas, and asks them to rate each headline 1–10 and answer "Would you click?" The result is a pre-launch signal on which copy to promote.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Meta GET ads (copy variants)"] --> B[Extract headline + body] --> C["POST /api/v1/focus-groups"] --> D[Per-persona ratings + click intent]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  META = os.environ["META_ACCESS_TOKEN"]
  ACCT = os.environ["META_AD_ACCOUNT_ID"]
  MV = os.environ["MAVERA_API_KEY"]
  GRAPH = "https://graph.facebook.com/v24.0"
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  # 1. Pull ad copy variants from the same campaign
  CAMPAIGN_ID = os.environ.get("META_CAMPAIGN_ID", "")
  if CAMPAIGN_ID:
      ads = requests.get(
          f"{GRAPH}/{CAMPAIGN_ID}/ads",
          params={
              "access_token": META,
              "fields": "id,name,creative{title,body,link_description,call_to_action_type}",
              "limit": 20,
          },
      ).json().get("data", [])
  else:
      ads = requests.get(
          f"{GRAPH}/{ACCT}/ads",
          params={
              "access_token": META,
              "fields": "id,name,creative{title,body,link_description,call_to_action_type}",
              "effective_status": '["ACTIVE","PAUSED"]',
              "limit": 20,
          },
      ).json().get("data", [])

  variants = []
  for ad in ads:
      c = ad.get("creative", {})
      if c.get("title") or c.get("body"):
          variants.append({
              "ad_id": ad["id"],
              "ad_name": ad.get("name", "Untitled"),
              "headline": c.get("title", ""),
              "body": c.get("body", ""),
              "cta": c.get("call_to_action_type", "LEARN_MORE"),
          })

  variants = variants[:6]
  print(f"Testing {len(variants)} ad copy variants")

  # 2. Build Focus Group questions
  variant_block = "\n\n".join(
      f"**Variant {i+1}: \"{v['headline']}\"**\n{v['body'][:200]}\nCTA: {v['cta']}"
      for i, v in enumerate(variants)
  )

  PERSONA_IDS = os.environ.get("PERSONA_IDS", "").split(",")
  if not PERSONA_IDS[0]:
      personas = requests.get(f"{MB}/personas", headers=MH).json()
      PERSONA_IDS = [p["id"] for p in (personas if isinstance(personas, list) else [])[:4]]

  questions = []
  for i, v in enumerate(variants):
      questions.append(f"Rate this headline 1-10 for how much it makes you want to learn more: \"{v['headline']}\"")
      questions.append(f"Would you click on this ad? (Yes/No): \"{v['headline']}\" — {v['body'][:100]}")

  questions.extend([
      "Which variant is the most compelling overall? Give the variant number and explain why.",
      "What would make you scroll past all of these? What's missing?",
  ])

  # 3. Run Focus Group
  fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
      "name": "Meta Ad Copy A/B Test",
      "persona_ids": PERSONA_IDS,
      "questions": questions,
      "context": f"You are evaluating ad copy variants for a social media campaign. Here are the variants:\n\n{variant_block}",
      "responses_per_persona": 2,
  }).json()

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

  # 5. Tabulate scores
  scores = {i: {"ratings": [], "clicks": []} for i in range(len(variants))}
  for resp in data.get("responses", []):
      q = resp.get("question", "")
      a = resp.get("answer", "")
      for i in range(len(variants)):
          if variants[i]["headline"] in q:
              if "Rate" in q:
                  try:
                      score = int("".join(c for c in a if c.isdigit())[:2])
                      if 1 <= score <= 10:
                          scores[i]["ratings"].append(score)
                  except ValueError:
                      pass
              elif "click" in q.lower():
                  scores[i]["clicks"].append(1 if a.lower().startswith("yes") else 0)

  print("\n=== Ad Copy A/B Results ===")
  for i, v in enumerate(variants):
      s = scores[i]
      avg_rating = sum(s["ratings"]) / max(len(s["ratings"]), 1)
      click_rate = sum(s["clicks"]) / max(len(s["clicks"]), 1)
      print(f"Variant {i+1}: \"{v['headline']}\"")
      print(f"  Rating: {avg_rating:.1f}/10 | Click intent: {click_rate:.0%}")
  ```

  ```javascript JavaScript theme={"dark"}
  const META = process.env.META_ACCESS_TOKEN;
  const ACCT = process.env.META_AD_ACCOUNT_ID;
  const MV = process.env.MAVERA_API_KEY;
  const GRAPH = "https://graph.facebook.com/v24.0";
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  // 1. Pull ad copy variants
  const CAMPAIGN_ID = process.env.META_CAMPAIGN_ID || "";
  const adsUrl = CAMPAIGN_ID
    ? `${GRAPH}/${CAMPAIGN_ID}/ads?access_token=${META}&fields=id,name,creative{title,body,link_description,call_to_action_type}&limit=20`
    : `${GRAPH}/${ACCT}/ads?access_token=${META}&fields=id,name,creative{title,body,link_description,call_to_action_type}&effective_status=["ACTIVE","PAUSED"]&limit=20`;

  const ads = await fetch(adsUrl).then(r => r.json()).then(d => d.data || []);
  const variants = ads
    .filter(a => a.creative?.title || a.creative?.body)
    .map(a => ({
      ad_id: a.id, ad_name: a.name || "Untitled",
      headline: a.creative.title || "", body: a.creative.body || "",
      cta: a.creative.call_to_action_type || "LEARN_MORE",
    })).slice(0, 6);

  console.log(`Testing ${variants.length} ad copy variants`);

  // 2. Build questions
  const variantBlock = variants.map((v, i) =>
    `**Variant ${i+1}: "${v.headline}"**\n${v.body.slice(0, 200)}\nCTA: ${v.cta}`
  ).join("\n\n");

  let personaIds = (process.env.PERSONA_IDS || "").split(",").filter(Boolean);
  if (!personaIds.length) {
    const personas = await fetch(`${MB}/personas`, { headers: MH }).then(r => r.json());
    personaIds = (Array.isArray(personas) ? personas : []).slice(0, 4).map(p => p.id);
  }

  const questions = [];
  for (const v of variants) {
    questions.push(`Rate this headline 1-10: "${v.headline}"`);
    questions.push(`Would you click? (Yes/No): "${v.headline}" — ${v.body.slice(0, 100)}`);
  }
  questions.push("Which variant is most compelling? Give the number and why.");
  questions.push("What would make you scroll past all of these?");

  // 3. Run Focus Group
  const fg = await fetch(`${MB}/focus-groups`, {
    method: "POST", headers: MH,
    body: JSON.stringify({
      name: "Meta Ad Copy A/B Test", persona_ids: personaIds,
      questions, context: `Evaluating ad copy:\n\n${variantBlock}`,
      responses_per_persona: 2,
    }),
  }).then(r => r.json());

  // 4. Poll
  let data;
  for (let i = 0; i < 24; 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;
  }

  // 5. Tabulate
  const scores = variants.map(() => ({ ratings: [], clicks: [] }));
  for (const resp of data.responses || []) {
    variants.forEach((v, i) => {
      if (resp.question?.includes(v.headline)) {
        if (resp.question.includes("Rate")) {
          const num = parseInt((resp.answer || "").match(/\d+/)?.[0] || "0");
          if (num >= 1 && num <= 10) scores[i].ratings.push(num);
        } else if (resp.question.toLowerCase().includes("click")) {
          scores[i].clicks.push(resp.answer?.toLowerCase().startsWith("yes") ? 1 : 0);
        }
      }
    });
  }

  console.log("\n=== Ad Copy A/B Results ===");
  variants.forEach((v, i) => {
    const avg = scores[i].ratings.reduce((a, b) => a + b, 0) / Math.max(scores[i].ratings.length, 1);
    const clickRate = scores[i].clicks.reduce((a, b) => a + b, 0) / Math.max(scores[i].clicks.length, 1);
    console.log(`Variant ${i+1}: "${v.headline}"\n  Rating: ${avg.toFixed(1)}/10 | Click intent: ${(clickRate * 100).toFixed(0)}%`);
  });
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
=== Ad Copy A/B Results ===
Variant 1: "Stop Guessing. Start Growing."
  Rating: 7.8/10 | Click intent: 75%
Variant 2: "Your Competitors Already Know This"
  Rating: 8.4/10 | Click intent: 88%
Variant 3: "Save 10 Hours a Week on Reporting"
  Rating: 6.9/10 | Click intent: 63%
Variant 4: "Built for Teams That Ship Fast"
  Rating: 7.2/10 | Click intent: 50%

Winner: Variant 2 — curiosity gap + competitive framing
  "Makes me wonder what I'm missing. I'd click to find out." — Meta Female 25-34
  "The competitive angle creates urgency." — Meta Male 35-44
```

### Error Handling

<AccordionGroup>
  <Accordion title="No creative title field">Some ad formats (Dynamic Creative, Catalog) don't have a top-level `title`. Check `object_story_spec.link_data.message` for the primary text instead.</Accordion>
  <Accordion title="Focus Group timeout on many variants">6 variants × 2 questions × 4 personas = 48 responses. Allow 2+ minutes for polling. Reduce `responses_per_persona` if speed matters more than depth.</Accordion>
  <Accordion title="Score parsing from open-ended answers">Personas respond naturally ("I'd give it an 8"). The regex extraction handles most formats but may miss edge cases like "eight out of ten".</Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Meta Ads Integration" icon="meta" href="/integrations/meta-ads">
    All Meta Ads jobs
  </Card>

  <Card title="Focus Groups" icon="users" href="/features/focus-groups">
    Full reference for synthetic focus groups
  </Card>
</CardGroup>
