6 min read

How to Calculate Failed Payments in Stripe

Payment failures eat into your revenue and hurt customer trust. In Stripe, you need to know how many payments failed, why they failed, and if there's a pattern. You can query your charge history, filter by status, or listen to events in real-time. Each approach has different accuracy and latency trade-offs.

Query Failed Charges via the Stripe API

The most direct way to see how many payments failed is to query your charges with Stripe's API. You can filter by status, date range, and customer to calculate failure rates.

Set up the Stripe JavaScript SDK

Install the Stripe SDK in your backend project. Import it and initialize with your secret key. Keep your secret key in environment variables—never expose it in frontend code.

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

// Or with async/await
const Stripe = require('stripe');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
Initialize Stripe SDK with your secret key

Fetch all failed charges in a date range

Use stripe.charges.list() to query charges with status: 'failed'. Filter by created date to get a specific time window. Stripe's API paginates results, so loop through all pages if you have lots of failed charges.

javascript
const failedCharges = await stripe.charges.list({
  status: 'failed',
  created: {
    gte: Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60,
    lte: Math.floor(Date.now() / 1000)
  },
  limit: 100
});

let allFailed = [...failedCharges.data];
let hasMore = failedCharges.has_more;

while (hasMore) {
  const nextPage = await stripe.charges.list({
    status: 'failed',
    created: { gte: startTime, lte: endTime },
    limit: 100,
    starting_after: allFailed[allFailed.length - 1].id
  });
  allFailed = [...allFailed, ...nextPage.data];
  hasMore = nextPage.has_more;
}
Paginate through all failed charges in your date range

Calculate the failure rate

Get the total charge count for the same period, then divide failed by total. You can also break this down by payment method, customer, or error code to find patterns.

javascript
const allCharges = await stripe.charges.list({
  created: {
    gte: Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60,
    lte: Math.floor(Date.now() / 1000)
  },
  limit: 100
});

const failureRate = (allFailed.length / allCharges.data.length) * 100;
console.log(`Failed: ${allFailed.length}, Total: ${allCharges.data.length}, Rate: ${failureRate.toFixed(2)}%`);

const failuresByReason = {};
allFailed.forEach(charge => {
  const reason = charge.failure_code || 'unknown';
  failuresByReason[reason] = (failuresByReason[reason] || 0) + 1;
});
console.log('Failures by reason:', failuresByReason);
Calculate failure rate and group by failure code
Watch out: The charges API returns charges created before Stripe moved to Payment Intents. For newer payment flows (invoices, subscriptions, Checkout), query stripe.paymentIntents.list({ status: 'requires_payment_method' }) instead.

Track Real-Time Failures with Webhooks

Webhooks give you the lowest-latency access to payment events. When a charge fails, Stripe sends a charge.failed event to your endpoint. Use this for real-time alerting or metrics collection.

Create a webhook endpoint

Set up an HTTP endpoint (e.g., /api/webhooks/stripe) that listens for POST requests. Stripe sends event data here as soon as a charge fails. Verify the request signature to ensure it came from Stripe.

javascript
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();

app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.log(`Webhook signature verification failed: ${err.message}`);
    return res.sendStatus(400);
  }
  
  if (event.type === 'charge.failed') {
    const charge = event.data.object;
    console.log(`Charge failed: ${charge.id}, Error: ${charge.failure_code}`);
  }
  
  res.json({ received: true });
});

app.listen(3000);
Verify and handle charge.failed webhook events

Register the webhook in Stripe

Go to Developers > Webhooks in your Stripe dashboard. Add an endpoint with your webhook URL (must be HTTPS). Select the events you want: charge.failed, charge.dispute.created, and invoice.payment_action_required. Stripe will test the endpoint with a ping; make sure it responds with a 200 status.

Log failures to track anomalies

Store each failed charge event in your database or send it to a metrics tool. This builds a real-time log of failures and lets you calculate rolling failure rates or alert when failures spike unexpectedly.

javascript
if (event.type === 'charge.failed') {
  const charge = event.data.object;
  
  await db.failures.create({
    chargeId: charge.id,
    customerId: charge.customer,
    amount: charge.amount,
    failureCode: charge.failure_code,
    failureMessage: charge.failure_message,
    timestamp: new Date(charge.created * 1000)
  });
  
  const failuresLastHour = await db.failures.count({
    timestamp: { $gte: new Date(Date.now() - 60 * 60 * 1000) }
  });
  
  if (failuresLastHour > 50) {
    console.error(`ALERT: ${failuresLastHour} failures in the last hour`);
  }
}
Store failures and alert on anomalies
Watch out: Webhooks can be delayed by seconds or minutes. For critical real-time checks, combine webhooks with periodic API queries.

Automate Daily Reconciliation

Manual checks don't scale. Schedule a job to run each morning, fetch yesterday's charges, and calculate the failure count automatically.

Set up a scheduled job

Use a task scheduler like node-schedule or AWS Lambda to run a script daily. Query Stripe's API for yesterday's charges, calculate the failure count, and log the results to your database or alerting tool.

javascript
const schedule = require('node-schedule');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Run every day at 9 AM
schedule.scheduleJob('0 9 * * *', async () => {
  const yesterday = Math.floor(Date.now() / 1000) - 24 * 60 * 60;
  const today = Math.floor(Date.now() / 1000);
  
  const failed = await stripe.charges.list({
    status: 'failed',
    created: { gte: yesterday, lte: today },
    limit: 100
  });
  
  const total = await stripe.charges.list({
    created: { gte: yesterday, lte: today },
    limit: 100
  });
  
  const rate = (failed.data.length / total.data.length * 100).toFixed(2);
  console.log(`Yesterday's failure rate: ${rate}%`);
});
Calculate daily failure metrics on a schedule

Common Pitfalls

  • Using stripe.charges.list() for Payment Intent flows. Newer integrations use Payment Intents, not the legacy Charges API. Query paymentIntents with status: 'requires_payment_method' instead.
  • Not paginating through results. Stripe limits responses to 100 items by default. If you have more than 100 failed charges in your time range, you'll miss data unless you loop through pages.
  • Ignoring failure codes. A card_declined failure is different from a rate_limit error. Group failures by failure_code to identify root causes and prioritize fixes.
  • Assuming webhooks are instant. Webhook delivery can lag by seconds or minutes. For live dashboards, query the API directly. Use webhooks for async processing and alerting.

Wrapping Up

You now have three ways to track failed payments: query the API for historical data, listen to webhooks for real-time alerts, or schedule daily reconciliation jobs. Start with the API if you need accuracy; use webhooks if you need speed. 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