Failed payments are invisible until you know where to look in Stripe. Your customers might be frustrated, your conversion rate dropping, but the dashboard won't scream about it. Here's how to get real visibility into what's actually failing and why.
Access Failed Payments in the Stripe Dashboard
The fastest way to see what's failing right now.
Open the Payments tab and filter for failed charges
In your Stripe account, go to Payments in the left sidebar. You'll see all your transactions. Click the Filters button at the top and select Status: Failed. This immediately shows every charge that didn't go through.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Fetch failed charges from the API
const failedCharges = await stripe.charges.list({
status: 'failed',
limit: 100,
expand: ['data.dispute', 'data.balance_transaction']
});Click a failed charge to see the failure reason
Each row shows the amount, date, and customer. Click on any charge to open its details. The failure_code and failure_message fields tell you exactly why it failed — e.g., card_declined, expired_card, insufficient_funds, or processing_error.
// Log failure details
failedCharges.data.forEach(charge => {
console.log({
id: charge.id,
amount: charge.amount,
currency: charge.currency,
failure_code: charge.failure_code,
failure_message: charge.failure_message,
created: new Date(charge.created * 1000)
});
});Use Radar to identify patterns in card declines
If you're seeing a spike in card_declined failures, Stripe's Radar for Fraud Teams can help. Navigate to Radar > Blocked transactions to see if a network or issuer is blocking cards. Radar shows aggregated patterns — not just individual failures.
// Check for charge disputes (which indicate potential fraud blocks)
const chargeWithDispute = failedCharges.data.find(c => c.dispute);
if (chargeWithDispute) {
console.log('Disputed charge:', {
chargeId: chargeWithDispute.id,
disputeReason: chargeWithDispute.dispute.reason,
disputeStatus: chargeWithDispute.dispute.status
});
}Query and Group Failures Programmatically
To build a real dashboard or analyze trends, you need the API. This lets you get all failures, group them, and filter by date range.
Pull failed charges from the API with a date range
Use stripe.charges.list() to fetch failures within a specific window. Include expand: ['data.balance_transaction'] to see the net amount (after fees). This is what you actually received.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const startOfMonth = Math.floor(new Date(2025, 2, 1).getTime() / 1000);
const endOfMonth = Math.floor(new Date(2025, 2, 31).getTime() / 1000);
const failedCharges = await stripe.charges.list({
status: 'failed',
created: {
gte: startOfMonth,
lte: endOfMonth
},
limit: 100,
expand: ['data.balance_transaction']
});
console.log(`Found ${failedCharges.data.length} failed charges in March`);Group failures by reason to spot the biggest issue
Once you have the data, group by failure_code to see which type of failure is most common. card_declined is usually the biggest bucket, but expired_card and processing_error also tell you something — the first is user-fixable, the second is Stripe's problem.
const groupedByReason = {};
failedCharges.data.forEach(charge => {
const code = charge.failure_code;
if (!groupedByReason[code]) {
groupedByReason[code] = { count: 0, totalAmount: 0 };
}
groupedByReason[code].count += 1;
groupedByReason[code].totalAmount += charge.amount;
});
const sorted = Object.entries(groupedByReason)
.sort(([, a], [, b]) => b.count - a.count);
sorted.forEach(([reason, stats]) => {
console.log(`${reason}: ${stats.count} failures ($${(stats.totalAmount / 100).toFixed(2)})`);
});Set Up Webhooks for Real-time Visibility
The API shows you what happened in the past. Webhooks let you catch failures as they occur — so you can alert customers, log them, or fix them immediately.
Register a webhook endpoint for charge failures
In Stripe, go to Developers > Webhooks and click Add endpoint. Set your URL (e.g., https://your-app.com/webhooks/stripe) and select the charge.failed event. Stripe will POST to this URL every time a charge fails.
// Webhook handler (Express.js example)
app.post('/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.error('Webhook sig verification failed:', err.message);
return res.sendStatus(400);
}
if (event.type === 'charge.failed') {
const charge = event.data.object;
console.log(`Charge ${charge.id} failed: ${charge.failure_code}`);
}
res.sendStatus(200);
});Log failures to your database or monitoring tool
When you receive a charge.failed webhook, extract the failure_code, failure_message, amount, and customer email. Store this in your database so you can query it later. This is also where you'd send a Slack alert or email the customer.
if (event.type === 'charge.failed') {
const charge = event.data.object;
// Log to your database
await db.insert('failed_payments', {
stripe_charge_id: charge.id,
failure_code: charge.failure_code,
failure_message: charge.failure_message,
amount: charge.amount,
customer_id: charge.customer,
email: charge.billing_details?.email,
created_at: new Date(charge.created * 1000)
});
// Send alert if it's a processing error
if (charge.failure_code === 'processing_error') {
await slack.send(`⚠️ Processing error on ${charge.id}`);
}
}stripe.webhooks.constructEvent — don't trust POST bodies without it. Test with stripe listen before going live.Common Pitfalls
- Confusing failed charges with refunded or disputed charges — they're different statuses and require different follow-ups
- Relying only on the dashboard and missing older failures — the dashboard shows only recent data; use the API or webhooks for complete history
- Storing failure reasons but not taking action — knowing 80% of failures are
card_declinedonly helps if you email customers to update their card - Assuming all
processing_errorfailures are transient — some indicate issues with your API integration; check Stripe's status page if you see a spike
Wrapping Up
Visualizing failed payments in Stripe is straightforward once you know the three layers: dashboard for quick checks, API for analysis, webhooks for automation. Start with the dashboard, graduate to the API when you need trends, and add webhooks when you're ready to alert or retry. If you want to track this automatically across tools, Product Analyst can help.