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

# Rejection Reason Analysis

> Group Greenhouse rejections by stage and reason, build personas, run Focus Group on brand perception — quantify the employer brand cost of your rejection process

## Scenario

Every rejected candidate becomes an employer brand risk. What they think about your process — fairly or not — shapes Glassdoor reviews, referral willingness, and market reputation. You pull rejection reasons and stage-at-rejection data from Greenhouse, build personas representing rejected candidates at each stage, then run a Focus Group asking "How does this rejection experience affect your perception of our brand?" The output quantifies the brand cost of your rejection process.

**Flow:** Greenhouse `GET /applications` (rejected) → Group by rejection reason/stage → Mavera `POST /personas` → `POST /focus-groups` → Brand perception impact

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Greenhouse GET /applications?status=rejected"] --> B["Group by rejection reason + stage"]
    B --> C["POST /api/v1/personas"]
    C --> D["POST /api/v1/focus-groups"]
    D --> E["Brand perception risk assessment"]
```

## Code

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

  GH_KEY = os.environ["GREENHOUSE_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  GH_BASE = "https://harvest.greenhouse.io/v1"
  MV_BASE = "https://app.mavera.io/api/v1"
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  gh_auth = base64.b64encode(f"{GH_KEY}:".encode()).decode()
  GH_H = {"Authorization": f"Basic {gh_auth}"}

  # 1. Pull rejection reasons
  reasons = requests.get(f"{GH_BASE}/rejection_reasons", headers=GH_H).json()
  reason_map = {r["id"]: r.get("name", "Unknown") for r in reasons}

  # 2. Pull rejected applications
  rejected_apps = []
  page = 1
  while len(rejected_apps) < 500:
      batch = requests.get(f"{GH_BASE}/applications",
          headers=GH_H,
          params={"per_page": 100, "page": page, "status": "rejected"}).json()
      if not batch:
          break
      rejected_apps.extend(batch)
      page += 1
      time.sleep(0.3)

  # 3. Group by rejection stage and reason
  stage_groups = defaultdict(list)
  for app in rejected_apps:
      stage = app.get("current_stage", {})
      stage_name = stage.get("name", "Unknown Stage") if stage else "Unknown Stage"
      reason_id = app.get("rejection_reason", {})
      reason_name = "No reason given"
      if reason_id and isinstance(reason_id, dict):
          reason_name = reason_id.get("name", reason_map.get(reason_id.get("id"), "Unknown"))
      elif reason_id and isinstance(reason_id, int):
          reason_name = reason_map.get(reason_id, "Unknown")
      stage_groups[stage_name].append({"reason": reason_name, "app_id": app["id"]})

  # 4. Create personas per rejection stage
  persona_ids = []
  stage_summary = []
  for stage_name, apps in stage_groups.items():
      if len(apps) < 5:
          continue
      reason_counts = defaultdict(int)
      for a in apps:
          reason_counts[a["reason"]] += 1
      top_reasons = sorted(reason_counts.items(), key=lambda x: -x[1])[:3]

      p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
          "name": f"GH Rejected: {stage_name}",
          "description": (
              f"Candidate rejected at {stage_name} stage. N={len(apps)}. "
              f"Top reasons: {', '.join(f'{r} ({n})' for r, n in top_reasons)}."
          ),
          "psychographic": {
              "stage_at_rejection": stage_name,
              "emotional_state": "disappointed, evaluating whether to engage with brand again",
          },
      }).json()
      persona_ids.append({"id": p["id"], "stage": stage_name, "n": len(apps)})
      stage_summary.append(f"- {stage_name}: {len(apps)} rejected. Top: {top_reasons[0][0]} ({top_reasons[0][1]})")
      time.sleep(0.3)

  # 5. Focus Group on brand perception
  fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
      "name": "Rejection Brand Impact Assessment",
      "persona_ids": [p["id"] for p in persona_ids],
      "questions": [
          "How does being rejected at your stage affect your perception of this company as an employer?",
          "Would you reapply to this company in 12 months? Would you refer a friend? Why or why not?",
          {"type": "ranking", "text": "Rank these factors by how much they affect your post-rejection perception: (A) Speed of response (B) Personalization of rejection (C) Feedback provided (D) Interviewer professionalism (E) Overall process transparency"},
          "What would a rejection email need to say to leave you with a positive impression?",
          "Would you leave a Glassdoor review about this experience? What would it say?",
      ],
      "context": f"Company rejection data summary:\n" + "\n".join(stage_summary),
      "responses_per_persona": 3,
  }).json()

  # 6. Poll results
  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"Focus Group: {fg['id']} | Personas: {len(persona_ids)}")
  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[Rejected at: {stage}] {resp.get('question','')[:70]}")
      print(f"  → {resp.get('answer','')[:300]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const GH_KEY = process.env.GREENHOUSE_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const GH_BASE = "https://harvest.greenhouse.io/v1";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const GH_H = { Authorization: `Basic ${btoa(`${GH_KEY}:`)}` };

  async function ghGet(path, params = {}) {
    const qs = new URLSearchParams(params).toString();
    const res = await fetch(`${GH_BASE}${path}?${qs}`, { headers: GH_H });
    if (res.status === 429) { await new Promise((r) => setTimeout(r, 10000)); return ghGet(path, params); }
    if (!res.ok) throw new Error(`GH ${res.status}`);
    return res.json();
  }

  // 1. Rejection reasons
  const reasons = await ghGet("/rejection_reasons");
  const reasonMap = Object.fromEntries(reasons.map((r) => [r.id, r.name || "Unknown"]));

  // 2. Rejected applications
  const rejectedApps = [];
  let page = 1;
  while (rejectedApps.length < 500) {
    const batch = await ghGet("/applications", { per_page: 100, page, status: "rejected" });
    if (!batch.length) break;
    rejectedApps.push(...batch);
    page++;
    await new Promise((r) => setTimeout(r, 300));
  }

  // 3. Group by stage
  const stageGroups = {};
  for (const app of rejectedApps) {
    const stageName = app.current_stage?.name || "Unknown Stage";
    const reasonName = app.rejection_reason?.name || reasonMap[app.rejection_reason?.id] || "No reason";
    (stageGroups[stageName] ??= []).push({ reason: reasonName });
  }

  // 4. Personas
  const personaIds = [];
  const stageSummary = [];
  for (const [stageName, apps] of Object.entries(stageGroups)) {
    if (apps.length < 5) continue;
    const counts = {};
    apps.forEach((a) => { counts[a.reason] = (counts[a.reason] || 0) + 1; });
    const topReasons = Object.entries(counts).sort(([, a], [, b]) => b - a).slice(0, 3);

    const p = await fetch(`${MV_BASE}/personas`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        name: `GH Rejected: ${stageName}`,
        description: `Rejected at ${stageName}. N=${apps.length}. Top: ${topReasons.map(([r, n]) => `${r} (${n})`).join(", ")}.`,
        psychographic: { stage_at_rejection: stageName, emotional_state: "disappointed" },
      }),
    }).then((r) => r.json());
    personaIds.push({ id: p.id, stage: stageName, n: apps.length });
    stageSummary.push(`- ${stageName}: ${apps.length} rejected. Top: ${topReasons[0][0]} (${topReasons[0][1]})`);
    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: "Rejection Brand Impact",
      persona_ids: personaIds.map((p) => p.id),
      questions: [
        "How does rejection at your stage affect your perception of this employer?",
        "Would you reapply in 12 months? Refer a friend? Why?",
        { type: "ranking", text: "Rank by perception impact: (A) Response speed (B) Personalization (C) Feedback (D) Interviewer professionalism (E) Transparency" },
        "What would a rejection email need to say to leave a positive impression?",
        "Would you leave a Glassdoor review? What would it say?",
      ],
      context: `Rejection summary:\n${stageSummary.join("\n")}`,
      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;
  }

  console.log(`Focus Group: ${fg.id} | Personas: ${personaIds.length}`);
  for (const resp of data.responses || []) {
    const stage = personaIds.find((p) => p.id === resp.persona_id)?.stage || "?";
    console.log(`\n[Rejected at: ${stage}] ${(resp.question || "").slice(0, 70)}`);
    console.log(`  → ${(resp.answer || "").slice(0, 300)}`);
  }
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "id": "fg_rej_brand_7x2",
  "personas": 4,
  "stage_breakdown": [
    { "stage": "Application Review", "rejected": 245, "top_reason": "Not qualified (89)" },
    { "stage": "Phone Screen", "rejected": 112, "top_reason": "Unresponsive (34)" },
    { "stage": "Onsite", "rejected": 67, "top_reason": "Culture fit (28)" },
    { "stage": "Offer", "rejected": 12, "top_reason": "Declined offer (8)" }
  ],
  "sample_responses": [
    {
      "stage": "Onsite",
      "question": "Would you reapply?",
      "answer": "No. I invested two full days in interviews with no feedback. A form rejection after that level of effort is disrespectful. I'd tell colleagues to skip this company."
    },
    {
      "stage": "Application Review",
      "question": "Ranking: perception impact",
      "answer": "1. Response speed — waiting 6 weeks for a no is worse than the no itself. 2. Personalization. 3. Transparency. 4. Feedback. 5. Professionalism."
    },
    {
      "stage": "Phone Screen",
      "question": "What would a good rejection email say?",
      "answer": "Acknowledge the specific role. One sentence of genuine feedback. An invitation to apply for future roles with a direct link. Takes 30 seconds to write."
    }
  ]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="Rejection reason structure varies">The `rejection_reason` field can be an object `{id, name}` or just an ID integer depending on API version. The code handles both formats.</Accordion>
  <Accordion title="Null current_stage">Applications rejected before entering a stage have `current_stage: null`. These are grouped under "Unknown Stage" — typically auto-rejected applications.</Accordion>
  <Accordion title="Large rejection volumes">High-volume orgs may have 10,000+ rejections. Use `created_after` parameter to limit to recent data: `?created_after=2025-01-01T00:00:00Z`.</Accordion>
  <Accordion title="Privacy considerations">Don't send candidate PII (names, emails) to Mavera. The code only sends aggregate counts, titles, and reasons — never individual identities.</Accordion>
</AccordionGroup>
