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

# Candidate Pipeline → Employer Brand Personas

> Build Mavera personas from Greenhouse candidate segments (source × stage outcome) and run Focus Groups to validate employer messaging before publishing

## Scenario

Your Greenhouse pipeline has thousands of candidates across sources (LinkedIn, referrals, careers page, agencies) and stages (applied, phone screen, onsite, offer, hired, rejected). Each segment has a distinct experience of your employer brand. You extract candidates grouped by source and stage outcome, build Mavera personas representing each segment, then run Focus Groups to test your employer messaging before publishing it.

**Flow:** Greenhouse `GET /candidates` → Filter by source/stage → Aggregate traits → Mavera `POST /personas` → `POST /focus-groups` → Employer messaging validation

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Greenhouse Candidates API"] --> B["Filter by source + stage outcome"]
    B --> C["POST /api/v1/personas"]
    C --> D["POST /api/v1/focus-groups"]
    D --> E["Employer brand feedback"]
```

## 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}"}

  SOURCES = ["LinkedIn", "Referral", "Careers Page"]
  OUTCOMES = ["hired", "rejected"]

  def gh_get(path, params=None):
      r = requests.get(f"{GH_BASE}{path}", headers=GH_H, params=params or {})
      if r.status_code == 429:
          time.sleep(10)
          return gh_get(path, params)
      r.raise_for_status()
      return r.json()

  # 1. Pull candidates and their applications
  candidates = []
  page = 1
  while len(candidates) < 500:
      batch = gh_get("/candidates", {"per_page": 100, "page": page})
      if not batch:
          break
      candidates.extend(batch)
      page += 1

  # 2. Group by source × outcome
  segments = defaultdict(list)
  for c in candidates:
      for app in c.get("applications", []):
          source_name = (app.get("source", {}) or {}).get("public_name", "Unknown")
          status = app.get("status", "active")
          if source_name in SOURCES and status in OUTCOMES:
              segments[(source_name, status)].append({
                  "name": f"{c.get('first_name','')} {c.get('last_name','')}",
                  "title": c.get("title", ""),
                  "company": c.get("company", ""),
                  "application_count": len(c.get("applications", [])),
              })

  # 3. Create Mavera personas per segment
  persona_ids = []
  for (source, outcome), members in segments.items():
      if len(members) < 3:
          continue
      titles = list({m["title"] for m in members if m["title"]})[:5]
      companies = list({m["company"] for m in members if m["company"]})[:5]

      p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
          "name": f"GH: {source} → {outcome.title()}",
          "description": (
              f"Candidates from {source} who were {outcome}. "
              f"N={len(members)}. Titles: {', '.join(titles[:3])}. "
              f"Companies: {', '.join(companies[:3])}."
          ),
          "demographic": {"job_titles": titles},
          "psychographic": {
              "source": source,
              "outcome": outcome,
              "mindset": f"Candidate who was {outcome} via {source}",
          },
      }).json()
      persona_ids.append({"id": p["id"], "label": f"{source} → {outcome.title()}"})
      print(f"Persona: {p['id']} — {source} → {outcome.title()} ({len(members)} candidates)")
      time.sleep(0.3)

  # 4. Run Focus Group with employer messaging
  EMPLOYER_MESSAGE = """Join a team that ships fast, learns faster, and celebrates wins together.
  We offer competitive comp, unlimited PTO, and a culture where engineers own their roadmap.
  "Best decision I ever made." — Senior Engineer, 2 years"""

  fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
      "name": "Employer Brand Messaging Test",
      "persona_ids": [p["id"] for p in persona_ids],
      "questions": [
          "How authentic does this employer message feel on a scale of 1-10?",
          "Would this message make you more or less likely to apply? Why?",
          "What specific claim feels most credible? Least credible?",
          "How would you describe this company's culture to a friend based on this message?",
      ],
      "context": EMPLOYER_MESSAGE,
      "responses_per_persona": 3,
  }).json()

  # 5. Poll for 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

  for resp in data.get("responses", []):
      label = next((p["label"] for p in persona_ids if p["id"] == resp.get("persona_id")), "?")
      print(f"\n[{label}] {resp.get('question','')[:60]}")
      print(f"  → {resp.get('answer','')[:250]}")
  ```

  ```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}:`)}` };

  const SOURCES = ["LinkedIn", "Referral", "Careers Page"];
  const OUTCOMES = ["hired", "rejected"];

  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(`Greenhouse ${res.status}`);
    return res.json();
  }

  // 1. Pull candidates
  const candidates = [];
  let page = 1;
  while (candidates.length < 500) {
    const batch = await ghGet("/candidates", { per_page: 100, page });
    if (!batch.length) break;
    candidates.push(...batch);
    page++;
  }

  // 2. Group by source × outcome
  const segments = {};
  for (const c of candidates) {
    for (const app of c.applications || []) {
      const source = app.source?.public_name || "Unknown";
      const status = app.status || "active";
      if (SOURCES.includes(source) && OUTCOMES.includes(status)) {
        const key = `${source}|${status}`;
        (segments[key] ??= []).push({
          title: c.title || "",
          company: c.company || "",
        });
      }
    }
  }

  // 3. Create personas
  const personaIds = [];
  for (const [key, members] of Object.entries(segments)) {
    if (members.length < 3) continue;
    const [source, outcome] = key.split("|");
    const titles = [...new Set(members.map((m) => m.title).filter(Boolean))].slice(0, 5);
    const companies = [...new Set(members.map((m) => m.company).filter(Boolean))].slice(0, 5);

    const p = await fetch(`${MV_BASE}/personas`, {
      method: "POST",
      headers: MV_H,
      body: JSON.stringify({
        name: `GH: ${source} → ${outcome.charAt(0).toUpperCase() + outcome.slice(1)}`,
        description: `Candidates from ${source} who were ${outcome}. N=${members.length}. Titles: ${titles.slice(0, 3).join(", ")}.`,
        demographic: { job_titles: titles },
        psychographic: { source, outcome, mindset: `Candidate who was ${outcome} via ${source}` },
      }),
    }).then((r) => r.json());

    personaIds.push({ id: p.id, label: `${source} → ${outcome}` });
    console.log(`Persona: ${p.id} — ${source} → ${outcome} (${members.length})`);
    await new Promise((r) => setTimeout(r, 300));
  }

  // 4. Focus Group
  const EMPLOYER_MESSAGE = `Join a team that ships fast, learns faster, and celebrates wins together.
  We offer competitive comp, unlimited PTO, and a culture where engineers own their roadmap.
  "Best decision I ever made." — Senior Engineer, 2 years`;

  const fg = await fetch(`${MV_BASE}/focus-groups`, {
    method: "POST",
    headers: MV_H,
    body: JSON.stringify({
      name: "Employer Brand Messaging Test",
      persona_ids: personaIds.map((p) => p.id),
      questions: [
        "How authentic does this employer message feel on a scale of 1-10?",
        "Would this message make you more or less likely to apply? Why?",
        "What specific claim feels most credible? Least credible?",
        "How would you describe this company's culture to a friend?",
      ],
      context: EMPLOYER_MESSAGE,
      responses_per_persona: 3,
    }),
  }).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;
  }

  for (const resp of data.responses || []) {
    const label = personaIds.find((p) => p.id === resp.persona_id)?.label || "?";
    console.log(`\n[${label}] ${(resp.question || "").slice(0, 60)}`);
    console.log(`  → ${(resp.answer || "").slice(0, 250)}`);
  }
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "personas_created": 6,
  "segments": [
    { "source": "LinkedIn", "outcome": "hired", "n": 82 },
    { "source": "LinkedIn", "outcome": "rejected", "n": 234 },
    { "source": "Referral", "outcome": "hired", "n": 45 },
    { "source": "Referral", "outcome": "rejected", "n": 67 },
    { "source": "Careers Page", "outcome": "hired", "n": 31 },
    { "source": "Careers Page", "outcome": "rejected", "n": 112 }
  ],
  "focus_group_sample": [
    {
      "persona": "LinkedIn → Rejected",
      "question": "How authentic does this employer message feel?",
      "answer": "5/10. 'Unlimited PTO' is a red flag — usually means nobody actually takes it. The engineer quote feels planted."
    },
    {
      "persona": "Referral → Hired",
      "question": "Would this make you more likely to apply?",
      "answer": "More likely. The 'own their roadmap' line matches what my referrer told me. Consistency matters."
    }
  ]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="Rate limit (50 req/10 sec)">Greenhouse returns `429` with a `Retry-After` header. The code sleeps 10 seconds on 429. For bulk pulls (1000+ candidates), add exponential backoff and paginate with `per_page=100`.</Accordion>
  <Accordion title="Missing source data">Not all applications have a `source` object. The code defaults to `"Unknown"`. Check your Greenhouse → Configure → Sources to ensure sources are assigned to all job boards.</Accordion>
  <Accordion title="Application status values">Valid statuses: `active`, `rejected`, `hired`. Custom stages show under `current_stage` rather than top-level `status`.</Accordion>
  <Accordion title="Auth encoding">Greenhouse uses HTTP Basic with the API key as username and an empty password. Always encode as `base64(key + ':')` — the trailing colon is required.</Accordion>
</AccordionGroup>
