Stripe Payment Failed: How to Fix It (And Prevent It Forever)

Check the invoice's last_payment_error.decline_code in your Stripe Dashboard or API. If it's a soft decline (insufficient_funds, processing_error), Stripe will auto-retry up to 4 times over ~3 weeks. If it's a hard decline (card_not_supported, stolen_card), retries won't help. You need the customer to update their payment method. The permanent fix is an automated dunning sequence that emails the customer with a one-click card update link.

Why Stripe Payments Fail

Stripe payments fail when the issuing bank declines the charge. This happens on 5-10% of all recurring SaaS charges; a leading cause of involuntary churn. Stripe categorizes failures into two types: soft declines (temporary issues the bank may resolve) and hard declines (permanent rejections that require customer action). The decline reason is stored on the Invoice object as last_payment_error.

When a subscription payment fails, Stripe marks the invoice as open and the subscription as past_due. It then enters a retry cycle based on your Smart Retries settings (or fixed schedule if Smart Retries is off). If all retries fail, Stripe either cancels the subscription, marks it unpaid, or leaves it past_due. depending on your Billing settings.

Retrieve the decline reason via Stripe API
// Get the latest invoice for a past_due subscription
const subscription = await stripe.subscriptions.retrieve('sub_xxx');
const invoice = await stripe.invoices.retrieve(
  subscription.latest_invoice
);

// The decline code tells you what went wrong
const error = invoice.last_payment_error;
console.log(error?.decline_code);  // e.g. "insufficient_funds"
console.log(error?.code);          // e.g. "card_declined"
console.log(error?.message);       // Human-readable message

The most common decline codes for SaaS subscriptions: insufficient_funds (30-40% of failures), card_not_supported or generic_decline (20-25%), expired_card (15-20%), and authentication_required for 3D Secure cards (10-15%). Each requires a different response, which is why a one-size-fits-all retry strategy leaves money on the table.

Quick Fix (Manual)

If you have a single failed payment you need to resolve right now, here's the manual process:

1

Open your Stripe Dashboard and go to Billing > Invoices. Filter by "Past due" status.

2

Click the failed invoice. Check the "Payment attempt" section for the decline code and error message.

3

If it's a soft decline (insufficient_funds, processing_error): click "Retry charge" to attempt payment again immediately. Stripe may recover it on retry.

4

If it's a hard decline (expired_card, stolen_card, card_not_supported): you need the customer to update their card. Click "Send invoice" to email them a hosted payment page.

5

Alternatively, generate a direct card update link using the Customer Portal or the Billing Portal API and send it to the customer yourself.

Retry a failed invoice via the API
// Retry the payment on a past-due invoice
const invoice = await stripe.invoices.pay('in_xxx');

// If it succeeds, subscription moves back to "active"
console.log(invoice.status); // "paid"

// If it fails again, check the new error
if (invoice.status === 'open') {
  console.log(invoice.last_payment_error?.decline_code);
}
Create a customer portal session for card update
// Send the customer a link to update their payment method
const session = await stripe.billingPortal.sessions.create({
  customer: 'cus_xxx',
  return_url: 'https://yourapp.com/account',
});

// Send session.url to the customer via email
console.log(session.url);
// e.g. "https://billing.stripe.com/p/session/xxx"

Warning: Don't spam-retry failed payments. Each retry attempt generates a new charge on the customer's card statement, and excessive retries can trigger fraud flags with the issuing bank. Stripe's Smart Retries already optimizes retry timing. manual retries should be a last resort, not a first response.

Permanent Fix (Automated)

Manually checking invoices and emailing customers doesn't scale. A single SaaS at $20K MRR with a 5% failure rate has 15-25 failed payments per month. Each one needs to be triaged (soft vs. hard decline), retried at the right time, and followed up with the right email. Here's how to automate the entire pipeline:

Step 1: Enable Stripe Smart Retries. Go to Dashboard > Settings > Subscriptions and emails > Manage failed payments. Turn on Smart Retries so Stripe uses ML to pick the optimal retry time. This alone recovers 5-10% more than a fixed retry schedule.

Step 2: Set up a dunning email sequence. Stripe's built-in email is a single generic message that recovers 10-15% of failures. A proper 5-7 email sequence with escalating urgency recovers 40-55%. Use our failed payment recovery calculator to see the dollar impact. You need: (1) immediate notification, (2) 3-day follow-up with card update link, (3) 7-day urgency email, (4) 14-day final warning, and (5) grace period last chance.

Step 3: Add pre-dunning card expiry alerts. 25-30% of payment failures are caused by expired cards. Listening for the customer.source.expiring webhook and emailing customers before the card expires prevents the failure from ever happening.

SaveMRR automates all three steps for $19/mo. Paste your Stripe restricted API key and SaveMRR immediately starts: smart dunning sequences with 7 emails, card expiry pre-dunning alerts, one-click card update links, and real-time tracking of every recovery. The first $200 recovered free. You don't pay until SaveMRR proves it works.

What SaveMRR replaces (webhook handler you'd build yourself)
// Without SaveMRR, you'd build something like this:
app.post('/webhooks/stripe', async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body, sig, endpointSecret
  );

  switch (event.type) {
    case 'invoice.payment_failed':
      const invoice = event.data.object;
      const decline = invoice.last_payment_error?.decline_code;

      // Triage: soft vs hard decline
      if (['insufficient_funds', 'processing_error'].includes(decline)) {
        await scheduleRetry(invoice, { delay: '3d' });
      }

      // Send dunning email #1
      await sendDunningEmail(invoice.customer, {
        template: 'payment_failed_1',
        cardUpdateUrl: await createPortalSession(invoice.customer),
      });

      // Schedule follow-up emails at 3d, 7d, 14d, 21d
      await scheduleDunningSequence(invoice.customer, invoice.id);
      break;

    case 'customer.source.expiring':
      // Pre-dunning: card expires soon
      await sendCardExpiryAlert(event.data.object);
      break;
  }

  res.json({ received: true });
});

// SaveMRR handles all of the above automatically.
// No webhook code. No email templates. No cron jobs.

Tip: The webhook handler above is ~200 lines of production code once you add error handling, idempotency, email templating, retry scheduling, and unsubscribe management. SaveMRR replaces all of it with a quick API key paste. Run a free Revenue Scan to see exactly how much you're losing to failed payments right now.

Related Stripe Billing Issues

Frequently Asked Questions

How many times does Stripe retry a failed payment?

With Smart Retries enabled, Stripe retries up to 4 times over approximately 3 weeks (the exact timing is determined by ML). With a fixed retry schedule, you configure the intervals yourself (e.g., 3, 5, and 7 days after the initial failure). After all retries are exhausted, Stripe takes the action you've configured: cancel subscription, mark as unpaid, or leave as past_due.

Should I retry a hard-declined payment manually?

Generally no. Hard declines (stolen_card, card_not_supported, fraudulent) are permanent rejections from the issuing bank. Retrying won't change the outcome and can trigger fraud flags. For hard declines, your only option is to get the customer to update their payment method. Focus your energy on sending a clear card update email with a one-click link, not on retrying the same failed card.

What's the difference between past_due, unpaid, and canceled?

past_due means the latest invoice payment failed but retries are still being attempted. unpaid means all retries were exhausted but the subscription is kept alive (no access, but not canceled). canceled means the subscription is permanently ended. You configure which status Stripe assigns after all retries fail in Dashboard > Settings > Subscriptions and emails > Manage failed payments.

How much revenue do failed payments cost the average SaaS?

Failed payments cause 20-40% of total SaaS churn (called involuntary churn). For a SaaS at $20K MRR with 5% monthly payment failure, that's $1,000/mo in at-risk revenue. Without automated recovery, you'll lose 50-70% of those customers permanently. A proper dunning sequence recovers 40-55%, which means $400-$550/mo saved. 20x the cost of a tool like SaveMRR.

Does enabling Smart Retries mean I don't need dunning emails?

No. Smart Retries handles the payment retry timing (when to re-attempt the charge). Dunning emails handle the customer communication (prompting them to update their card). You need both. Smart Retries alone recovers ~15-25% of failures. Smart Retries + dunning emails together recover 40-55%. They're complementary, not interchangeable.

SaveMRR catches these automatically

Stop firefighting Stripe billing issues manually. Paste your API key, get a free Revenue Scan in 60 seconds, and let SaveMRR handle recovery automatically.

Run my free scan