Scenario
Close organizes contacts under Leads (companies/accounts). You want to auto-create Mavera Custom Personas for each ICP segment based on lead data — industry, company size, title patterns — keeping your persona library current with your actual pipeline.Architecture
Code
import os, requests
from requests.auth import HTTPBasicAuth
from collections import defaultdict
CLOSE_KEY = os.environ["CLOSE_API_KEY"]
CLOSE_AUTH = HTTPBasicAuth(CLOSE_KEY, "")
MAVERA_KEY = os.environ["MAVERA_API_KEY"]
def close_get(path, params=None):
r = requests.get(f"https://api.close.com/api/v1{path}", auth=CLOSE_AUTH, params=params or {})
r.raise_for_status()
return r.json()
# 1. Pull leads with contacts
leads = close_get("/lead/", {"_limit": 200, "_fields": "id,display_name,contacts,custom"}).get("data", [])
# 2. Segment by ICP
segments = defaultdict(list)
for lead in leads:
custom = lead.get("custom", {})
industry = custom.get("Industry", custom.get("industry", "General"))
title = (lead.get("contacts", [{}])[0]).get("title", "Unknown") if lead.get("contacts") else "Unknown"
role = "IC"
tl = title.lower()
if any(t in tl for t in ["cto", "ceo", "cfo", "vp", "chief", "head"]):
role = "Executive"
elif any(t in tl for t in ["director", "manager", "lead"]):
role = "Manager"
segments[f"{industry} | {role}"].append({
"company": lead.get("display_name", "Unknown"), "title": title,
"size": custom.get("Company Size", "Unknown"),
})
# 3. Create personas in Mavera
created = 0
for seg_key, members in segments.items():
if len(members) < 2:
continue
industry, role = seg_key.split(" | ")
titles = list({m["title"] for m in members if m["title"] != "Unknown"})[:3]
companies = [m["company"] for m in members][:3]
resp = requests.post(
"https://app.mavera.io/api/v1/personas",
headers={"Authorization": f"Bearer {MAVERA_KEY}"},
json={
"name": f"{industry} {role}",
"description": (
f"A {role.lower()}-level buyer in {industry}. "
f"Based on {len(members)} leads. Titles: {', '.join(titles)}. "
f"Companies: {', '.join(companies)}. Size: {members[0]['size']}."
),
"metadata": {"source": "close_crm", "segment": seg_key, "lead_count": len(members)},
},
)
if resp.ok:
print(f"Created: {resp.json().get('name')} ({len(members)} leads)")
created += 1
else:
print(f"Failed for {seg_key}: {resp.status_code}")
print(f"\nTotal personas created: {created}")
const CLOSE_KEY = process.env.CLOSE_API_KEY;
const CLOSE_AUTH = "Basic " + Buffer.from(`${CLOSE_KEY}:`).toString("base64");
const MAVERA_KEY = process.env.MAVERA_API_KEY;
async function closeGet(path, params = {}) {
const url = new URL(`https://api.close.com/api/v1${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { Authorization: CLOSE_AUTH } });
if (!res.ok) throw new Error(`Close ${res.status}`);
return res.json();
}
// 1. Pull leads
const leads = (await closeGet("/lead/", { _limit: 200, _fields: "id,display_name,contacts,custom" })).data || [];
// 2. Segment by ICP
const segments = {};
for (const lead of leads) {
const custom = lead.custom || {};
const industry = custom.Industry || custom.industry || "General";
const title = lead.contacts?.[0]?.title || "Unknown";
const tl = title.toLowerCase();
let role = "IC";
if (["cto", "ceo", "cfo", "vp", "chief", "head"].some((t) => tl.includes(t))) role = "Executive";
else if (["director", "manager", "lead"].some((t) => tl.includes(t))) role = "Manager";
const key = `${industry} | ${role}`;
(segments[key] ||= []).push({ company: lead.display_name || "Unknown", title, size: custom["Company Size"] || "Unknown" });
}
// 3. Create personas
let created = 0;
for (const [segKey, members] of Object.entries(segments)) {
if (members.length < 2) continue;
const [industry, role] = segKey.split(" | ");
const titles = [...new Set(members.map((m) => m.title).filter((t) => t !== "Unknown"))].slice(0, 3);
const companies = members.slice(0, 3).map((m) => m.company);
const res = await fetch("https://app.mavera.io/api/v1/personas", {
method: "POST",
headers: { Authorization: `Bearer ${MAVERA_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
name: `${industry} ${role}`,
description: `A ${role.toLowerCase()}-level buyer in ${industry}. ${members.length} leads. ` +
`Titles: ${titles.join(", ")}. Companies: ${companies.join(", ")}.`,
metadata: { source: "close_crm", segment: segKey, lead_count: members.length },
}),
});
if (res.ok) { console.log(`Created: ${(await res.json()).name} (${members.length} leads)`); created++; }
else console.error(`Failed for ${segKey}: ${res.status}`);
}
console.log(`\nTotal created: ${created}`);
Example Output
Created: SaaS Executive (12 leads)
Created: SaaS Manager (8 leads)
Created: Healthcare Executive (5 leads)
Created: FinTech IC (15 leads)
Created: Manufacturing Manager (4 leads)
Created: E-Commerce Executive (7 leads)
Total personas created: 6
Error Handling
| Error | Cause | Fix |
|---|---|---|
| Custom fields missing | Field keys vary by org | List via GET /custom_field/lead/ and map dynamically |
| Duplicate personas | Running sync repeatedly | Check existing personas via GET /personas first; update instead |
| Empty contacts | Lead has no contacts | Skip or use company-level data only |
| Sparse segments | Too-granular custom fields | Set minimum threshold (3+ leads per segment) |