> ## 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.

# Churn Prediction → Retention Content

> Generate personalized retention messaging from Stripe subscription churn and payment failure data

### Scenario

Pull subscriptions with `status=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

```mermaid theme={"dark"}
flowchart LR
A["GET /v1/subscriptions past_due + canceled"] --> B["Extract plan/tenure/failures"] --> C["POST /api/v1/generations"] --> D["Retention email variants"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  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"])
  ```

  ```javascript JavaScript theme={"dark"}
  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);
  })();
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
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

<AccordionGroup>
  <Accordion title="Stripe: Subscription expand depth error">
    Stripe limits expansion depth. For nested data like `customer.default_payment_method`, make a separate `stripe.Customer.retrieve()` call.
  </Accordion>

  <Accordion title="Mavera: Generation prompt too long (413)">
    Prompts capped at 8,000 tokens. Batch profiles into groups of 20 or summarize into aggregate statistics.
  </Accordion>
</AccordionGroup>
