Scenario
Pull subscriptions withstatus=past_due or status=canceled along with payment failure history. Extract traits — plan type, tenure, failed attempts — and feed into Mavera for personalized retention messaging.
Architecture
Code
import os, time, stripe, requests
stripe.api_key = os.environ["STRIPE_API_KEY"]
MAVERA_API_KEY = os.environ["MAVERA_API_KEY"]
MAVERA_BASE = "https://app.mavera.io/api/v1"
def get_at_risk_subscriptions():
at_risk = []
for status in ["past_due", "canceled"]:
subs = stripe.Subscription.list(limit=50, status=status, expand=["data.customer"])
for sub in subs.auto_paging_iter():
invoices = stripe.Invoice.list(subscription=sub.id, limit=10)
at_risk.append({
"customer_email": sub.customer.email,
"plan": sub["items"].data[0].price.nickname or sub["items"].data[0].price.id,
"status": status,
"tenure_days": (int(time.time()) - sub.start_date) // 86400,
"payment_failures": sum(1 for inv in invoices.data if inv.status == "uncollectible"),
})
return at_risk
def generate_retention_content(profiles):
summary = "\n".join(
f"- {p['customer_email']}: {p['plan']}, {p['tenure_days']}d, {p['payment_failures']} failures"
for p in profiles[:20])
resp = requests.post(f"{MAVERA_BASE}/generations", json={
"prompt": f"Generate retention emails for at-risk customers:\n{summary}\n\nProduce subject line + 2-sentence body per profile.",
"max_tokens": 2000,
}, headers={"Authorization": f"Bearer {MAVERA_API_KEY}"})
resp.raise_for_status()
return resp.json()
profiles = get_at_risk_subscriptions()
print(f"Found {len(profiles)} at-risk subscriptions")
print(generate_retention_content(profiles)["text"])
const STRIPE_API_KEY = process.env.STRIPE_API_KEY;
const MAVERA_API_KEY = process.env.MAVERA_API_KEY;
const MAVERA_BASE = "https://app.mavera.io/api/v1";
async function stripeGet(path, params = {}) {
const url = new URL(`https://api.stripe.com/v1/${path}`);
Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, v));
const res = await fetch(url, { headers: { Authorization: `Bearer ${STRIPE_API_KEY}` } });
if (!res.ok) throw new Error(`Stripe ${res.status}: ${await res.text()}`);
return res.json();
}
async function getAtRiskSubscriptions() {
const atRisk = [];
for (const status of ["past_due", "canceled"]) {
const data = await stripeGet("subscriptions", { limit: "50", status, "expand[]": "data.customer" });
for (const sub of data.data) {
const tenureDays = Math.floor((Date.now() / 1000 - sub.start_date) / 86400);
const invoices = await stripeGet("invoices", { subscription: sub.id, limit: "10" });
atRisk.push({
customer_email: sub.customer.email,
plan: sub.items.data[0].price.nickname || sub.items.data[0].price.id,
status, tenure_days: tenureDays,
payment_failures: invoices.data.filter((i) => i.status === "uncollectible").length,
});
}
}
return atRisk;
}
async function generateRetentionContent(profiles) {
const summary = profiles.slice(0, 20).map((p) =>
`- ${p.customer_email}: ${p.plan}, ${p.tenure_days}d, ${p.payment_failures} failures, ${p.status}`
).join("\n");
const res = await fetch(`${MAVERA_BASE}/generations`, {
method: "POST",
headers: { Authorization: `Bearer ${MAVERA_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
prompt: `Generate retention email variants for at-risk customers:\n${summary}\n\nProduce a subject line and 2-sentence body per profile.`,
max_tokens: 2000,
}),
});
if (!res.ok) throw new Error(`Mavera ${res.status}: ${await res.text()}`);
return res.json();
}
(async () => {
const profiles = await getAtRiskSubscriptions();
console.log(`Found ${profiles.length} at-risk subscriptions`);
console.log((await generateRetentionContent(profiles)).text);
})();
Example Output
Subject: "We miss you, Sarah — here's 30% off your next quarter"
Body: You've been with us for 14 months and we noticed your recent payment
didn't go through. Reply for a dedicated account review and 30% off.
Subject: "Your Pro plan is paused — let's fix that"
Body: After 8 months on Pro, we know switching tools is a pain. One-click
reactivation below with your settings intact, plus a free month.
Error Handling
Stripe: Subscription expand depth error
Stripe: Subscription expand depth error
Stripe limits expansion depth. For nested data like
customer.default_payment_method, make a separate stripe.Customer.retrieve() call.Mavera: Generation prompt too long (413)
Mavera: Generation prompt too long (413)
Prompts capped at 8,000 tokens. Batch profiles into groups of 20 or summarize into aggregate statistics.