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 Shopify store has thousands of customers with varying purchase histories. You pull customer records with order counts and total spend via GraphQL, segment them into cohorts (new buyers with 1 order, returning buyers with 2–5 orders, VIP buyers with 6+ orders or $500+ spend), and create a Mavera persona for each cohort.

Architecture

Code

import os, requests, time

STORE = os.environ["SHOPIFY_STORE"]
TOKEN = os.environ["SHOPIFY_ACCESS_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
SH = f"https://{STORE}.myshopify.com/admin/api/2024-10/graphql.json"
SH_H = {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"}
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

QUERY = """{
  customers(first: 100, sortKey: UPDATED_AT, reverse: true) {
    edges { node {
      id displayName email ordersCount
      amountSpent { amount currencyCode }
      defaultAddress { country provinceCode }
    }}
    pageInfo { hasNextPage endCursor }
  }
}"""

def fetch_customers():
    customers, cursor = [], None
    while True:
        q = QUERY if not cursor else QUERY.replace("first: 100", f'first: 100, after: "{cursor}"')
        resp = requests.post(SH, json={"query": q}, headers=SH_H)
        resp.raise_for_status()
        data = resp.json()["data"]["customers"]
        customers.extend(e["node"] for e in data["edges"])
        if not data["pageInfo"]["hasNextPage"]:
            break
        cursor = data["edges"][-1]["node"]["id"]
        time.sleep(0.5)
    return customers

def segment(customers):
    cohorts = {"new": [], "returning": [], "vip": []}
    for c in customers:
        spend = float(c["amountSpent"]["amount"])
        if c["ordersCount"] >= 6 or spend >= 500:
            cohorts["vip"].append(c)
        elif c["ordersCount"] >= 2:
            cohorts["returning"].append(c)
        else:
            cohorts["new"].append(c)
    return cohorts

customers = fetch_customers()
labels = {"new": "New Buyer", "returning": "Returning Buyer", "vip": "VIP Customer"}
for name, members in segment(customers).items():
    if not members:
        continue
    avg_orders = sum(m["ordersCount"] for m in members) / len(members)
    avg_spend = sum(float(m["amountSpent"]["amount"]) for m in members) / len(members)
    resp = requests.post(f"{MB}/personas", json={
        "name": f"Shopify {labels[name]}",
        "description": f"{labels[name]}: {len(members)} customers, avg {avg_orders:.1f} orders, avg ${avg_spend:.0f} spend.",
        "demographic": {"custom_attributes": {"cohort": name, "avg_order_count": round(avg_orders, 1)}},
    }, headers=MH)
    resp.raise_for_status()
    p = resp.json()
    print(f"Created '{p['name']}' (id={p['id']}) — {len(members)} customers")

Example Output

{
  "id": "persona_8f3a1b2c",
  "name": "Shopify VIP Customer",
  "description": "VIP Customer: 47 customers, avg 9.3 orders, avg $812 spend.",
  "demographic": { "custom_attributes": { "cohort": "vip", "avg_order_count": 9.3 } },
  "created_at": "2025-03-17T14:22:08Z"
}

Error Handling

Shopify returns a THROTTLED error in extensions.cost when you exceed your point budget. Read throttleStatus.currentlyAvailable from each response. When available points fall below query cost, sleep for (cost - available) / restore_rate seconds. Standard stores restore at 50 pts/sec; Plus at 500 pts/sec.
If a persona with the same name exists, the API returns 422. Fetch existing personas with GET /api/v1/personas?name=Shopify+VIP+Customer first, and use PATCH /api/v1/personas/{id} to update instead.