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

# Offer Acceptance Analysis

> Use Lever accepted and declined offers with Mavera focus groups for acceptance factor analysis

## Scenario

Some offers get accepted instantly; others languish and decline. The reasons are rarely captured in structured data — they live in recruiter notes and candidate conversations. You pull accepted and declined offers from Lever, build personas for each group, then run a Focus Group asking "What factors drove your decision?" with ranking and open-ended questions. The result tells you what to fix in your offer process.

**Flow:** Lever `GET /opportunities` (offer stage) → Separate accepted vs declined → Mavera `POST /personas` → `POST /focus-groups` (Ranking + open-ended) → Decision factor analysis

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Lever GET /opportunities (archived)"] --> B["Filter: Accepted vs Declined"]
    B --> C["POST /api/v1/personas"]
    C --> D["POST /api/v1/focus-groups"]
    D --> E["Offer acceptance factor analysis"]
```

### Code

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

  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 archived opportunities
  opps = []
  offset = None
  while len(opps) < 600:
      params = {"limit": 100, "archived_posting_id__exists": True}
      if offset:
          params["offset"] = offset
      resp = lv_get("/opportunities", params)
      opps.extend(resp.get("data", []))
      offset = resp.get("next")
      if not offset:
          break
      time.sleep(0.15)

  # 2. Separate accepted (hired) vs declined
  stages_resp = lv_get("/stages")
  stage_map = {s["id"]: s["text"].lower() for s in stages_resp.get("data", [])}

  accepted = []
  declined = []
  for opp in opps:
      stage_name = stage_map.get(opp.get("stage", ""), "")
      archive_reason = (opp.get("archivedReason") or "").lower()
      if "hired" in stage_name:
          accepted.append(opp)
      elif "offer" in archive_reason and ("declined" in archive_reason or "rejected" in archive_reason):
          declined.append(opp)
      elif "offer_declined" in archive_reason or "candidate_rejected" in archive_reason:
          declined.append(opp)

  # 3. Build persona context
  def extract_profile(opps_list):
      headlines = list({o.get("headline", "") for o in opps_list if o.get("headline")})[:5]
      sources = list({(o.get("sources") or ["Unknown"])[0] for o in opps_list})[:3]
      return headlines, sources

  acc_headlines, acc_sources = extract_profile(accepted)
  dec_headlines, dec_sources = extract_profile(declined)

  # 4. Create personas
  accepted_persona = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
      "name": "Lever: Offer Accepted",
      "description": (
          f"Candidates who accepted offers. N={len(accepted)}. "
          f"Headlines: {', '.join(acc_headlines[:3])}. Sources: {', '.join(acc_sources)}."
      ),
      "psychographic": {"decision": "accepted", "mindset": "Chose this company over alternatives"},
  }).json()

  declined_persona = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
      "name": "Lever: Offer Declined",
      "description": (
          f"Candidates who declined offers. N={len(declined)}. "
          f"Headlines: {', '.join(dec_headlines[:3])}. Sources: {', '.join(dec_sources)}."
      ),
      "psychographic": {"decision": "declined", "mindset": "Had a better option or dealbreaker"},
  }).json()
  time.sleep(0.3)

  # 5. Focus Group
  fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
      "name": "Offer Acceptance Factor Analysis",
      "persona_ids": [accepted_persona["id"], declined_persona["id"]],
      "questions": [
          {"type": "ranking", "text": (
              "Rank these factors by importance to your offer decision: "
              "(A) Base compensation (B) Equity/stock options (C) Team/manager quality "
              "(D) Company mission/product (E) Remote/flexibility policy "
              "(F) Career growth opportunity (G) Interview experience "
              "(H) Speed of offer process"
          )},
          "What was the single most important factor in your decision to accept or decline?",
          "Was there a moment during the interview process that significantly influenced your final decision?",
          "If you declined: What would the company have needed to change about the offer for you to accept?",
          "How did the offer compare to your other options? Be specific about the gaps.",
          "Describe the ideal offer experience — from verbal offer to signed letter.",
      ],
      "context": f"Company offer data: {len(accepted)} accepted, {len(declined)} declined (acceptance rate: {len(accepted)/max(len(accepted)+len(declined),1)*100:.0f}%)",
      "responses_per_persona": 4,
  }).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"Acceptance rate: {len(accepted)}/{len(accepted)+len(declined)} ({len(accepted)/max(len(accepted)+len(declined),1)*100:.0f}%)")
  print(f"Focus Group: {fg['id']}")
  for resp in data.get("responses", []):
      persona = "Accepted" if resp.get("persona_id") == accepted_persona["id"] else "Declined"
      print(f"\n[{persona}] {resp.get('question','')[:70]}")
      print(f"  → {resp.get('answer','')[:300]}")
  ```

  ```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. Archived opportunities
  const opps = [];
  let offset = null;
  while (opps.length < 600) {
    const params = { limit: 100, archived_posting_id__exists: true };
    if (offset) params.offset = offset;
    const resp = await lvGet("/opportunities", params);
    opps.push(...(resp.data || []));
    offset = resp.next;
    if (!offset) break;
    await new Promise((r) => setTimeout(r, 150));
  }

  // 2. Separate
  const stageMap = Object.fromEntries(
    ((await lvGet("/stages")).data || []).map((s) => [s.id, s.text.toLowerCase()])
  );
  const accepted = [], declined = [];
  for (const opp of opps) {
    const stage = stageMap[opp.stage] || "";
    const reason = (opp.archivedReason || "").toLowerCase();
    if (stage.includes("hired")) accepted.push(opp);
    else if (reason.includes("declined") || reason.includes("rejected")) declined.push(opp);
  }

  // 3. Personas
  const extract = (list) => ({
    headlines: [...new Set(list.map((o) => o.headline).filter(Boolean))].slice(0, 5),
    sources: [...new Set(list.map((o) => (o.sources || ["Unknown"])[0]))].slice(0, 3),
  });
  const accProfile = extract(accepted);
  const decProfile = extract(declined);

  const accPersona = await fetch(`${MV_BASE}/personas`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "Lever: Offer Accepted",
      description: `Accepted offers. N=${accepted.length}. Headlines: ${accProfile.headlines.slice(0, 3).join(", ")}.`,
      psychographic: { decision: "accepted" },
    }),
  }).then((r) => r.json());

  const decPersona = await fetch(`${MV_BASE}/personas`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "Lever: Offer Declined",
      description: `Declined offers. N=${declined.length}. Headlines: ${decProfile.headlines.slice(0, 3).join(", ")}.`,
      psychographic: { decision: "declined" },
    }),
  }).then((r) => r.json());

  // 4. Focus Group
  const fg = await fetch(`${MV_BASE}/focus-groups`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "Offer Acceptance Factors",
      persona_ids: [accPersona.id, decPersona.id],
      questions: [
        { type: "ranking", text: "Rank by importance: (A) Base comp (B) Equity (C) Team/manager (D) Mission (E) Flexibility (F) Growth (G) Interview experience (H) Offer speed" },
        "What was the single most important factor?",
        "Was there a pivotal moment during interviews?",
        "If declined: What change to the offer would have flipped your decision?",
        "How did this offer compare to alternatives? Be specific.",
        "Describe the ideal offer experience — verbal to signed.",
      ],
      context: `${accepted.length} accepted, ${declined.length} declined (${(accepted.length / Math.max(accepted.length + declined.length, 1) * 100).toFixed(0)}% rate)`,
      responses_per_persona: 4,
    }),
  }).then((r) => r.json());

  // 5. 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;
  }

  const rate = (accepted.length / Math.max(accepted.length + declined.length, 1) * 100).toFixed(0);
  console.log(`Acceptance rate: ${accepted.length}/${accepted.length + declined.length} (${rate}%)`);
  for (const resp of data.responses || []) {
    const label = resp.persona_id === accPersona.id ? "Accepted" : "Declined";
    console.log(`\n[${label}] ${(resp.question || "").slice(0, 70)}`);
    console.log(`  → ${(resp.answer || "").slice(0, 300)}`);
  }
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "acceptance_rate": "68%",
  "accepted": 45,
  "declined": 21,
  "ranking_consensus": {
    "accepted": ["C (Team/manager)", "F (Growth)", "A (Base comp)", "D (Mission)", "E (Flexibility)"],
    "declined": ["A (Base comp)", "B (Equity)", "E (Flexibility)", "H (Offer speed)", "C (Team/manager)"]
  },
  "key_insights": [
    {
      "persona": "Accepted",
      "insight": "Team quality is the #1 factor. 'I met my future manager during the onsite — she was brilliant and clearly cared about mentorship. That sealed it.'"
    },
    {
      "persona": "Declined",
      "insight": "Comp was #1 but speed was #4. 'Your offer took 2 weeks after the final round. By then I had a signed offer from [competitor]. If you'd moved in 48hrs, I might have chosen differently.'"
    },
    {
      "persona": "Declined",
      "insight": "'The equity explanation was confusing. I couldn't compare your options to the RSUs I had from [competitor]. A clear comparison doc would have helped.'"
    }
  ]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="Archive reason formats">Lever's `archivedReason` is a free-text string that varies by org. Common values: `"Offer declined"`, `"Candidate rejected offer"`, `"Withdrew"`. Normalize with case-insensitive matching.</Accordion>
  <Accordion title="No explicit offer object">Lever doesn't have a standalone Offers endpoint in v1. Offer data lives on the Opportunity as stage + archive reason. Use `GET /opportunities/{id}/offers` for detailed offer fields if available.</Accordion>
  <Accordion title="Small sample sizes">If fewer than 10 declined offers, Focus Group results are directional but not statistically meaningful. Note this in your report.</Accordion>
</AccordionGroup>
