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.
const stripe = require('stripe')('sk_live_...');
// Or with async/await
const Stripe = require('stripe');
const stripe = new Stripe(process.env.STRIPE_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.
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;
}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.
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);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.
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);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.
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`);
}
}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.
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}%`);
});Common Pitfalls
- Using
stripe.charges.list()for Payment Intent flows. Newer integrations use Payment Intents, not the legacy Charges API. QuerypaymentIntentswithstatus: '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_declinedfailure is different from arate_limiterror. Group failures byfailure_codeto 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.