Scenario
You’ve built Custom Audiences in Meta — lookalikes, website visitors, email lists, engagement-based — but you can’t test messaging against those segments without spending ad budget. This job exports Custom Audience definitions, maps each to a Mavera persona that mirrors the targeting criteria, then runs Focus Groups as if you’re interviewing your actual ad audience. Pre-test creative without burning a dollar.Architecture
Code
import os, requests, time
META = os.environ["META_ACCESS_TOKEN"]
ACCT = os.environ["META_AD_ACCOUNT_ID"]
MV = os.environ["MAVERA_API_KEY"]
GRAPH = "https://graph.facebook.com/v24.0"
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
# 1. Pull Custom Audiences
audiences = requests.get(
f"{GRAPH}/{ACCT}/customaudiences",
params={
"access_token": META,
"fields": "id,name,description,subtype,approximate_count,data_source,delivery_status",
"limit": 25,
},
).json().get("data", [])
active_audiences = [a for a in audiences
if a.get("delivery_status", {}).get("status") != "deleted"
and a.get("approximate_count", 0) > 100]
print(f"Active audiences: {len(active_audiences)}")
# 2. Create mirrored personas
persona_map = []
for aud in active_audiences[:6]:
subtype = aud.get("subtype", "CUSTOM")
data_source = aud.get("data_source", {})
source_type = data_source.get("type", "unknown")
source_desc = data_source.get("sub_type", source_type)
if subtype == "LOOKALIKE":
persona_desc = (
f"Lookalike audience: {aud['name']}. ~{aud.get('approximate_count',0):,} users. "
f"Mirrors users similar to your seed audience. Likely shares demographic and "
f"behavioral traits with your best customers but hasn't engaged with your brand yet."
)
elif subtype == "WEBSITE":
persona_desc = (
f"Website visitor audience: {aud['name']}. ~{aud.get('approximate_count',0):,} users. "
f"Has visited your website — already aware of your product. "
f"Evaluating or comparing options. Warm prospect mindset."
)
elif subtype == "ENGAGEMENT":
persona_desc = (
f"Engagement audience: {aud['name']}. ~{aud.get('approximate_count',0):,} users. "
f"Has interacted with your social content. Aware and interested but not yet converted. "
f"Responds to social proof and community-oriented messaging."
)
else:
persona_desc = (
f"Custom audience: {aud['name']}. Source: {source_desc}. "
f"~{aud.get('approximate_count',0):,} users. {aud.get('description','No description.')}"
)
persona = requests.post(f"{MB}/personas", headers=MH, json={
"name": f"Meta Audience: {aud['name'][:50]}",
"description": persona_desc,
"psychographic": {
"audience_type": subtype.lower(),
"awareness_level": "warm" if subtype in ("WEBSITE", "ENGAGEMENT") else "cold",
"source": source_desc,
},
}).json()
persona_map.append({
"audience_id": aud["id"],
"audience_name": aud["name"],
"persona_id": persona["id"],
"subtype": subtype,
"size": aud.get("approximate_count", 0),
})
time.sleep(0.3)
# 3. Run Focus Group with ad creative stimulus
AD_CREATIVE = """
Headline: "Your Competitors Already Know This"
Body: "Teams using centralized analytics make decisions 40% faster. See how in 2 minutes."
CTA: Learn More
Visual: Split-screen showing cluttered dashboards vs clean single-pane view.
"""
fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
"name": "Custom Audience Creative Test",
"persona_ids": [p["persona_id"] for p in persona_map],
"questions": [
f"You see this ad in your Instagram feed:\n\n{AD_CREATIVE}\n\nWhat's your first reaction?",
"Does the headline create curiosity or feel clickbait-y? Why?",
"Rate your likelihood to click (1-10). What would change your rating?",
"What would this ad need to say to make you stop scrolling?",
"If you clicked, what would you expect on the landing page?",
],
"context": "Testing ad creative against different audience segments defined by Meta targeting.",
"responses_per_persona": 2,
}).json()
# 4. Poll and display
for _ in range(24):
time.sleep(5)
data = requests.get(f"{MB}/focus-groups/{fg['id']}", headers=MH).json()
if data.get("status") == "completed":
break
print("\n=== Audience-Mirrored Focus Group ===")
for pm in persona_map:
print(f"\n[{pm['audience_name']}] ({pm['subtype']}, {pm['size']:,} users)")
audience_responses = [r for r in data.get("responses", [])
if r.get("persona_id") == pm["persona_id"]]
for resp in audience_responses[:3]:
print(f" Q: {resp.get('question','')[:60]}...")
print(f" A: {resp.get('answer','')[:200]}")
const META = process.env.META_ACCESS_TOKEN;
const ACCT = process.env.META_AD_ACCOUNT_ID;
const MV = process.env.MAVERA_API_KEY;
const GRAPH = "https://graph.facebook.com/v24.0";
const MB = "https://app.mavera.io/api/v1";
const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
// 1. Pull Custom Audiences
const audiences = await fetch(
`${GRAPH}/${ACCT}/customaudiences?access_token=${META}&fields=id,name,description,subtype,approximate_count,data_source,delivery_status&limit=25`
).then(r => r.json()).then(d => d.data || []);
const activeAudiences = audiences
.filter(a => a.delivery_status?.status !== "deleted" && (a.approximate_count || 0) > 100)
.slice(0, 6);
console.log(`Active audiences: ${activeAudiences.length}`);
// 2. Create mirrored personas
const personaMap = [];
for (const aud of activeAudiences) {
const subtype = aud.subtype || "CUSTOM";
const sourceDesc = aud.data_source?.sub_type || aud.data_source?.type || "unknown";
const descMap = {
LOOKALIKE: `Lookalike: ${aud.name}. ~${(aud.approximate_count || 0).toLocaleString()} users. Similar to seed audience.`,
WEBSITE: `Website visitors: ${aud.name}. ~${(aud.approximate_count || 0).toLocaleString()} users. Aware, evaluating.`,
ENGAGEMENT: `Engaged users: ${aud.name}. ~${(aud.approximate_count || 0).toLocaleString()} users. Interested, not converted.`,
};
const desc = descMap[subtype] || `Custom: ${aud.name}. Source: ${sourceDesc}. ${aud.description || ""}`;
const persona = await fetch(`${MB}/personas`, {
method: "POST", headers: MH,
body: JSON.stringify({
name: `Meta Audience: ${(aud.name || "").slice(0, 50)}`,
description: desc,
psychographic: {
audience_type: subtype.toLowerCase(),
awareness_level: ["WEBSITE", "ENGAGEMENT"].includes(subtype) ? "warm" : "cold",
source: sourceDesc,
},
}),
}).then(r => r.json());
personaMap.push({
audience_id: aud.id, audience_name: aud.name,
persona_id: persona.id, subtype, size: aud.approximate_count || 0,
});
await new Promise(r => setTimeout(r, 300));
}
// 3. Focus Group
const AD_CREATIVE = `Headline: "Your Competitors Already Know This"\nBody: "Teams using centralized analytics make decisions 40% faster."\nCTA: Learn More`;
const fg = await fetch(`${MB}/focus-groups`, {
method: "POST", headers: MH,
body: JSON.stringify({
name: "Custom Audience Creative Test",
persona_ids: personaMap.map(p => p.persona_id),
questions: [
`You see this ad:\n\n${AD_CREATIVE}\n\nFirst reaction?`,
"Curiosity or clickbait? Why?",
"Likelihood to click (1-10)? What would change it?",
"What would make you stop scrolling?",
"If you clicked, what do you expect on the landing page?",
],
context: "Testing creative against Meta targeting segments.",
responses_per_persona: 2,
}),
}).then(r => r.json());
// 4. Poll
let data;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
data = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: MH }).then(r => r.json());
if (data.status === "completed") break;
}
console.log("\n=== Audience-Mirrored Focus Group ===");
for (const pm of personaMap) {
console.log(`\n[${pm.audience_name}] (${pm.subtype}, ${pm.size.toLocaleString()} users)`);
const resps = (data.responses || []).filter(r => r.persona_id === pm.persona_id);
resps.slice(0, 3).forEach(r => {
console.log(` Q: ${(r.question || "").slice(0, 60)}...`);
console.log(` A: ${(r.answer || "").slice(0, 200)}`);
});
}
Example Output
=== Audience-Mirrored Focus Group ===
[High-Value Lookalike 1%] (LOOKALIKE, 2,400,000 users)
Q: You see this ad: ... First reaction?
A: The competitive angle is interesting but I need more specifics. "40% faster" — than what?
Show me who these teams are.
Q: Likelihood to click (1-10)?
A: 6. I'd click if the visual showed a real product, not a generic split-screen mockup.
[Website Visitors — Last 30 Days] (WEBSITE, 45,000 users)
Q: First reaction?
A: I've already been on your site. This headline doesn't tell me anything new.
Show me what's changed since I last visited or offer a specific deal.
Q: What would make you stop scrolling?
A: "Here's what you missed" or a time-limited trial offer. I'm warm — don't treat me
like a cold prospect.
[Email List — Existing Customers] (CUSTOM, 12,000 users)
Q: Curiosity or clickbait?
A: Clickbait for me — I'm already a customer. This should be an upsell or a feature
announcement, not a cold acquisition ad.
Error Handling
Custom Audience permissions
Custom Audience permissions
The
customaudiences edge requires ads_read permission. Some audience types (Customer File) may be restricted by Business Manager privacy settings.Approximate count accuracy
Approximate count accuracy
The
approximate_count field is an estimate and may lag by 24–48 hours. Don’t use it for precise sizing.Persona quality depends on audience definition
Persona quality depends on audience definition
Lookalike and Website audiences produce better personas because the behavioral signal is clearer. CRM-upload audiences need manual description enrichment.
All Meta Ads Jobs
Browse all Meta Ads integration jobs
Focus Groups
Full reference for synthetic focus groups