Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to fix Stripe payment failed issue

Stripe payment failures can stem from card declines, authentication requirements, invalid parameters, or network issues. Debug them by checking the PaymentIntent status, reading the last_payment_error object, inspecting the Stripe Dashboard event logs, and handling each failure type in your code. Most failures are resolved by displaying clear messages and letting customers retry with corrected information.

What you'll learn

  • How to read Stripe PaymentIntent error details
  • How to debug payment failures using Dashboard event logs
  • How to handle authentication-required (3D Secure) scenarios
  • How to build a resilient payment flow that handles all failure types
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read20 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
TL;DR

Stripe payment failures can stem from card declines, authentication requirements, invalid parameters, or network issues. Debug them by checking the PaymentIntent status, reading the last_payment_error object, inspecting the Stripe Dashboard event logs, and handling each failure type in your code. Most failures are resolved by displaying clear messages and letting customers retry with corrected information.

Debugging Stripe Payment Failures Step by Step

When a Stripe payment fails, the PaymentIntent moves to a 'requires_payment_method' or 'requires_action' status instead of 'succeeded'. The last_payment_error field contains the failure details — decline code, error type, and a human-readable message. Understanding these statuses and error types lets you display helpful messages to the customer and log the details for your team. This guide covers the full debugging workflow from API error to resolution.

Prerequisites

  • A Stripe account in test mode
  • Node.js 18+ with the Stripe npm package
  • Access to the Stripe Dashboard for event log inspection
  • A payment form using Stripe.js and Elements

Step-by-step guide

1

Check the PaymentIntent status

Retrieve the PaymentIntent and check its status. Common statuses after failure: 'requires_payment_method' (card declined or invalid), 'requires_action' (3D Secure needed), 'canceled' (explicitly canceled), 'processing' (still in progress).

typescript
1const paymentIntent = await stripe.paymentIntents.retrieve('pi_abc123');
2console.log('Status:', paymentIntent.status);
3console.log('Error:', paymentIntent.last_payment_error);

Expected result: You can see the PaymentIntent status and the specific error that caused the failure.

2

Read the last_payment_error object

The last_payment_error field contains all the details you need: type (card_error, invalid_request_error, etc.), code (card_declined, expired_card, etc.), decline_code (insufficient_funds, etc.), and message.

typescript
1const error = paymentIntent.last_payment_error;
2if (error) {
3 console.log('Error type:', error.type);
4 console.log('Error code:', error.code);
5 console.log('Decline code:', error.decline_code);
6 console.log('Message:', error.message);
7}

Expected result: The error details tell you exactly why the payment failed — card decline, authentication, or API issue.

3

Check the Stripe Dashboard event logs

Go to Stripe Dashboard → Developers → Events. Filter by 'payment_intent.payment_failed'. Click an event to see the full error details, timeline, and any associated requests. This is the fastest way to debug production payment failures.

Expected result: The Dashboard shows the complete event history for the failed payment, including the error response Stripe sent.

4

Handle 3D Secure authentication requirements

If the status is 'requires_action', the customer's bank requires 3D Secure authentication. Use stripe.confirmPayment() on the frontend with Stripe.js to trigger the authentication flow automatically.

typescript
1// Frontend — Stripe.js handles 3D Secure automatically
2const { error, paymentIntent } = await stripe.confirmPayment({
3 elements,
4 confirmParams: {
5 return_url: 'https://yoursite.com/payment/complete',
6 },
7});
8
9if (error) {
10 // Authentication failed or was abandoned
11 console.log('Payment failed:', error.message);
12} else if (paymentIntent.status === 'succeeded') {
13 console.log('Payment succeeded after 3D Secure');
14}

Expected result: Stripe.js opens the 3D Secure authentication modal. After the customer completes authentication, the payment either succeeds or fails with a clear error.

5

Build a comprehensive error handler

Handle all common failure types in your server-side code. Map each error type to an appropriate user message and log the details for debugging.

typescript
1function handlePaymentError(error) {
2 switch (error.type) {
3 case 'card_error':
4 return {
5 status: 402,
6 message: getDeclineMessage(error.decline_code || error.code),
7 retry: true,
8 };
9 case 'validation_error':
10 return {
11 status: 400,
12 message: 'Please check your payment details and try again.',
13 retry: true,
14 };
15 case 'invalid_request_error':
16 console.error('Stripe integration error:', error.message);
17 return {
18 status: 500,
19 message: 'Something went wrong. Please try again.',
20 retry: false,
21 };
22 default:
23 console.error('Unexpected Stripe error:', error);
24 return {
25 status: 500,
26 message: 'An unexpected error occurred.',
27 retry: false,
28 };
29 }
30}

Expected result: Your error handler categorizes failures and provides appropriate user messages and retry guidance for each type.

6

Test with Stripe test cards

Use Stripe test cards to simulate every failure scenario. This ensures your error handling works correctly before going live. Teams at RapidDev routinely test all decline paths during payment integration audits.

typescript
1// Test cards for payment failures
2// 4242424242424242 — Always succeeds
3// 4000000000000002 — Generic decline
4// 4000000000009995 — Insufficient funds
5// 4000000000000069 — Expired card
6// 4000002500003155 — Requires 3D Secure authentication
7// 4000000000000101 — Incorrect CVC (check fails)

Expected result: Each test card triggers the expected failure path. Your error handler returns the correct user message for each scenario.

Complete working example

payment-debug.js
1require('dotenv').config();
2const express = require('express');
3const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
4
5const app = express();
6app.use(express.json());
7
8function getDeclineMessage(code) {
9 const messages = {
10 insufficient_funds: 'Insufficient funds. Please use a different card.',
11 expired_card: 'Your card has expired. Please update your payment method.',
12 incorrect_cvc: 'Incorrect CVC. Please check and try again.',
13 processing_error: 'Processing error. Please try again in a moment.',
14 generic_decline: 'Card declined. Please try a different payment method.',
15 fraudulent: 'Card declined. Please contact your bank.',
16 lost_card: 'Card declined. Please contact your bank.',
17 stolen_card: 'Card declined. Please contact your bank.',
18 };
19 return messages[code] || 'Payment failed. Please try a different payment method.';
20}
21
22app.post('/api/pay', async (req, res) => {
23 const { amount, currency, paymentMethodId } = req.body;
24
25 try {
26 const paymentIntent = await stripe.paymentIntents.create({
27 amount,
28 currency: currency || 'usd',
29 payment_method: paymentMethodId,
30 confirm: true,
31 automatic_payment_methods: {
32 enabled: true,
33 allow_redirects: 'never',
34 },
35 });
36
37 if (paymentIntent.status === 'requires_action') {
38 return res.json({
39 requiresAction: true,
40 clientSecret: paymentIntent.client_secret,
41 });
42 }
43
44 res.json({ success: true, id: paymentIntent.id });
45 } catch (err) {
46 const error = err.raw || err;
47 console.error('Payment failed:', {
48 type: error.type,
49 code: error.code,
50 decline_code: error.decline_code,
51 message: error.message,
52 });
53
54 if (error.code === 'card_declined') {
55 return res.status(402).json({
56 error: getDeclineMessage(error.decline_code),
57 decline_code: error.decline_code,
58 retry: true,
59 });
60 }
61
62 res.status(500).json({
63 error: 'Payment could not be processed. Please try again.',
64 retry: error.type !== 'invalid_request_error',
65 });
66 }
67});
68
69// Debug endpoint — retrieve PaymentIntent details
70app.get('/api/payment/:id', async (req, res) => {
71 const pi = await stripe.paymentIntents.retrieve(req.params.id);
72 res.json({
73 status: pi.status,
74 amount: pi.amount,
75 currency: pi.currency,
76 error: pi.last_payment_error,
77 });
78});
79
80const PORT = process.env.PORT || 3000;
81app.listen(PORT, () => console.log(`Server on port ${PORT}`));

Common mistakes when fixing Stripe payment failed issue

Why it's a problem: Not checking the PaymentIntent status after creation

How to avoid: Always check paymentIntent.status. It may be 'requires_action' (3D Secure needed) or 'requires_payment_method' (failed) even after calling confirm.

Why it's a problem: Ignoring the last_payment_error field

How to avoid: Read paymentIntent.last_payment_error for the specific failure reason. This field contains the decline code, error type, and message.

Why it's a problem: Showing generic error messages for all payment failures

How to avoid: Map specific decline codes to actionable messages. 'Insufficient funds' is more helpful than 'Payment failed' because the customer knows to try a different card.

Why it's a problem: Not handling 3D Secure (requires_action) on the frontend

How to avoid: Use stripe.confirmPayment() with Stripe.js to handle 3D Secure authentication automatically. Without this, payments requiring authentication will always fail.

Best practices

  • Always check paymentIntent.status after creation — don't assume success
  • Read last_payment_error for specific decline codes and error types
  • Map decline codes to user-friendly, actionable messages
  • Handle 'requires_action' status by triggering 3D Secure on the frontend
  • Log full error details server-side for debugging while showing simple messages to users
  • Use the Stripe Dashboard Events tab to inspect failed payment details
  • Test every failure path with Stripe test cards before going live
  • Implement retry logic for transient errors like processing_error

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a Node.js Express payment endpoint that creates a Stripe PaymentIntent, handles card_declined errors with specific decline code messages, handles requires_action for 3D Secure, and includes a debug endpoint to retrieve PaymentIntent details. Map fraud-related declines to generic messages.

Stripe Prompt

Build a comprehensive Stripe payment handler in Node.js that handles all failure types: card declines with specific codes, 3D Secure authentication requirements, validation errors, and API errors. Include a decline code message mapper and a debug endpoint.

Frequently asked questions

What does PaymentIntent status 'requires_action' mean?

It means the customer's bank requires additional authentication, usually 3D Secure. Use stripe.confirmPayment() on the frontend with Stripe.js to trigger the authentication flow. The payment isn't failed — it's waiting for the customer to authenticate.

How do I find out why a specific payment failed?

Check paymentIntent.last_payment_error for the error details. Also check Dashboard → Developers → Events and filter for payment_intent.payment_failed to see the full event timeline.

Should I automatically retry failed payments?

For one-time payments, don't auto-retry without customer action. For subscriptions, enable Smart Retries in Dashboard → Settings → Billing. Auto-retrying declines like insufficient_funds won't help — the customer needs to resolve the issue first.

What test card triggers 3D Secure authentication?

Use card number 4000002500003155 in test mode. It always requires 3D Secure authentication. Use 4242424242424242 for a card that always succeeds without authentication.

Why does my payment fail with 'invalid_request_error'?

This error type means your API request has a problem — missing required fields, invalid parameter values, or referencing a nonexistent object. Check the error message for the specific field that's wrong. This is a code bug, not a customer issue.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.