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

# Bulk Response Analysis

> Pull SurveyMonkey bulk responses and analyze with Mavera for statistical patterns, sentiment trends, and recommendations

### Scenario

Your SurveyMonkey survey collected 1,200 responses across 20 questions — multiple choice, ratings, open-ended. Manually analyzing this would take days. This job pulls all responses via the bulk endpoint, structures them for analysis, then sends the entire dataset to Mave Agent with the instruction: "Analyze responses. Identify statistical patterns, sentiment trends, and actionable recommendations." The result is an AI-generated research report that would normally require a dedicated analyst.

**Flow:** SurveyMonkey `GET /v3/surveys/{id}/responses/bulk` → Aggregate and structure → Mave `POST /api/v1/mave/chat`: "Analyze responses. Identify statistical patterns, sentiment trends, recommendations." → Research report

### Architecture

```mermaid theme={"dark"}
flowchart LR
A["GET /v3/surveys/{id}/responses/bulk"] --> B["Parse all answer types"] --> C["Build statistical summary"] --> D["Mave Agent: Analyze patterns"] --> E["AI-generated research report"]
```

### Code

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

  SM = os.environ["SURVEYMONKEY_TOKEN"]
  MV = os.environ["MAVERA_API_KEY"]
  SM_BASE = "https://api.surveymonkey.com/v3"
  MB = "https://app.mavera.io/api/v1"
  SM_H = {"Authorization": f"Bearer {SM}", "Content-Type": "application/json"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  SURVEY_ID = os.environ.get("SURVEY_ID", "your_survey_id")

  # 1. Get survey structure
  survey = requests.get(f"{SM_BASE}/surveys/{SURVEY_ID}/details",
      headers=SM_H).json()
  print(f"Survey: {survey.get('title', 'Untitled')} ({survey.get('response_count', 0)} responses)")

  # Build question/answer lookup
  questions = {}
  choice_labels = {}
  for page in survey.get("pages", []):
      for q in page.get("questions", []):
          qid = q["id"]
          questions[qid] = {
              "title": q.get("headings", [{}])[0].get("heading", ""),
              "type": q.get("family", ""),
              "subtype": q.get("subtype", ""),
          }
          for row in q.get("answers", {}).get("rows", []):
              choice_labels[row["id"]] = row.get("text", "")
          for choice in q.get("answers", {}).get("choices", []):
              choice_labels[choice["id"]] = choice.get("text", "")
          for col in q.get("answers", {}).get("columns", []):
              choice_labels[col["id"]] = col.get("text", col.get("label", ""))

  # 2. Pull all responses (paginated)
  all_responses = []
  page_num = 1
  while True:
      r = requests.get(f"{SM_BASE}/surveys/{SURVEY_ID}/responses/bulk",
          headers=SM_H, params={"page": page_num, "per_page": 100})
      if r.status_code == 429:
          retry = int(r.headers.get("X-Ratelimit-App-Global-Day-Reset", 60))
          print(f"Rate limited — daily limit. Retry in {retry}s")
          time.sleep(min(retry, 60))
          continue
      r.raise_for_status()
      data = r.json()
      batch = data.get("data", [])
      all_responses.extend(batch)

      total = data.get("total", 0)
      if len(all_responses) >= total or not batch:
          break
      page_num += 1
      time.sleep(0.6)

  print(f"Fetched {len(all_responses)} responses")

  # 3. Aggregate answers by question
  qa_data = defaultdict(list)
  for resp in all_responses:
      for page_data in resp.get("pages", []):
          for q_data in page_data.get("questions", []):
              qid = q_data["id"]
              for ans in q_data.get("answers", []):
                  if "text" in ans:
                      qa_data[qid].append(ans["text"])
                  elif "choice_id" in ans:
                      label = choice_labels.get(ans["choice_id"], ans["choice_id"])
                      qa_data[qid].append(label)
                      if "row_id" in ans:
                          row = choice_labels.get(ans["row_id"], "")
                          qa_data[qid][-1] = f"{row}: {label}"

  # 4. Build analysis summary
  summary_parts = []
  for qid, answers in qa_data.items():
      q_info = questions.get(qid, {})
      title = q_info.get("title", qid)
      q_type = q_info.get("type", "")

      if q_type in ("single_choice", "multiple_choice", "matrix"):
          counts = Counter(answers).most_common(10)
          dist = ", ".join(f"{label}: {count} ({count/len(answers)*100:.0f}%)"
                          for label, count in counts)
          summary_parts.append(f"**{title}** (n={len(answers)})\n  {dist}")
      elif q_type == "open_ended":
          sample = "; ".join(answers[:15])[:500]
          summary_parts.append(f"**{title}** (n={len(answers)}, open-ended)\n  Samples: {sample}")
      else:
          try:
              nums = [float(a) for a in answers if a.replace(".", "").replace("-", "").isdigit()]
              if nums:
                  avg = sum(nums) / len(nums)
                  summary_parts.append(f"**{title}** (n={len(nums)}, avg={avg:.1f})")
          except ValueError:
              summary_parts.append(f"**{title}** (n={len(answers)})")

  summary = "\n\n".join(summary_parts)

  # 5. Mave analysis
  analysis = requests.post(f"{MB}/mave/chat", headers=MV_H, json={
      "message": f"""Analyze {len(all_responses)} survey responses from "{survey.get('title', 'Survey')}".

  QUESTION-BY-QUESTION DATA:
  {summary}

  Tasks:
  1) Statistical patterns: Correlations between questions, unexpected distributions
  2) Sentiment trends: Overall and per-question sentiment analysis
  3) Key findings: Top 5 insights ranked by impact
  4) Segment differences: Any visible subgroups in the data
  5) Red flags: Responses that indicate problems or risks
  6) Recommendations: 5 actionable next steps based on the data
  7) Suggested follow-up questions: What should you ask next?"""
  }).json()

  print(f"\n{'='*60}")
  print(f"ANALYSIS: {survey.get('title', 'Survey')} ({len(all_responses)} responses)")
  print(f"{'='*60}")
  print(analysis.get("content", "")[:3000])
  print(f"\nSources: {len(analysis.get('sources', []))}")
  ```

  ```javascript JavaScript theme={"dark"}
  const SM = process.env.SURVEYMONKEY_TOKEN;
  const MV = process.env.MAVERA_API_KEY;
  const SM_BASE = "https://api.surveymonkey.com/v3";
  const MB = "https://app.mavera.io/api/v1";
  const smH = { Authorization: `Bearer ${SM}`, "Content-Type": "application/json" };
  const mvH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const SURVEY_ID = process.env.SURVEY_ID || "your_survey_id";

  // 1. Survey structure
  const survey = await fetch(`${SM_BASE}/surveys/${SURVEY_ID}/details`,
    { headers: smH }).then(r => r.json());
  console.log(`Survey: ${survey.title} (${survey.response_count} responses)`);

  const questions = {};
  const choiceLabels = {};
  for (const page of survey.pages || []) {
    for (const q of page.questions || []) {
      questions[q.id] = { title: q.headings?.[0]?.heading || "", type: q.family || "",
        subtype: q.subtype || "" };
      for (const r of q.answers?.rows || []) choiceLabels[r.id] = r.text || "";
      for (const c of q.answers?.choices || []) choiceLabels[c.id] = c.text || "";
      for (const c of q.answers?.columns || []) choiceLabels[c.id] = c.text || c.label || "";
    }
  }

  // 2. Pull responses
  const allResponses = [];
  let pageNum = 1;
  while (true) {
    let res = await fetch(
      `${SM_BASE}/surveys/${SURVEY_ID}/responses/bulk?page=${pageNum}&per_page=100`,
      { headers: smH });
    if (res.status === 429) {
      const retry = parseInt(res.headers.get("X-Ratelimit-App-Global-Day-Reset") || "60");
      console.log(`Rate limited. Waiting ${Math.min(retry, 60)}s`);
      await new Promise(r => setTimeout(r, Math.min(retry, 60) * 1000));
      continue;
    }
    const data = await res.json();
    allResponses.push(...(data.data || []));
    if (allResponses.length >= (data.total || 0) || !(data.data || []).length) break;
    pageNum++;
    await new Promise(r => setTimeout(r, 600));
  }
  console.log(`Fetched ${allResponses.length} responses`);

  // 3. Aggregate
  const qaData = {};
  for (const resp of allResponses) {
    for (const pg of resp.pages || []) {
      for (const q of pg.questions || []) {
        (qaData[q.id] ??= []);
        for (const ans of q.answers || []) {
          if (ans.text) qaData[q.id].push(ans.text);
          else if (ans.choice_id) {
            let label = choiceLabels[ans.choice_id] || ans.choice_id;
            if (ans.row_id) label = `${choiceLabels[ans.row_id] || ""}: ${label}`;
            qaData[q.id].push(label);
          }
        }
      }
    }
  }

  // 4. Build summary
  const summaryParts = [];
  for (const [qid, answers] of Object.entries(qaData)) {
    const q = questions[qid] || {};
    const title = q.title || qid;
    if (["single_choice", "multiple_choice", "matrix"].includes(q.type)) {
      const counts = {};
      answers.forEach(a => { counts[a] = (counts[a] || 0) + 1; });
      const dist = Object.entries(counts).sort(([, a], [, b]) => b - a).slice(0, 10)
        .map(([l, c]) => `${l}: ${c} (${(c / answers.length * 100).toFixed(0)}%)`).join(", ");
      summaryParts.push(`**${title}** (n=${answers.length})\n  ${dist}`);
    } else if (q.type === "open_ended") {
      summaryParts.push(`**${title}** (n=${answers.length}, open-ended)\n  Samples: ${answers.slice(0, 15).join("; ").slice(0, 500)}`);
    } else {
      const nums = answers.map(Number).filter(n => !isNaN(n));
      if (nums.length) summaryParts.push(`**${title}** (n=${nums.length}, avg=${(nums.reduce((s, n) => s + n, 0) / nums.length).toFixed(1)})`);
      else summaryParts.push(`**${title}** (n=${answers.length})`);
    }
  }

  // 5. Mave analysis
  const analysis = await fetch(`${MB}/mave/chat`, { method: "POST", headers: mvH,
    body: JSON.stringify({
      message: `Analyze ${allResponses.length} responses from "${survey.title}".

  QUESTION DATA:
  ${summaryParts.join("\n\n")}

  Tasks: 1) Statistical patterns 2) Sentiment trends 3) Top 5 insights 4) Segment differences 5) Red flags 6) 5 recommendations 7) Follow-up questions`,
    }),
  }).then(r => r.json());

  console.log(`\n${"=".repeat(60)}`);
  console.log(`ANALYSIS: ${survey.title} (${allResponses.length} responses)`);
  console.log(`${"=".repeat(60)}`);
  console.log((analysis.content || "").slice(0, 3000));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
============================================================
ANALYSIS: Q1 Customer Satisfaction Survey (1,247 responses)
============================================================

## Key Findings

### 1. Bimodal Satisfaction (Critical)
Overall satisfaction shows two peaks: 8-9/10 (43%) and 3-4/10 (22%).
The middle is thin — customers either love you or are frustrated.
No "passive middle" suggests polarizing experiences.

### 2. Onboarding is the Breakpoint
92% of dissatisfied respondents (≤5/10) cite onboarding as their
primary pain. Satisfaction jumps from 4.1 → 8.3 after 30-day mark.
Intervention window: days 3-14.

### 3. Feature Satisfaction ≠ Retention Intent
Reporting module scores 8.2/10 in satisfaction but is cited by 38%
of "likely to churn" respondents as "not enough." High expectations,
not low quality, drives churn risk.

### 4. Support Channel Preference Shift
Under-35 respondents (34% of base) prefer chat (67%) over email (12%).
Over-45 prefer email (58%) over chat (21%). Your support channel
allocation doesn't match.

### 5. Open-Ended Red Flag
"We're evaluating alternatives" appears in 14% of detractor comments.
Cross-reference with CRM to identify at-risk accounts.

## Recommendations
1. Launch 14-day onboarding drip with check-in at day 3, 7, 14
2. Shift 30% of support capacity from email to chat
3. Create "power user" track for reporting module
4. Run churn risk analysis on detractor accounts
5. Deploy NPS follow-up survey targeting the "3-4" cohort

Sources: 0
```

### Error Handling

<AccordionGroup>
  <Accordion title="500 req/day limit">Private apps are capped at 500 requests per day. The bulk endpoint returns 100 responses per page, so a 2,000-response survey needs 20 calls. Cache responses locally after the first pull.</Accordion>
  <Accordion title="Matrix question parsing">Matrix questions nest rows and columns. The code concatenates "Row: Column" labels. For complex matrices (10×10), the summary may be verbose — truncate to top 5 combinations.</Accordion>
  <Accordion title="Choice ID vs label mismatch">If `choice_labels` doesn't find a match, the raw choice ID is used. This happens with custom "Other" options. Pre-populate the lookup from the survey detail endpoint.</Accordion>
  <Accordion title="Mave context limits">Surveys with 20+ questions and 1,000+ responses generate large summaries. The code caps open-ended samples at 15 and text at 500 chars. For very large surveys, analyze in sections.</Accordion>
</AccordionGroup>
