Skip to main content

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.

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 /personasPOST /focus-groups → Brand perception impact

Architecture

Code

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]}")

Example Output

{
  "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

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