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: LeverGET /opportunities → Group by stage → Mavera POST /personas (per stage) → POST /focus-groups → Experience ratings by stage
Architecture
Code
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]}")
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)}`);
}
Example Output
{
"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
Rate limits (10 req/sec steady)
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.Offset pagination
Offset pagination
Lever uses cursor-based pagination via
offset in the response next field. Never use numeric page numbers — always pass the returned cursor.Stage ID → name mapping
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.