6 min read

How to Monitor Failed Payments in Stripe

Payment failures happen — card declines, expired methods, insufficient funds. In Stripe, a failed charge doesn't email you automatically, and most teams discover failures only when customers complain. You need three layers: dashboard visibility for spot-checks, API queries for analytics, and webhooks for real-time alerts.

View and Query Failed Payments in Stripe

Start with the dashboard for quick visibility, then add API queries for programmatic monitoring.

Navigate to the Payments dashboard to spot failures

Open your Stripe dashboard and go to Payments > All transactions. Click the Status filter and select Failed to see all declined charges. You'll immediately see the failure reason (card declined, expired, lost card), customer, amount, and timestamp.

Query failed charges via the API

For dashboards, exports, or analytics pipelines, fetch failed charges programmatically using the Charges API. Filter by status: 'failed' and paginate through results. Each charge includes the failure_code (e.g., card_declined, expired_card) and failure_message.

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

// List all failed charges from the past 30 days
const thirtyDaysAgo = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60;

const failedCharges = await stripe.charges.list({
  limit: 100,
  status: 'failed',
  created: { gte: thirtyDaysAgo },
});

failedCharges.data.forEach((charge) => {
  console.log(`Charge ${charge.id}: ${charge.failure_code} - ${charge.failure_message}`);
  console.log(`Customer: ${charge.customer}, Amount: ${charge.amount / 100}`);
});
Fetch all failed charges with failure reason and customer details
Watch out: Stripe has both Charges (older, lower-level) and Payment Intents (newer, recommended). Payment Intents with status requires_payment_method or canceled represent failures. For complete monitoring, query both.

Set Up Webhooks for Real-Time Failure Alerts

Polling the API is manual and slow. Webhooks notify you instantly when a payment fails, so you can recover immediately.

Create a secure webhook endpoint on your server

Build an HTTPS endpoint that receives and verifies webhook signatures. Always validate the Stripe-Signature header using your webhook secret — never trust an unverified request.

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

const app = express();

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  const secret = process.env.STRIPE_WEBHOOK_SECRET;
  
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, secret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.sendStatus(400);
  }
  
  // Process the event (see next step)
  console.log('Verified event:', event.type);
  res.sendStatus(200); // Always respond 200 immediately
});

app.listen(3000, () => console.log('Webhook server running on port 3000'));
Webhook endpoint with Stripe signature verification

Register your webhook endpoint in the Stripe dashboard

Go to Developers > Webhooks and click Add endpoint. Paste your endpoint URL (must be HTTPS and publicly accessible). Select the events you want: payment_intent.payment_failed and charge.failed. Save and copy your Signing Secret to .env.

Listen for failure events and alert your team

When Stripe sends a failure webhook, extract the payment intent or charge object. Log it, send a Slack alert, create a PagerDuty incident, or flag it in your database so you can retry later. Always return HTTP 200 immediately, then process asynchronously.

javascript
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  const event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  
  if (event.type === 'payment_intent.payment_failed') {
    const paymentIntent = event.data.object;
    const { customer, last_payment_error, amount } = paymentIntent;
    
    console.error(`Payment failed for customer ${customer}:`, last_payment_error.message);
    
    // Send alert (Slack, email, PagerDuty, etc.)
    sendAlert({
      customer,
      amount: amount / 100,
      reason: last_payment_error.code,
      paymentIntentId: paymentIntent.id
    });
  }
  
  if (event.type === 'charge.failed') {
    const charge = event.data.object;
    console.error(`Charge ${charge.id} failed: ${charge.failure_message}`);
  }
  
  res.sendStatus(200);
});
Extract failure details and send alerts
Tip: Stripe will retry webhooks for up to 3 days if your endpoint doesn't return 2xx. Always respond with 200 quickly, then offload heavy work to a background job. Never make Stripe wait for your retry logic or email sending.

Implement Automatic Retry Logic

Detecting failures is only half the battle. You recover revenue by retrying with a fresh payment method.

Identify which failures are worth retrying

Not all failures can be recovered. Transient failures like network timeouts or rate limits often succeed on retry, but permanent failures (lost card, closed account, fraud) will fail again. Check the failure_code on the charge or payment intent before retrying. Codes like card_declined, expired_card, and incorrect_cvc may be retryable if the customer provides a new card.

Retry with a fresh payment method and idempotency key

Create a new Payment Intent with the customer's updated or saved payment method. Always pass an idempotencyKey to prevent duplicate charges if your request fails and you retry. Set confirm: true to immediately attempt the charge.

javascript
const customerId = 'cus_...';
const originalPaymentIntentId = 'pi_...';
const amountInCents = 5000; // $50.00
const idempotencyKey = `retry_${originalPaymentIntentId}_${Date.now()}`;

const retryPaymentIntent = await stripe.paymentIntents.create(
  {
    customer: customerId,
    amount: amountInCents,
    currency: 'usd',
    payment_method: newPaymentMethodId, // Customer's updated card
    confirm: true,
  },
  { idempotencyKey } // Prevents duplicate if request is retried
);

if (retryPaymentIntent.status === 'succeeded') {
  console.log('Retry succeeded:', retryPaymentIntent.id);
  // Update order status, send confirmation email
} else if (retryPaymentIntent.status === 'requires_action') {
  // Customer must complete 3D Secure or other challenge
  console.log('Customer action required for:', retryPaymentIntent.client_secret);
} else {
  console.error('Retry failed:', retryPaymentIntent.last_payment_error);
}
Retry a payment with idempotency protection and status handling

Space out retries with exponential backoff

Don't retry immediately after a failure — wait. Space retries using exponential backoff: retry after 5 minutes, then 15 minutes, then 1 hour. This gives customers time to fix their payment method and avoids hammering Stripe rate limits or angering customers with repeated charge attempts.

Watch out: Every call to stripe.paymentIntents.create() with confirm: true charges the customer immediately. Verify the payment method is valid before confirming, or you risk multiple failed charges in a row.

Common Pitfalls

  • Conflating payment_intent.payment_failed and charge.failed events — they use different data structures and failure codes. Always handle both.
  • Skipping webhook signature verification — you risk processing spoofed events and accidentally crediting fraudulent customers or charging the wrong person.
  • Retrying too fast without backoff — Stripe may throttle your requests, and customers hate seeing repeated charges. Use exponential backoff and cap retry attempts.
  • Assuming all saved payment methods are still valid — cards expire, get reported lost, or change issuer. Always be ready for a second failure and provide a recovery path.

Wrapping Up

You now have a multi-layer strategy: dashboard visibility to spot failures manually, API queries for dashboards and analytics, webhooks for real-time alerts, and retry logic to recover revenue. Combine all three. If you want to track failed payments alongside subscription churn, LTV, and other business metrics across your entire stack, Product Analyst can help.

Track these metrics automatically

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

Try Product Analyst — Free