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

# Pipeline Stage → Candidate Experience Focus Group

> Use Lever opportunities by stage with Mavera personas and focus groups to get stage-by-stage candidate experience ratings

## Scenario

Lever tracks every candidate as an "Opportunity" that flows through pipeline stages — New Lead, Recruiter Screen, Phone Interview, Onsite, Offer, Hired. Each stage represents a different candidate mindset and experience level with your process. You pull opportunities grouped by their current stage, create a persona for each stage, then run a Focus Group asking synthetic candidates to rate their experience at that point. The result tells you where your pipeline loses goodwill.

**Flow:** Lever `GET /opportunities` → Group by stage → Mavera `POST /personas` (per stage) → `POST /focus-groups` → Experience ratings by stage

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Lever GET /opportunities"] --> B["Group by stage"]
    B --> C["GET /stages → Map IDs to names"]
    C --> D["POST /api/v1/personas"]
    D --> E["POST /api/v1/focus-groups"]
    E --> F["Stage-by-stage experience scores"]
```

### Code

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

  LV_KEY = os.environ["LEVER_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  LV_BASE = "https://api.lever.co/v1"
  MV_BASE = "https://app.mavera.io/api/v1"
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  lv_auth = base64.b64encode(f"{LV_KEY}:".encode()).decode()
  LV_H = {"Authorization": f"Basic {lv_auth}"}

  def lv_get(path, params=None):
      r = requests.get(f"{LV_BASE}{path}", headers=LV_H, params=params or {})
      if r.status_code == 429:
          time.sleep(1)
          return lv_get(path, params)
      r.raise_for_status()
      return r.json()

  # 1. Pull stage definitions
  stages_resp = lv_get("/stages")
  stage_map = {s["id"]: s["text"] for s in stages_resp.get("data", [])}

  # 2. Pull opportunities with stage info
  opportunities = []
  offset = None
  while len(opportunities) < 500:
      params = {"limit": 100, "expand": "stage"}
      if offset:
          params["offset"] = offset
      resp = lv_get("/opportunities", params)
      opportunities.extend(resp.get("data", []))
      offset = resp.get("next")
      if not offset:
          break
      time.sleep(0.15)

  # 3. Group by current stage
  stage_groups = defaultdict(list)
  for opp in opportunities:
      stage_id = opp.get("stage")
      stage_name = stage_map.get(stage_id, "Unknown")
      stage_groups[stage_name].append({
          "name": opp.get("name", ""),
          "headline": opp.get("headline", ""),
          "origin": opp.get("origin", ""),
          "sources": opp.get("sources", []),
      })

  # 4. Create personas per pipeline stage
  persona_ids = []
  for stage_name, opps in stage_groups.items():
      if len(opps) < 3:
          continue
      headlines = list({o["headline"] for o in opps if o["headline"]})[:5]
      origins = list({o["origin"] for o in opps if o["origin"]})[:3]

      p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
          "name": f"Lever: {stage_name} Candidate",
          "description": (
              f"Candidate currently in '{stage_name}' stage. N={len(opps)}. "
              f"Headlines: {', '.join(headlines[:3])}. Origins: {', '.join(origins)}."
          ),
          "demographic": {"job_titles": headlines},
          "psychographic": {
              "pipeline_stage": stage_name,
              "mindset": f"Candidate at {stage_name} — " + (
                  "just applied, low investment" if "new" in stage_name.lower()
                  else "deeply invested, high expectations" if "onsite" in stage_name.lower() or "offer" in stage_name.lower()
                  else "moderate engagement"
              ),
          },
      }).json()
      persona_ids.append({"id": p["id"], "stage": stage_name, "n": len(opps)})
      print(f"Persona: {p['id']} — {stage_name} ({len(opps)} candidates)")
      time.sleep(0.3)

  # 5. Run candidate experience Focus Group
  PROCESS_DESCRIPTION = """Our interview process:
  1. Application review (3-5 business days)
  2. 30-min recruiter screen
  3. 45-min hiring manager phone interview
  4. 4-hour onsite (3 technical + 1 culture)
  5. Offer within 48 hours of onsite decision"""

  fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
      "name": "Candidate Experience by Pipeline Stage",
      "persona_ids": [p["id"] for p in persona_ids],
      "questions": [
          {"type": "likert", "text": "Rate your overall experience at your current stage (1-5)", "scale": 5},
          "What has been the most frustrating part of the process so far?",
          "What communication would improve your experience right now?",
          "Would you recommend this company to a friend based on your experience so far?",
          "What's the #1 thing that would make you drop out of this process?",
      ],
      "context": PROCESS_DESCRIPTION,
      "responses_per_persona": 3,
  }).json()

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

  print(f"\nFocus Group: {fg['id']}")
  for resp in data.get("responses", []):
      stage = next((p["stage"] for p in persona_ids if p["id"] == resp.get("persona_id")), "?")
      print(f"\n[{stage}] {resp.get('question','')[:60]}")
      print(f"  → {resp.get('answer','')[:250]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const LV_KEY = process.env.LEVER_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const LV_BASE = "https://api.lever.co/v1";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const LV_H = { Authorization: `Basic ${btoa(`${LV_KEY}:`)}` };

  async function lvGet(path, params = {}) {
    const qs = new URLSearchParams(params).toString();
    const res = await fetch(`${LV_BASE}${path}?${qs}`, { headers: LV_H });
    if (res.status === 429) { await new Promise((r) => setTimeout(r, 1000)); return lvGet(path, params); }
    if (!res.ok) throw new Error(`Lever ${res.status}`);
    return res.json();
  }

  // 1. Stages
  const stageMap = Object.fromEntries(
    ((await lvGet("/stages")).data || []).map((s) => [s.id, s.text])
  );

  // 2. Opportunities
  const opportunities = [];
  let offset = null;
  while (opportunities.length < 500) {
    const params = { limit: 100, expand: "stage" };
    if (offset) params.offset = offset;
    const resp = await lvGet("/opportunities", params);
    opportunities.push(...(resp.data || []));
    offset = resp.next;
    if (!offset) break;
    await new Promise((r) => setTimeout(r, 150));
  }

  // 3. Group by stage
  const stageGroups = {};
  for (const opp of opportunities) {
    const stageName = stageMap[opp.stage] || "Unknown";
    (stageGroups[stageName] ??= []).push({
      headline: opp.headline || "",
      origin: opp.origin || "",
    });
  }

  // 4. Personas
  const personaIds = [];
  for (const [stageName, opps] of Object.entries(stageGroups)) {
    if (opps.length < 3) continue;
    const headlines = [...new Set(opps.map((o) => o.headline).filter(Boolean))].slice(0, 5);
    const origins = [...new Set(opps.map((o) => o.origin).filter(Boolean))].slice(0, 3);

    const p = await fetch(`${MV_BASE}/personas`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        name: `Lever: ${stageName} Candidate`,
        description: `${stageName} stage. N=${opps.length}. Headlines: ${headlines.slice(0, 3).join(", ")}.`,
        demographic: { job_titles: headlines },
        psychographic: { pipeline_stage: stageName },
      }),
    }).then((r) => r.json());
    personaIds.push({ id: p.id, stage: stageName, n: opps.length });
    console.log(`Persona: ${p.id} — ${stageName} (${opps.length})`);
    await new Promise((r) => setTimeout(r, 300));
  }

  // 5. Focus Group
  const fg = await fetch(`${MV_BASE}/focus-groups`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "Candidate Experience by Stage",
      persona_ids: personaIds.map((p) => p.id),
      questions: [
        { type: "likert", text: "Rate your overall experience at this stage (1-5)", scale: 5 },
        "What has been most frustrating so far?",
        "What communication would improve your experience?",
        "Would you recommend this company to a friend?",
        "What would make you drop out of this process?",
      ],
      context: "Our process: 1) App review (3-5 days) 2) 30-min recruiter screen 3) 45-min phone 4) 4hr onsite 5) Offer in 48hr",
      responses_per_persona: 3,
    }),
  }).then((r) => r.json());

  // 6. Poll
  let data;
  for (let i = 0; i < 20; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    data = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then((r) => r.json());
    if (data.status === "completed") break;
  }

  for (const resp of data.responses || []) {
    const stage = personaIds.find((p) => p.id === resp.persona_id)?.stage || "?";
    console.log(`\n[${stage}] ${(resp.question || "").slice(0, 60)}`);
    console.log(`  → ${(resp.answer || "").slice(0, 250)}`);
  }
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "focus_group_id": "fg_cand_exp_3k9",
  "stage_scores": [
    { "stage": "New Lead", "experience_rating": 3.8, "n": 142 },
    { "stage": "Recruiter Screen", "experience_rating": 4.2, "n": 89 },
    { "stage": "Phone Interview", "experience_rating": 3.5, "n": 45 },
    { "stage": "Onsite", "experience_rating": 2.9, "n": 28 },
    { "stage": "Offer", "experience_rating": 4.6, "n": 12 }
  ],
  "key_findings": [
    {
      "stage": "Onsite",
      "issue": "4-hour onsite feels excessive. 'I took a full day off work for this. 3 back-to-back technicals is exhausting — I can't show my best work in round 3.'",
      "recommendation": "Split onsite into two half-day sessions or reduce to 3 hours total."
    },
    {
      "stage": "New Lead",
      "issue": "'3-5 business days' for app review feels slow. 'I've already done 2 phone screens with competitors by then.'",
      "recommendation": "Auto-acknowledge within 24hrs. Fast-track referrals."
    }
  ]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="Rate limits (10 req/sec steady)">Lever allows 10/sec sustained with 20/sec burst. The code uses 150ms delays. For bulk pulls, monitor `X-RateLimit-Remaining` header.</Accordion>
  <Accordion title="Offset pagination">Lever uses cursor-based pagination via `offset` in the response `next` field. Never use numeric page numbers — always pass the returned cursor.</Accordion>
  <Accordion title="Stage ID → name mapping">Opportunity `stage` field is an ID, not a name. Always fetch `/stages` first to build a map. Stage names are configurable per-org.</Accordion>
</AccordionGroup>
