6 min read

How to Monitor Refund Rate in Stripe

High refund rates are a silent revenue killer. They signal product issues, customer dissatisfaction, or fraud patterns—but most teams don't watch them until it's too late. Stripe gives you the tools to catch refunds as they happen and understand why customers are asking for their money back.

View Refunds Directly in the Stripe Dashboard

The easiest starting point is the Dashboard's built-in reporting.

Navigate to the Refunds Page

In the Stripe Dashboard, go to Payments > Refunds. This page shows all refunds issued, sorted by most recent first. You can filter by date range, amount, status, or refund reason using the filters at the top.

javascript
// Fetch refunds programmatically via API
const stripe = require('stripe')('sk_test_...');

// Get all refunds from the last 30 days
const refunds = await stripe.refunds.list({
  limit: 100,
  created: {
    gte: Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60
  }
});
Fetch refunds from the Stripe API (Node.js)

Track Refund Reason Codes

Click any refund to see the reason. Stripe captures why the customer requested it: requested_by_customer, duplicate, fraudulent, or expired_uncaptured_charge. Group these to spot patterns—if most refunds are marked duplicate, you likely have a payment flow bug.

javascript
// Count refunds by reason
const refunds = await stripe.refunds.list({ limit: 100 });

const reasonCounts = {};
refunds.data.forEach(refund => {
  const reason = refund.reason || 'unknown';
  reasonCounts[reason] = (reasonCounts[reason] || 0) + 1;
});

console.log(reasonCounts);
// Output: { requested_by_customer: 45, duplicate: 12, fraudulent: 3 }
Aggregate refunds by reason code
Tip: The Dashboard refunds page only shows the most recent 100 refunds. For historical analysis or trend reporting, use the API.

Calculate Refund Rate Using Stripe API

To track refund rate over time, you need to compare refunds against total charges.

Query Charges and Calculate the Ratio

Fetch all successful charges in a time window, then count refunds. Refund rate = (total refunded amount / total charged amount) × 100. Use the amount_refunded field on each charge to avoid double-counting.

javascript
const stripe = require('stripe')('sk_test_...');

const thirtyDaysAgo = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60;

// Fetch all successful charges
const charges = await stripe.charges.list({
  limit: 100,
  created: { gte: thirtyDaysAgo }
});

let totalCharged = 0;
let totalRefunded = 0;

charges.data.forEach(charge => {
  if (charge.paid) {
    totalCharged += charge.amount;
    totalRefunded += charge.amount_refunded;
  }
});

const refundRate = (totalRefunded / totalCharged) * 100;
console.log(`Refund rate (last 30 days): ${refundRate.toFixed(2)}%`);
Calculate 30-day refund rate

Track Refund Rate by Customer or Product

Tag charges with metadata so you can segment refund rates. When creating a charge, add metadata with customer_id, product_id, or subscription_plan. Then filter refund calculations by that metadata.

javascript
// Create a charge with metadata
await stripe.charges.create({
  amount: 5000,
  currency: 'usd',
  source: 'tok_visa',
  metadata: {
    product_id: 'prod_123',
    customer_segment: 'enterprise'
  }
});

// Later, filter charges by metadata
const chargesForProduct = await stripe.charges.list({
  limit: 100,
  created: { gte: thirtyDaysAgo },
  metadata: { product_id: 'prod_123' }
});

let productTotal = 0;
let productRefunded = 0;
chargesForProduct.data.forEach(charge => {
  productTotal += charge.amount;
  productRefunded += charge.amount_refunded;
});

const productRefundRate = (productRefunded / productTotal) * 100;
Segment refund rate by product using metadata
Watch out: Pagination. The Stripe API returns max 100 items per request. Use the starting_after parameter to iterate through all charges if you have high transaction volume.

Monitor Refunds in Real-Time with Webhooks

Don't wait for reports. Catch high refund spikes as they happen.

Listen for the charge.refunded Event

Set up a webhook endpoint that listens for charge.refunded events. Stripe will POST to your endpoint every time a refund occurs. Parse the event and log it to your monitoring system (Datadog, CloudWatch, or your own metric store).

javascript
const express = require('express');
const stripe = require('stripe')('sk_test_...');

const app = express();
const endpointSecret = 'whsec_test_...';

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'charge.refunded') {
    const charge = event.data.object;
    console.log(`Refund: $${charge.amount_refunded / 100} from ${charge.id}`);
    // Send to monitoring, trigger alert, etc.
  }

  res.json({ received: true });
});
Webhook endpoint to catch refund events in real-time

Register Your Webhook Endpoint

In the Stripe Dashboard, go to Developers > Webhooks. Click Add an endpoint, paste your server URL, and select the charge.refunded event. Stripe will sign all requests with a HMAC-SHA256 signature—verify it on your server to prevent spoofing.

javascript
// Always verify the webhook signature before processing
const event = stripe.webhooks.constructEvent(
  req.body,                                  // raw request body
  req.headers['stripe-signature'],           // signature from Stripe header
  endpointSecret                             // from Stripe Dashboard Webhooks page
);

// If the signature is invalid, constructEvent() throws an error.
// Never trust an event that fails signature verification.
Verify webhook authenticity with signature validation

Trigger Alerts on Refund Rate Spikes

Every time a charge.refunded event fires, increment a counter. If your refund rate crosses a threshold (e.g., >5% in the last hour), trigger an alert. Store the timestamp and amount so you can correlate spikes with deployments or outages.

javascript
// Simple refund spike detector
let refundCount = 0;
let chargeCount = 0;
const refundThreshold = 0.05; // 5%

if (event.type === 'charge.succeeded') {
  chargeCount++;
} else if (event.type === 'charge.refunded') {
  refundCount++;
  const currentRate = refundCount / chargeCount;
  
  if (currentRate > refundThreshold) {
    // Alert: refund rate spike detected
    console.warn(`ALERT: Refund rate at ${(currentRate * 100).toFixed(2)}%`);
    // Send to Slack, PagerDuty, email, or your incident system
  }
}
Detect and alert on refund rate anomalies
Tip: Test your webhook with Stripe's webhook tester in the Developers > Webhooks section before deploying. Send a test charge.refunded event to verify your endpoint handles it correctly.

Common Pitfalls

  • Forgetting to account for partial refunds. The amount_refunded field captures the actual refunded amount, not just whether a refund occurred. A charge might be refunded twice for partial amounts.
  • Only querying the Dashboard and missing API-issued refunds. Both channels increment the same counters. Always query the API for the complete picture.
  • Skipping webhook signature verification. An attacker can POST fake charge.refunded events to your endpoint. Always call stripe.webhooks.constructEvent() to verify.
  • Comparing refund rates week-to-week without establishing seasonality. A Monday spike that normalizes by Thursday might be your baseline. Build 30-day trend history before alerting on anomalies.

Wrapping Up

Monitoring refund rate in Stripe is straightforward: start with the Dashboard for quick checks, use the API to calculate trends, and set up webhooks for real-time alerts. Once you're tracking refunds consistently, you can correlate spikes with product changes and fix the root causes. If you want to track this automatically across tools, Product Analyst can help.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free