Skip to main content

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.

Scenario

Your Google Ads campaigns expose which age ranges and genders actually convert — not who you think your buyer is, but who Google proves is buying. You pull gender_view and age_range_view with conversion metrics, then create or update Mavera personas that match your real converting demographics. The result is a persona library calibrated to paid data.

Architecture

Code

import os, requests, time
from google.ads.googleads.client import GoogleAdsClient

MV = os.environ["MAVERA_API_KEY"]
CUSTOMER_ID = os.environ["GOOGLE_ADS_CUSTOMER_ID"]
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

client = GoogleAdsClient.load_from_env()
ga_service = client.get_service("GoogleAdsService")

gender_query = """
    SELECT
        ad_group_criterion.gender.type,
        metrics.impressions, metrics.clicks, metrics.conversions,
        metrics.cost_micros, metrics.conversions_value
    FROM gender_view
    WHERE segments.date DURING LAST_90_DAYS
    ORDER BY metrics.conversions DESC
"""

age_query = """
    SELECT
        ad_group_criterion.age_range.type,
        metrics.impressions, metrics.clicks, metrics.conversions,
        metrics.cost_micros, metrics.conversions_value
    FROM age_range_view
    WHERE segments.date DURING LAST_90_DAYS
    ORDER BY metrics.conversions DESC
"""

gender_data = {}
for row in ga_service.search(customer_id=CUSTOMER_ID, query=gender_query):
    g = row.ad_group_criterion.gender.type_.name
    gender_data.setdefault(g, {"impressions": 0, "clicks": 0, "conversions": 0, "value": 0})
    gender_data[g]["impressions"] += row.metrics.impressions
    gender_data[g]["clicks"] += row.metrics.clicks
    gender_data[g]["conversions"] += row.metrics.conversions
    gender_data[g]["value"] += row.metrics.conversions_value

age_data = {}
for row in ga_service.search(customer_id=CUSTOMER_ID, query=age_query):
    a = row.ad_group_criterion.age_range.type_.name
    age_data.setdefault(a, {"impressions": 0, "clicks": 0, "conversions": 0, "value": 0})
    age_data[a]["impressions"] += row.metrics.impressions
    age_data[a]["clicks"] += row.metrics.clicks
    age_data[a]["conversions"] += row.metrics.conversions
    age_data[a]["value"] += row.metrics.conversions_value

total_conv = sum(d["conversions"] for d in age_data.values()) or 1

top_ages = sorted(age_data.items(), key=lambda x: x[1]["conversions"], reverse=True)[:3]
top_gender = sorted(gender_data.items(), key=lambda x: x[1]["conversions"], reverse=True)[:2]

created = []
for age_name, age_metrics in top_ages:
    for gender_name, gender_metrics in top_gender:
        conv_share = (age_metrics["conversions"] + gender_metrics["conversions"]) / (2 * total_conv)
        if conv_share < 0.05:
            continue

        cpa = ((age_metrics.get("cost_micros", 0) or 0) / 1_000_000) / max(age_metrics["conversions"], 1)
        persona = requests.post(f"{MB}/personas", headers=MH, json={
            "name": f"Google Ads: {gender_name.replace('_', ' ').title()} {age_name.replace('AGE_RANGE_', '').replace('_', '-')}",
            "description": (
                f"Data-backed persona from Google Ads (last 90 days). "
                f"Age: {age_name.replace('AGE_RANGE_', '').replace('_', '-')}. Gender: {gender_name}. "
                f"Conversions: {age_metrics['conversions']:.0f} ({age_metrics['conversions']/total_conv:.0%} of total). "
                f"CPA: ${cpa:.2f}. Conv value: ${age_metrics['value']:.0f}."
            ),
            "demographic": {
                "age_range": age_name.replace("AGE_RANGE_", "").replace("_", "-"),
                "gender": gender_name.lower(),
            },
        }).json()
        created.append({"name": persona.get("name"), "id": persona["id"]})
        time.sleep(0.3)

print(f"Created {len(created)} demographic personas")
for p in created:
    print(f"  {p['name']}{p['id']}")

Example Output

{
  "created": 4,
  "personas": [
    { "name": "Google Ads: Male 25-34", "id": "per_gads_m25", "conv_share": "34%", "cpa": "$12.40" },
    { "name": "Google Ads: Female 25-34", "id": "per_gads_f25", "conv_share": "22%", "cpa": "$15.80" },
    { "name": "Google Ads: Male 35-44", "id": "per_gads_m35", "conv_share": "18%", "cpa": "$18.20" },
    { "name": "Google Ads: Female 35-44", "id": "per_gads_f35", "conv_share": "12%", "cpa": "$21.50" }
  ]
}

Error Handling

Google can’t classify all users. UNDETERMINED often has high volume. Exclude it from persona creation but monitor its conversion share.
With fewer than 30 conversions per segment, data is noisy. Use 90-day windows or aggregate across campaigns for statistical significance.
Check existing personas with GET /api/v1/personas?search=Google Ads before creating. Use PATCH to update demographics instead.

All Google Ads jobs

View all 7 Google Ads integration jobs

Personas API

Full reference for POST /api/v1/personas