Skip to content
ExponaBLOG
PAYMENT ENGINEERING & STRIPE INTEGRATION · DAY 068 / 100

Payment analytics: a practical Payments & Stripe guide

Measure the health of your revenue.

Expona Admin10 min read

Introduction

You've built payment infrastructure, handled Stripe webhooks, and deployed to production—but do you actually know if your payments are healthy? Payment analytics: a practical Payments & Stripe guide moves beyond transaction logs into the metrics that matter: authorization rates, churn, recovery, and disputes. These numbers tell you whether revenue is flowing smoothly or if silent leaks are draining your bottom line. Over the next 68 days and beyond, measuring payment health becomes as routine as monitoring latency or error rates.

Without visibility into payment performance, you're flying blind. A 2% dip in authorization rate across 10,000 daily transactions means 200 lost sales per day—$6,000 per month on a $30 average order. Churn that accelerates by 5 percentage points can erase months of growth. Disputes that spike in March might point to a product quality issue you haven't noticed yet. This guide walks through the metrics you need to track, how to calculate them from Stripe data, and what thresholds should trigger investigation.

Authorization Rate—Share of Attempts That Succeed

Authorization rate is your first-line health indicator: the percentage of payment attempts that succeed on the first try. It combines card decline rate, processor response, fraud detection, and 3D Secure friction into a single number.

A healthy SaaS or e-commerce business typically runs 95–99% authorization rate. Dips below 93% warrant immediate investigation. Every percentage point lost represents real revenue walking out the door.

How to measure it:

typescript
async function getAuthorizationRate(days = 30) {
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - days);

  // Fetch all payment intents from Stripe
  const paymentIntents = await stripe.paymentIntents.list({
    created: {
      gte: Math.floor(startDate.getTime() / 1000),
    },
    limit: 100, // Paginate in production
  });

  let succeeded = 0;
  let total = 0;

  paymentIntents.data.forEach((pi) => {
    // Count attempts (retries via automatic retry rules count separately)
    total++;
    if (pi.status === "succeeded") {
      succeeded++;
    }
  });

  const authRate = (succeeded / total) * 100;
  console.log(`Authorization Rate (${days}d): ${authRate.toFixed(2)}%`);
  return { authRate, total, succeeded };
}

In real production, you'll want to slice this by:

  • Payment method: Card, bank transfer, digital wallet. Card often has lower rates in high-fraud verticals.
  • Card network: Visa, Mastercard, Amex. Amex may have stricter fraud rules.
  • Geography: Certain regions see higher decline rates due to currency, local fraud patterns, or issuer rules.
  • Customer segment: New vs. returning. New customers always decline more often due to fraud velocity checks.

A practical scenario: you notice authorization rate dropped from 97% to 94% last week. Your fraud detection rules recently became stricter to combat a surge in chargebacks. You now have two options: relax the rules slightly (accepting modest chargeback growth) or accept lower conversion (and investigate whether customer support can help at-risk orders). The data lets you make that trade-off consciously rather than reactively.

MRR and Churn—For Subscription Businesses

Monthly Recurring Revenue (MRR) and churn rate are the twin pillars of subscription health. MRR is the predictable revenue you collect each month; churn is the percentage of customers who stop paying.

MRR calculation:

typescript
async function calculateMRR(month: string) {
  // month format: "2024-03"
  const [year, monthNum] = month.split("-");
  const startOfMonth = new Date(parseInt(year), parseInt(monthNum) - 1, 1);
  const endOfMonth = new Date(parseInt(year), parseInt(monthNum), 0, 23, 59, 59);

  const subscriptions = await stripe.subscriptions.list({
    limit: 100, // Paginate in production
    expand: ["data.items"],
  });

  let mrr = 0;

  subscriptions.data.forEach((sub) => {
    // Check if subscription was active during this month
    const createdDate = new Date(sub.created * 1000);
    const currentPeriodEnd = new Date(sub.current_period_end * 1000);

    if (
      createdDate <= endOfMonth &&
      currentPeriodEnd >= startOfMonth &&
      sub.status === "active"
    ) {
      // Sum the monthly amount of all items
      sub.items.data.forEach((item) => {
        if (item.plan.interval === "month") {
          mrr += item.plan.amount / 100; // Convert cents to dollars
        }
      });
    }
  });

  console.log(`MRR for ${month}: $${mrr.toFixed(2)}`);
  return mrr;
}

Churn rate calculation:

typescript
async function getChurnRate(days = 30) {
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - days);

  const cancelledSubs = await stripe.subscriptions.list({
    status: "canceled",
    created: {
      gte: Math.floor(startDate.getTime() / 1000),
    },
    limit: 100,
  });

  // For accurate churn, count subscriptions that were active at period start
  const allSubs = await stripe.subscriptions.list({
    limit: 100,
  });

  const activeAtPeriodStart = allSubs.data.filter((sub) => {
    const createdAt = new Date(sub.created * 1000);
    return createdAt < startDate;
  }).length;

  const churnRate = (cancelledSubs.data.length / activeAtPeriodStart) * 100;
  console.log(`Churn Rate (${days}d): ${churnRate.toFixed(2)}%`);
  return churnRate;
}

A healthy subscription business runs 5–7% monthly churn. SaaS targeting enterprises often sees 2–3% (sticky); B2C or free-tier-heavy products might see 10–15%. The key is consistency: if your churn was stable at 6% for six months, then suddenly spikes to 9%, something broke. Was there a recent price increase? Product regression? Competitive loss? The spike itself is data.

Real scenario: You release a feature that auto-expires trial subscriptions after 14 days. For the first time, you see churn tick up from 6% to 8%. Your product team asks whether to revert it. But looking deeper, you notice that while overall churn rose, the churn of customers who actually used the feature during their trial dropped by 3 percentage points. The issue is that dormant trials expire; engaged users stay. You keep the change and focus retention efforts on activation during trial, not on fighting expiration.

Failed Payment Recovery—How Much Dunning Recovers

For subscriptions, failed payments are inevitable: expired cards, insufficient funds, account closure. Dunning—automated retry and customer communication—recovers a significant portion. Tracking recovery rate tells you whether your dunning strategy is working.

Recovery calculation:

typescript
async function getRecoveryRate(days = 30) {
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - days);

  // List all invoices in this period
  const invoices = await stripe.invoices.list({
    created: {
      gte: Math.floor(startDate.getTime() / 1000),
    },
    limit: 100,
  });

  let totalFailed = 0;
  let recovered = 0;

  invoices.data.forEach((invoice) => {
    // Count auto-charged invoice failures (subscription renewal)
    if (invoice.status === "open" || invoice.status === "draft") {
      return; // Not attempted yet
    }

    if (invoice.paid === false) {
      totalFailed++;

      // Check if this invoice was later paid (recovered)
      // In production, track invoice payment history more carefully
      if (invoice.status === "paid") {
        recovered++;
      }
    }
  });

  const recoveryRate = totalFailed > 0 ? (recovered / totalFailed) * 100 : 0;
  console.log(
    `Recovery Rate (${days}d): ${recoveryRate.toFixed(2)}% of ${totalFailed} failures`
  );
  return { recoveryRate, recovered, totalFailed };
}

In practice, strong dunning strategies recover 60–80% of initial failures. Each retry over 3–4 days catches customers who had temporary payment issues (card flagged, low balance, wrong address). Beyond that, email and push notification can nudge customer updates, adding another 5–10% recovery.

A practical example: you run a fitness subscription at $30/month. Your dunning catches 70% of failures via automatic retry. Of the remaining 30% (permanent failures), email outreach recovers another 8%. That means 78% of what would have churned stays active. On 10,000 active subscribers with a typical 3% monthly failure rate (300 failures), you recover ~234 customers. That's $7,020 in recovered MRR.

Refund and Dispute Rates—Early Warning Signs

Refund rate and chargeback/dispute rate are both financial leaks and signals of deeper problems. A spike in either often precedes customer complaints, product issues, or fraud.

Refund rate calculation:

typescript
async function getRefundRate(days = 30) {
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - days);

  const charges = await stripe.charges.list({
    created: {
      gte: Math.floor(startDate.getTime() / 1000),
    },
    limit: 100,
  });

  let totalCharges = 0;
  let refundedAmount = 0;
  let refundedCount = 0;
  let successfulAmount = 0;

  charges.data.forEach((charge) => {
    if (charge.status === "succeeded" && charge.paid) {
      totalCharges++;
      successfulAmount += charge.amount;

      if (charge.refunded) {
        refundedCount++;
        refundedAmount += charge.amount_refunded;
      }
    }
  });

  const refundRate = (refundedCount / totalCharges) * 100;
  const refundValue = (refundedAmount / successfulAmount) * 100;

  console.log(`Refund Rate (${days}d): ${refundRate.toFixed(2)}% of transactions`);
  console.log(`Refund Value (${days}d): ${refundValue.toFixed(2)}% of revenue`);
  return { refundRate, refundValue, refundedCount };
}

Dispute rate calculation:

typescript
async function getDisputeRate(days = 30) {
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - days);

  const disputes = await stripe.disputes.list({
    created: {
      gte: Math.floor(startDate.getTime() / 1000),
    },
    limit: 100,
  });

  const charges = await stripe.charges.list({
    created: {
      gte: Math.floor(startDate.getTime() / 1000),
    },
    limit: 100,
  });

  const successfulCharges = charges.data.filter(
    (c) => c.status === "succeeded"
  ).length;
  const disputeCount = disputes.data.length;

  const disputeRate = (disputeCount / successfulCharges) * 100;
  console.log(`Dispute Rate (${days}d): ${disputeRate.toFixed(2)}%`);
  return disputeRate;
}

Healthy benchmarks:

  • Refund rate: 1–3% for most e-commerce and SaaS. Digital goods and high-value items often run higher.
  • Dispute rate: Under 0.5% for established businesses. Above 1% and payment processors begin scrutinizing your account.

Both metrics tend to spike for predictable reasons. A refund surge often follows a product bug (bad release), customer support issue, or seasonal return spike. Disputes spike when:

  • Billing descriptor is unclear (customer doesn't recognize the charge).
  • Shipping or delivery fails.
  • Fraud increases (you're being targeted).
  • Customer service is slow to respond.

In a real scenario, you notice disputes rising from 0.3% to 0.8% over a month. Your team investigates and finds that a recent marketing campaign for a subscription trial didn't clearly state the card requirement or trial end date. Customers signed up, forgot about the trial, and disputed the renewal charge. You fix the signup flow and add a reminder email five days before renewal. Disputes return to baseline within two weeks.

Common Mistakes

  • Measuring only success, not failure: Authorization rate alone misses the full picture. Track decline reasons (insufficient funds, lost card, fraud filter) to spot systemic issues.
  • Ignoring cohort differences: New customers, high-risk regions, and certain card networks all have different characteristics. Aggregate metrics can hide critical segments.
  • Confusing refund and churn: A refund is a single transaction reversal; churn is a subscription cancellation. They have different causes and require different fixes.
  • Setting static thresholds: "Alert if disputes exceed 0.5%" works until your volume grows 10x. Build alerting relative to baseline or trend, not absolute numbers.
  • Not connecting metrics to business impact: "Churn is 7%" is a number. "Churn is 7%, costing us $50k in lost MRR" is a number that drives action.
  • Overlooking Stripe event delays: Stripe webhooks arrive in order but may be delivered seconds or minutes later. Real-time dashboards can lag. Always validate with Stripe API queries for critical decisions.

Key Takeaways

  • Authorization rate is your first-line health check; drops below 93% demand investigation into decline reasons, fraud rules, or processor issues.
  • MRR and churn reveal subscription business health; stable churn with growing MRR signals a healthy funnel; rising churn often indicates product, pricing, or onboarding problems.
  • Failed payment recovery (dunning effectiveness) directly recovers 60–80% of subscription payment failures; tracking this metric quantifies the value of your retry strategy.
  • Refund and dispute spikes are early warning signals for product issues, unclear billing, or fraud; investigate root cause within days, not weeks.
  • Slice metrics by cohort (new vs. returning, geography, card network) to spot problems hidden in aggregate numbers.
  • Connect metrics to business impact in dollars: each 1% of authorization rate loss, 1% churn increase, or 0.1% dispute spike has a quantifiable cost to your bottom line.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles