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

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 /personasPOST /focus-groups (Ranking + open-ended) → Decision factor analysis

Architecture

Code

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

Example Output

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

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.
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.
If fewer than 10 declined offers, Focus Group results are directional but not statistically meaningful. Note this in your report.