Scenario
Your HubSpot portal has contacts across lifecycle stages — subscriber, lead, MQL, SQL, customer, evangelist. Each stage represents a distinct mindset. You create a Mavera persona per stage so every generated or tested piece of content speaks to the right funnel position.Architecture
Code
import os, requests, time
HS = os.environ["HUBSPOT_ACCESS_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
STAGES = {
"subscriber": "Subscriber", "lead": "Lead",
"marketingqualifiedlead": "MQL", "salesqualifiedlead": "SQL",
"customer": "Customer", "evangelist": "Evangelist",
}
def search_stage(stage, limit=50):
r = requests.post(
"https://api.hubapi.com/crm/v3/objects/contacts/search",
headers={"Authorization": f"Bearer {HS}"},
json={
"filterGroups": [{"filters": [
{"propertyName": "lifecyclestage", "operator": "EQ", "value": stage}
]}],
"properties": ["jobtitle", "company", "industry"],
"limit": limit,
},
)
if r.status_code == 429:
time.sleep(1); return search_stage(stage, limit)
r.raise_for_status()
return r.json().get("results", [])
personas = []
for stage, label in STAGES.items():
contacts = search_stage(stage)
if not contacts: continue
titles = list({c["properties"].get("jobtitle","") for c in contacts if c["properties"].get("jobtitle")})[:5]
industries = list({c["properties"].get("industry","") for c in contacts if c["properties"].get("industry")})[:3]
r = requests.post(
"https://app.mavera.io/api/v1/personas",
headers={"Authorization": f"Bearer {MV}", "Content-Type": "application/json"},
json={
"name": f"HubSpot {label}",
"description": f"{label} stage. Roles: {', '.join(titles[:3])}. Industries: {', '.join(industries)}. N={len(contacts)}.",
"demographic": {"job_titles": titles, "industries": industries},
"psychographic": {"mindset": f"Typical {label.lower()} intent level"},
},
)
r.raise_for_status()
personas.append({"stage": label, "id": r.json()["id"], "n": len(contacts)})
print(f"{label}: {r.json()['id']} ({len(contacts)} contacts)")
time.sleep(0.3)
const HS = process.env.HUBSPOT_ACCESS_TOKEN;
const MV = process.env.MAVERA_API_KEY;
const STAGES = {
subscriber: "Subscriber", lead: "Lead",
marketingqualifiedlead: "MQL", salesqualifiedlead: "SQL",
customer: "Customer", evangelist: "Evangelist",
};
async function searchStage(stage, limit = 50) {
const res = await fetch("https://api.hubapi.com/crm/v3/objects/contacts/search", {
method: "POST",
headers: { Authorization: `Bearer ${HS}`, "Content-Type": "application/json" },
body: JSON.stringify({
filterGroups: [{ filters: [
{ propertyName: "lifecyclestage", operator: "EQ", value: stage },
]}],
properties: ["jobtitle", "company", "industry"],
limit,
}),
});
if (res.status === 429) { await new Promise(r => setTimeout(r, 1000)); return searchStage(stage, limit); }
if (!res.ok) throw new Error(`HubSpot ${res.status}`);
return (await res.json()).results || [];
}
const personas = [];
for (const [stage, label] of Object.entries(STAGES)) {
const contacts = await searchStage(stage);
if (!contacts.length) continue;
const titles = [...new Set(contacts.map(c => c.properties.jobtitle).filter(Boolean))].slice(0, 5);
const industries = [...new Set(contacts.map(c => c.properties.industry).filter(Boolean))].slice(0, 3);
const res = await fetch("https://app.mavera.io/api/v1/personas", {
method: "POST",
headers: { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" },
body: JSON.stringify({
name: `HubSpot ${label}`,
description: `${label} stage. Roles: ${titles.slice(0,3).join(", ")}. Industries: ${industries.join(", ")}. N=${contacts.length}.`,
demographic: { job_titles: titles, industries },
psychographic: { mindset: `Typical ${label.toLowerCase()} intent level` },
}),
});
if (!res.ok) throw new Error(`Mavera ${res.status}`);
const persona = await res.json();
personas.push({ stage: label, id: persona.id, n: contacts.length });
console.log(`${label}: ${persona.id} (${contacts.length} contacts)`);
await new Promise(r => setTimeout(r, 300));
}
Example Output
[ { "stage": "Subscriber", "id": "per_abc123", "n": 1240 },
{ "stage": "Lead", "id": "per_def456", "n": 890 },
{ "stage": "MQL", "id": "per_ghi789", "n": 340 },
{ "stage": "SQL", "id": "per_jkl012", "n": 120 },
{ "stage": "Customer", "id": "per_mno345", "n": 560 },
{ "stage": "Evangelist", "id": "per_pqr678", "n": 45 } ]
Error Handling
CRM Search rate limit (4 req/sec)
CRM Search rate limit (4 req/sec)
The code retries on 429 with 1s backoff. For large portals, add exponential backoff or paginate with
after cursor.Empty lifecycle stages
Empty lifecycle stages
Not all portals use all 6 stages. Custom stages need the internal name. Check with
GET /crm/v3/properties/contacts/lifecyclestage.