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

# Enterprise Survey → Persona Library

> Export Qualtrics enterprise survey responses and build a comprehensive Mavera persona library from thousands of real answers

### Scenario

Your Qualtrics survey has 5,000+ responses across 40 questions — demographics, psychographics, behavioral data, and open-ended feedback. This is the richest persona-building dataset in your organization, but it sits in a Qualtrics dashboard that only the research team accesses. This job exports the full response dataset using Qualtrics' async export flow (create export → poll for completion → download CSV), sends it to Mave Agent for segment discovery, then creates a comprehensive persona library in Mavera. The result is an enterprise-grade persona set grounded in thousands of real survey responses.

**Flow:** Qualtrics async export (`POST /surveys/{id}/export-responses` → `GET .../export-responses/{exportId}` → Download) → Parse CSV → Mave segment discovery → `POST /api/v1/personas` per segment → Comprehensive persona library

### Architecture

```mermaid theme={"dark"}
flowchart LR
A["POST export-responses"] --> B["Poll until complete"] --> C["Download ZIP + parse CSV"] --> D["Mave Agent: Segment respondents"] --> E["POST /api/v1/personas"] --> F["Enterprise persona library"]
```

### Code

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

  QT = os.environ["QUALTRICS_TOKEN"]
  DC = os.environ["QUALTRICS_DC"]
  MV = os.environ["MAVERA_API_KEY"]
  Q_BASE = f"https://{DC}.qualtrics.com/API/v3"
  MB = "https://app.mavera.io/api/v1"
  Q_H = {"X-API-TOKEN": QT, "Content-Type": "application/json"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  SURVEY_ID = os.environ.get("QUALTRICS_SURVEY_ID", "SV_xxxxx")

  # 1. Create response export
  export = requests.post(f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses",
      headers=Q_H, json={"format": "csv"}).json()
  progress_id = export.get("result", {}).get("progressId")
  print(f"Export started: {progress_id}")

  # 2. Poll until complete
  file_id = None
  for attempt in range(60):
      time.sleep(5)
      status = requests.get(
          f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses/{progress_id}",
          headers=Q_H).json()
      pct = status.get("result", {}).get("percentComplete", 0)
      print(f"  Progress: {pct}% (attempt {attempt + 1})")

      if pct == 100:
          file_id = status["result"].get("fileId")
          break
      if status.get("result", {}).get("status") == "failed":
          print(f"Export failed: {status}")
          exit()

  if not file_id:
      print("Export timed out")
      exit()

  # 3. Download and parse
  zip_data = requests.get(
      f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses/{file_id}/file",
      headers=Q_H).content

  with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
      csv_name = [n for n in zf.namelist() if n.endswith(".csv")][0]
      with zf.open(csv_name) as f:
          reader = csv.DictReader(io.TextIOWrapper(f, encoding="utf-8-sig"))
          rows = list(reader)

  # Filter out header rows (Qualtrics includes 2 extra header rows)
  responses = [r for r in rows if r.get("Status", "") not in ("", "Response Type")]
  responses = [r for r in responses if r.get("Finished", "1") == "1"]
  print(f"Parsed {len(responses)} completed responses")

  # 4. Build analysis summary
  # Identify key columns (skip metadata columns)
  meta_cols = {"StartDate", "EndDate", "Status", "IPAddress", "Progress",
               "Duration (in seconds)", "Finished", "RecordedDate",
               "ResponseId", "RecipientLastName", "RecipientFirstName",
               "RecipientEmail", "ExternalReference", "LocationLatitude",
               "LocationLongitude", "DistributionChannel", "UserLanguage"}
  data_cols = [c for c in responses[0].keys() if c not in meta_cols][:30]

  col_summaries = {}
  for col in data_cols:
      values = [r.get(col, "").strip() for r in responses if r.get(col, "").strip()]
      if not values:
          continue
      unique = set(values)
      if len(unique) <= 25:
          counts = Counter(values).most_common(10)
          col_summaries[col] = f"n={len(values)} | " + ", ".join(
              f"{v}: {c} ({c/len(values)*100:.0f}%)" for v, c in counts)
      else:
          col_summaries[col] = f"n={len(values)} | Samples: {'; '.join(list(values)[:8])}"

  summary = "\n".join(f"**{k}**: {v}" for k, v in col_summaries.items())

  # 5. Mave segment discovery
  segments = requests.post(f"{MB}/mave/chat", headers=MV_H, json={
      "message": f"""Analyze {len(responses)} enterprise survey responses.

  COLUMN SUMMARIES:
  {summary[:6000]}

  Tasks:
  1) Identify 5-8 distinct audience segments from answer patterns
  2) For each: name, % of audience, defining characteristics, pain points,
     goals, preferred communication style, buying triggers
  3) Rate confidence (low/medium/high) for each segment
  4) Note cross-segment patterns and tensions
  5) Suggest segment-specific messaging angles

  Return structured JSON with a "segments" array."""
  }).json()

  content = segments.get("content", "")
  print("=== Segment Discovery ===")
  print(content[:2000])

  # 6. Create persona library
  try:
      json_start = content.find("[")
      json_end = content.rfind("]") + 1
      if json_start >= 0 and json_end > json_start:
          parsed = json.loads(content[json_start:json_end])
      else:
          parsed_obj = json.loads(
              content[content.find("{"):content.rfind("}")+1])
          parsed = parsed_obj.get("segments", [])
  except (json.JSONDecodeError, ValueError):
      parsed = []

  personas = []
  for seg in parsed:
      name = seg.get("name", "Unknown Segment")
      pct = seg.get("percentage", seg.get("size_percent", "?"))
      confidence = seg.get("confidence", "medium")

      r = requests.post(f"{MB}/personas", headers=MV_H, json={
          "name": f"QX: {name}",
          "description": (
              f"Enterprise survey segment ({len(responses)} respondents). "
              f"Est. {pct}% of audience. Confidence: {confidence}. "
              f"{seg.get('characteristics', seg.get('description', ''))}"
          ),
          "demographic": seg.get("demographic", {}),
          "psychographic": {
              "pain_points": seg.get("pain_points", []),
              "goals": seg.get("goals", []),
              "buying_triggers": seg.get("buying_triggers", []),
              "communication_style": seg.get("communication_style", ""),
          },
      })
      r.raise_for_status()
      personas.append({"name": name, "id": r.json()["id"], "pct": pct,
                       "confidence": confidence})
      print(f"Created: {name} ({pct}%, {confidence}) → {r.json()['id']}")
      time.sleep(0.3)

  print(f"\nPersona library: {len(personas)} segments from {len(responses)} responses")
  ```

  ```javascript JavaScript theme={"dark"}
  import JSZip from "jszip";

  const QT = process.env.QUALTRICS_TOKEN;
  const DC = process.env.QUALTRICS_DC;
  const MV = process.env.MAVERA_API_KEY;
  const Q_BASE = `https://${DC}.qualtrics.com/API/v3`;
  const MB = "https://app.mavera.io/api/v1";
  const qH = { "X-API-TOKEN": QT, "Content-Type": "application/json" };
  const mvH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const SURVEY_ID = process.env.QUALTRICS_SURVEY_ID || "SV_xxxxx";

  // 1. Create export
  const exp = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses`, {
    method: "POST", headers: qH,
    body: JSON.stringify({ format: "csv" }),
  }).then(r => r.json());
  const progressId = exp.result?.progressId;
  console.log(`Export started: ${progressId}`);

  // 2. Poll
  let fileId = null;
  for (let i = 0; i < 60; i++) {
    await new Promise(r => setTimeout(r, 5000));
    const status = await fetch(
      `${Q_BASE}/surveys/${SURVEY_ID}/export-responses/${progressId}`,
      { headers: qH }).then(r => r.json());
    const pct = status.result?.percentComplete || 0;
    console.log(`  Progress: ${pct}% (attempt ${i + 1})`);
    if (pct === 100) { fileId = status.result.fileId; break; }
    if (status.result?.status === "failed") { console.log("Failed"); process.exit(1); }
  }
  if (!fileId) { console.log("Timed out"); process.exit(1); }

  // 3. Download and parse CSV from ZIP
  const zipBuf = await fetch(
    `${Q_BASE}/surveys/${SURVEY_ID}/export-responses/${fileId}/file`,
    { headers: qH }).then(r => r.arrayBuffer());

  const zip = await JSZip.loadAsync(zipBuf);
  const csvFile = Object.keys(zip.files).find(n => n.endsWith(".csv"));
  const csvText = await zip.files[csvFile].async("string");

  const lines = csvText.split("\n");
  const headers = lines[0].split(",").map(h => h.replace(/"/g, "").trim());
  const responses = lines.slice(3).filter(l => l.trim()).map(line => {
    const vals = line.match(/(".*?"|[^,]*)/g) || [];
    const obj = {};
    headers.forEach((h, i) => { obj[h] = (vals[i] || "").replace(/"/g, "").trim(); });
    return obj;
  }).filter(r => r.Finished === "1" || r.Finished === "TRUE");

  console.log(`Parsed ${responses.length} completed responses`);

  // 4. Build summary
  const metaCols = new Set(["StartDate", "EndDate", "Status", "IPAddress",
    "Progress", "Duration (in seconds)", "Finished", "RecordedDate",
    "ResponseId", "RecipientLastName", "RecipientFirstName",
    "RecipientEmail", "ExternalReference", "LocationLatitude",
    "LocationLongitude", "DistributionChannel", "UserLanguage"]);
  const dataCols = headers.filter(h => !metaCols.has(h)).slice(0, 30);

  const colSummaries = {};
  for (const col of dataCols) {
    const vals = responses.map(r => (r[col] || "").trim()).filter(Boolean);
    if (!vals.length) continue;
    const unique = new Set(vals);
    if (unique.size <= 25) {
      const counts = {};
      vals.forEach(v => { counts[v] = (counts[v] || 0) + 1; });
      colSummaries[col] = `n=${vals.length} | ` + Object.entries(counts)
        .sort(([, a], [, b]) => b - a).slice(0, 10)
        .map(([v, c]) => `${v}: ${c} (${(c / vals.length * 100).toFixed(0)}%)`).join(", ");
    } else {
      colSummaries[col] = `n=${vals.length} | Samples: ${vals.slice(0, 8).join("; ")}`;
    }
  }

  // 5. Mave analysis
  const summary = Object.entries(colSummaries).map(([k, v]) => `**${k}**: ${v}`).join("\n");
  const segments = await fetch(`${MB}/mave/chat`, { method: "POST", headers: mvH,
    body: JSON.stringify({
      message: `Analyze ${responses.length} enterprise survey responses.

  COLUMN SUMMARIES:
  ${summary.slice(0, 6000)}

  Identify 5-8 segments. For each: name, %, characteristics, pain points, goals, communication style, buying triggers.
  Return JSON with "segments" array.`,
    }),
  }).then(r => r.json());

  const content = segments.content || "";
  console.log("=== Segment Discovery ===");
  console.log(content.slice(0, 2000));

  // 6. Create personas
  let parsed = [];
  try {
    const s = content.indexOf("["), e = content.lastIndexOf("]") + 1;
    if (s >= 0 && e > s) parsed = JSON.parse(content.slice(s, e));
  } catch { parsed = []; }

  for (const seg of parsed) {
    const res = await fetch(`${MB}/personas`, { method: "POST", headers: mvH,
      body: JSON.stringify({
        name: `QX: ${seg.name || "Unknown"}`,
        description: `Enterprise survey. Est. ${seg.percentage || "?"}%. ${seg.characteristics || seg.description || ""}`,
        demographic: seg.demographic || {},
        psychographic: { pain_points: seg.pain_points || [], goals: seg.goals || [],
          buying_triggers: seg.buying_triggers || [], communication_style: seg.communication_style || "" },
      }),
    }).then(r => r.json());
    console.log(`Created: ${seg.name} (${seg.percentage || "?"}%) → ${res.id}`);
    await new Promise(r => setTimeout(r, 300));
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Export started: ES_abc123def
  Progress: 25% (attempt 1)
  Progress: 75% (attempt 2)
  Progress: 100% (attempt 3)
Parsed 4,832 completed responses

=== Segment Discovery ===
[
  {"name": "Strategic Decision Maker", "percentage": 22,
   "confidence": "high",
   "characteristics": "C-level or VP, 500+ employee companies, high purchase authority",
   "pain_points": ["Vendor consolidation pressure", "Board-level ROI justification"],
   "goals": ["Strategic transformation", "Competitive advantage"],
   "buying_triggers": ["Peer company adoption", "Industry mandate"]},
  {"name": "Hands-On Evaluator", "percentage": 31,
   "confidence": "high",
   "characteristics": "Director/Manager level, runs POCs, technical decision influence",
   "pain_points": ["Integration complexity", "Time to evaluate"],
   "goals": ["Fast proof of value", "Team adoption"],
   "buying_triggers": ["Self-serve trial", "Case study from similar org"]},
  {"name": "Budget-Constrained Pragmatist", "percentage": 18,
   "confidence": "medium",
   "characteristics": "SMB or startup, price-sensitive, wears multiple hats",
   "pain_points": ["Affordability", "Implementation resources"],
   "goals": ["Maximum ROI per dollar", "Low-maintenance solution"],
   "buying_triggers": ["Startup pricing", "All-in-one platform"]}
]

Created: Strategic Decision Maker (22%, high) → per_qx_sdm_1
Created: Hands-On Evaluator (31%, high) → per_qx_hoe_2
Created: Budget-Constrained Pragmatist (18%, medium) → per_qx_bcp_3
Created: Risk-Averse Enterprise (15%, medium) → per_qx_rae_4
Created: Innovation Champion (14%, medium) → per_qx_ic_5

Persona library: 5 segments from 4,832 responses
```

### Error Handling

<AccordionGroup>
  <Accordion title="Async export lifecycle">Qualtrics exports are async: create → poll → download. Large surveys (10K+ responses) may take 5+ minutes. The code polls for up to 5 minutes (60 × 5s). For very large surveys, increase the timeout.</Accordion>
  <Accordion title="CSV header rows">Qualtrics CSV exports include 3 header rows (column names, import IDs, question text). The code skips rows 2-3 by filtering on the `Status` field. Verify your export format matches.</Accordion>
  <Accordion title="ZIP decompression">The export downloads as a ZIP file containing one CSV. Python uses `zipfile`; JavaScript needs `jszip`. Install `jszip` with `npm install jszip`.</Accordion>
  <Accordion title="Datacenter ID">The API URL includes your datacenter ID (e.g., `ca1`, `iad1`, `fra1`). Find it in Account Settings → Qualtrics IDs. Using the wrong DC returns 404.</Accordion>
</AccordionGroup>
