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

# Product Affinity → Focus Group

> Pull Klaviyo catalog and purchase data, then run a Mavera Focus Group with Likert and NPS questions to validate cross-sell hypotheses before building flows

### Scenario

Your Klaviyo account tracks what customers buy. You want to test cross-sell hypotheses — if someone bought Product A, would they buy Product B? You pull purchase data and catalog information, then run a Mavera Focus Group with product-context questions using Likert scale and NPS format. The result is validated cross-sell opportunities before you build the flow.

### Architecture

```mermaid theme={"dark"}
flowchart LR
A["Klaviyo catalog + purchase events"] --> B["Identify product pair affinity"] --> C["POST /api/v1/personas"] --> D["POST /api/v1/focus-groups"] --> E["Cross-sell validation"]
```

### Code

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

  KL_KEY = os.environ["KLAVIYO_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
  KH = {
      "Authorization": f"Klaviyo-API-Key {KL_KEY}",
      "Content-Type": "application/json",
      "revision": "2024-10-15",
  }

  r = requests.get("https://a.klaviyo.com/api/catalog-items", headers=KH, params={
      "fields[catalog-item]": "title,description,url,price",
      "page[size]": 50,
  })

  products = []
  if r.status_code == 200:
      products = r.json().get("data", [])

  if not products:
      products = [
          {"id": "prod_001", "attributes": {"title": "Running Shoes Pro", "price": 129, "description": "High-performance running shoes for daily training"}},
          {"id": "prod_002", "attributes": {"title": "Performance Socks 3-Pack", "price": 24, "description": "Moisture-wicking athletic socks"}},
          {"id": "prod_003", "attributes": {"title": "GPS Running Watch", "price": 299, "description": "Multi-sport GPS watch with heart rate monitoring"}},
          {"id": "prod_004", "attributes": {"title": "Hydration Belt", "price": 35, "description": "Hands-free running hydration system"}},
          {"id": "prod_005", "attributes": {"title": "Recovery Foam Roller", "price": 45, "description": "Deep-tissue massage foam roller"}},
          {"id": "prod_006", "attributes": {"title": "Training Plan (Digital)", "price": 19, "description": "12-week progressive running program"}},
      ]

  PRODUCT_PAIRS = [
      {"bought": "Running Shoes Pro", "suggest": "Performance Socks 3-Pack", "logic": "Complementary — socks protect the shoe investment"},
      {"bought": "GPS Running Watch", "suggest": "Training Plan (Digital)", "logic": "Watch + plan = structured training"},
      {"bought": "Running Shoes Pro", "suggest": "Recovery Foam Roller", "logic": "Runners who invest in shoes care about recovery"},
      {"bought": "Hydration Belt", "suggest": "GPS Running Watch", "logic": "Hydration belt = long runs = watch needed"},
  ]

  personas = []
  for name, desc in [
      ("Casual Runner", "Runs 2-3x per week for fitness. Buys basics, price-conscious. Doesn't consider themselves 'a runner' — it's just exercise."),
      ("Dedicated Runner", "Runs 4-5x per week, tracks every metric. Invests in gear. Training for a half marathon. Reads running blogs."),
      ("New Runner", "Started running 2 months ago. Bought first pair of real running shoes. Overwhelmed by options. Wants guidance."),
  ]:
      p = requests.post(f"{MB}/personas", headers=MH, json={"name": name, "description": desc}).json()
      personas.append(p)
      time.sleep(0.3)

  product_catalog = "\n".join(
      f"- {p.get('attributes', p).get('title', 'Unknown')}: ${p.get('attributes', p).get('price', 0)} — {p.get('attributes', p).get('description', '')}"
      for p in products
  )

  pair_questions = []
  for pair in PRODUCT_PAIRS:
      pair_questions.append(
          f"You recently bought '{pair['bought']}'. On a scale of 1-5 (1=No interest, 5=Would buy immediately), "
          f"how likely are you to buy '{pair['suggest']}'? "
          f"Explain your rating — what would make you more or less likely?"
      )

  pair_questions.append(
      "On a scale of 0-10, how likely are you to recommend our store to a friend? "
      "What's the #1 thing we'd need to do to move your score up by 2 points?"
  )

  fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
      "name": "Klaviyo: Product Affinity Cross-Sell Study",
      "persona_ids": [p["id"] for p in personas],
      "questions": pair_questions,
      "context": f"""Product affinity study for an e-commerce running gear store.

  PRODUCT CATALOG:
  {product_catalog}

  CROSS-SELL HYPOTHESES:
  {chr(10).join(f"- {p['bought']} → {p['suggest']}: {p['logic']}" for p in PRODUCT_PAIRS)}

  Each persona represents a different buyer segment. We want to validate which cross-sell pairs resonate with each segment before building automated flows.""",
      "responses_per_persona": 2,
  }).json()

  for _ in range(30):
      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')} — {data.get('status')}\n")
  for resp in data.get("responses", []):
      print(f"[{resp.get('persona_id','?')}] {resp.get('question','')[:80]}")
      print(f"  → {resp.get('answer','')[:300]}\n")
  ```

  ```javascript JavaScript theme={"dark"}
  const KL_KEY = process.env.KLAVIYO_API_KEY;
  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 KH = { Authorization: `Klaviyo-API-Key ${KL_KEY}`, "Content-Type": "application/json", revision: "2024-10-15" };

  let products = [];
  const catRes = await fetch("https://a.klaviyo.com/api/catalog-items?fields[catalog-item]=title,description,price&page[size]=50", { headers: KH });
  if (catRes.ok) products = (await catRes.json()).data || [];
  if (!products.length) {
    products = [
      { attributes: { title: "Running Shoes Pro", price: 129, description: "High-performance running shoes" } },
      { attributes: { title: "Performance Socks 3-Pack", price: 24, description: "Moisture-wicking socks" } },
      { attributes: { title: "GPS Running Watch", price: 299, description: "Multi-sport GPS watch" } },
      { attributes: { title: "Hydration Belt", price: 35, description: "Hands-free hydration" } },
      { attributes: { title: "Recovery Foam Roller", price: 45, description: "Deep-tissue roller" } },
      { attributes: { title: "Training Plan (Digital)", price: 19, description: "12-week running program" } },
    ];
  }

  const PAIRS = [
    { bought: "Running Shoes Pro", suggest: "Performance Socks 3-Pack" },
    { bought: "GPS Running Watch", suggest: "Training Plan (Digital)" },
    { bought: "Running Shoes Pro", suggest: "Recovery Foam Roller" },
    { bought: "Hydration Belt", suggest: "GPS Running Watch" },
  ];

  const personas = [];
  for (const [name, desc] of [
    ["Casual Runner", "Runs 2-3x/week for fitness. Price-conscious. Just exercise."],
    ["Dedicated Runner", "4-5x/week, tracks metrics, training for half marathon."],
    ["New Runner", "Started 2 months ago. First real shoes. Overwhelmed."],
  ]) {
    const p = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
      body: JSON.stringify({ name, description: desc }) }).then((r) => r.json());
    personas.push(p);
    await new Promise((r) => setTimeout(r, 300));
  }

  const catalog = products.map((p) => `- ${p.attributes.title}: $${p.attributes.price} — ${p.attributes.description}`).join("\n");
  const questions = PAIRS.map((p) =>
    `You bought '${p.bought}'. Rate 1-5 (1=No interest, 5=Buy immediately) likelihood to buy '${p.suggest}'. Explain your rating.`
  );
  questions.push("NPS 0-10: recommend our store to a friend? What moves your score up 2 points?");

  const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: MH,
    body: JSON.stringify({
      name: "Klaviyo: Product Affinity Study",
      persona_ids: personas.map((p) => p.id),
      questions,
      context: `Running gear store cross-sell study.\n\nCatalog:\n${catalog}\n\nValidating cross-sell pairs before building flows.`,
      responses_per_persona: 2,
    }),
  }).then((r) => r.json());

  let data;
  for (let i = 0; i < 30; 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;
  }

  console.log(`Focus Group: ${data.id} — ${data.status}\n`);
  for (const resp of data.responses || []) {
    console.log(`[${resp.persona_id}] ${(resp.question || "").slice(0, 80)}`);
    console.log(`  → ${(resp.answer || "").slice(0, 300)}\n`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Focus Group: fg_kl_affinity_8m3n — completed

[Casual Runner] Shoes → Socks (1-5 scale)
  → 4/5. Actually yeah — I just spent $129 on shoes, and my old
    cotton socks are going to ruin them. At $24 for 3 pairs, it's
    a no-brainer add-on. BUT only if you show me this within 2
    days of my shoe purchase. After a week, I've already grabbed
    socks at Target.

[Dedicated Runner] Watch → Training Plan
  → 5/5. I bought the watch specifically to follow structured
    training. A $19 plan that tells my watch what workout to do
    today? That's the missing piece. This should be offered at
    checkout, not after.

[New Runner] Shoes → Foam Roller
  → 2/5. I don't know what a foam roller is for. I'm too new to
    think about recovery — I'm still trying to run a mile without
    stopping. Sell me the roller in month 3, not month 1.

[Casual Runner] NPS
  → 7/10. Good products, fair prices. To get me to 9: show me a
    "starter bundle" with shoes + socks + a beginner guide at 15%
    off. I hate shopping piece by piece.
```

### Error Handling

<AccordionGroup>
  <Accordion title="Catalog API availability">The Catalog Items API (`/api/catalog-items`) requires catalog sync to be configured in Klaviyo. If not set up, the endpoint returns empty. The code includes fallback sample data.</Accordion>
  <Accordion title="Purchase event data">For production cross-sell analysis, query Klaviyo's Events API filtered by `Placed Order` events to find actual co-purchase patterns before hypothesizing pairs.</Accordion>
  <Accordion title="Likert scale interpretation">Focus Group responses include both the numeric rating and explanation. Parse the numeric value (e.g. "4/5") and the qualitative reasoning separately for analysis.</Accordion>
</AccordionGroup>

***

## What's Next

<CardGroup cols={2}>
  <Card title="Klaviyo Integration" icon="envelope" href="/integrations/klaviyo">
    Back to Klaviyo integration overview
  </Card>

  <Card title="Segment Overlap Analysis" icon="circle-nodes" href="/integrations/klaviyo/segment-overlap-analysis">
    Simplified segmentation recommendations
  </Card>

  <Card title="SMS vs. Email Creative Testing" icon="mobile-screen" href="/integrations/klaviyo/sms-vs-email-testing">
    Channel preference insights
  </Card>

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