Stripe Card Declined Codes Explained: Every Code and What to Do
Stripe returns a decline_code on every failed charge. The code tells you whether to retry (soft declines like insufficient_funds) or ask the customer to update their card (hard declines like expired_card). The 5 most common codes cover 85% of SaaS payment failures. Here's every code, what it means, and the correct recovery action for each.
How Stripe Decline Codes Work
When a payment fails, Stripe's API returns an error object on the Charge, PaymentIntent, or Invoice. The key field is last_payment_error.decline_code; a machine-readable string from the issuing bank that categorizes the failure reason. Stripe translates the bank's raw network decline code into a standardized set of ~25 decline codes.
Decline codes fall into two categories: soft declines (temporary, may succeed on retry) and hard declines (permanent, retry won't help). Your recovery strategy should be completely different for each. Retrying a hard decline wastes time and can trigger fraud alerts. Not retrying a soft decline leaves money on the table.
const paymentIntent = await stripe.paymentIntents.retrieve('pi_xxx');
if (paymentIntent.status === 'requires_payment_method') {
const error = paymentIntent.last_payment_error;
console.log(error.code); // "card_declined"
console.log(error.decline_code); // "insufficient_funds"
console.log(error.message); // "Your card has insufficient funds."
// For invoices (subscription payments):
const invoice = await stripe.invoices.retrieve('in_xxx');
console.log(invoice.last_payment_error?.decline_code);
}Important: the decline_code is not always present. Some failures return only a code (like "card_declined") without a specific decline_code. When decline_code is missing, Stripe received a generic rejection from the issuing bank with no additional information. Treat these as soft declines for retry purposes, but send a dunning email with a card update link as a fallback.
Every Decline Code + Action
Here's every Stripe decline code organized by category. For each code: what it means, whether to retry, and the correct recovery action.
SOFT DECLINES (temporary. worth retrying):
const softDeclines = {
'insufficient_funds': {
meaning: 'Customer does not have enough balance right now',
action: 'Retry in 3-5 days (after payday). Send dunning email.',
frequency: '30-40% of all SaaS failures',
},
'processing_error': {
meaning: 'Temporary issue at the bank or network level',
action: 'Retry in 24 hours. Usually resolves itself.',
frequency: '5-8%',
},
'authentication_required': {
meaning: '3D Secure / SCA challenge needed',
action: 'Send customer a payment link that triggers 3DS flow.',
frequency: '10-15% (higher in EU due to SCA mandate)',
},
'approve_with_id': {
meaning: 'Bank wants to verify identity before approving',
action: 'Retry once. If fails again, ask customer to call bank.',
frequency: '2-3%',
},
'try_again_later': {
meaning: 'Issuer system is temporarily unavailable',
action: 'Retry in 2-4 hours. Almost always succeeds on retry.',
frequency: '1-2%',
},
'reenter_transaction': {
meaning: 'Bank is asking for the charge to be resubmitted',
action: 'Retry immediately. Often a network glitch.',
frequency: '<1%',
},
};HARD DECLINES (permanent. do not retry):
const hardDeclines = {
'expired_card': {
meaning: 'Card has passed its expiration date',
action: 'Send card update email immediately. Do NOT retry.',
frequency: '15-20% of all SaaS failures',
},
'generic_decline': {
meaning: 'Bank declined with no specific reason given',
action: 'Retry once in 24h. If fails again, send update email.',
frequency: '15-20% (catch-all code)',
note: 'Can be soft or hard. Retry once to differentiate.',
},
'card_not_supported': {
meaning: 'Card does not support this type of purchase',
action: 'Customer needs a different card entirely.',
frequency: '3-5%',
},
'do_not_honor': {
meaning: 'Bank refuses without explanation (usually fraud flag)',
action: 'Send card update email. Customer may need to call bank.',
frequency: '5-8%',
},
'stolen_card': {
meaning: 'Card reported stolen by the cardholder',
action: 'Do NOT retry. Do NOT contact about this card.',
frequency: '<1%',
},
'lost_card': {
meaning: 'Card reported lost by the cardholder',
action: 'Do NOT retry. Send card update email for a new card.',
frequency: '<1%',
},
'pickup_card': {
meaning: 'Bank wants the card confiscated (fraud suspected)',
action: 'Do NOT retry. Send card update email for a new card.',
frequency: '<1%',
},
'incorrect_number': {
meaning: 'The card number is wrong',
action: 'Customer entered the card number incorrectly.',
frequency: '<1% (rare for stored cards)',
},
'incorrect_cvc': {
meaning: 'The CVC/CVV does not match',
action: 'Customer needs to re-enter their card details.',
frequency: '<1% (rare for subscriptions)',
},
'invalid_account': {
meaning: 'Card account is closed or invalid',
action: 'Customer needs a completely different card.',
frequency: '1-2%',
},
'card_velocity_exceeded': {
meaning: 'Too many transactions in a short period',
action: 'Wait 24h and retry once. If fails, send update email.',
frequency: '<1%',
},
'withdrawal_count_limit_exceeded': {
meaning: 'Customer hit their daily transaction limit',
action: 'Wait 24h and retry once.',
frequency: '<1%',
},
'currency_not_supported': {
meaning: 'Card cannot be charged in this currency',
action: 'Customer needs a card that supports your billing currency.',
frequency: '<1%',
},
};Warning: Never retry stolen_card, lost_card, or pickup_card declines. These indicate potential fraud and retrying can trigger disputes. For generic_decline and do_not_honor, one retry is acceptable, but if it fails again, treat it as a hard decline and ask the customer to update their payment method.
Tip: The 5 most common codes. insufficient_funds, expired_card, generic_decline, do_not_honor, and authentication_required. account for 85% of all SaaS subscription payment failures. Optimizing your handling for just these 5 captures the vast majority of recoverable revenue.
Permanent Fix (Automated)
Building decline-code-specific recovery logic manually means writing a webhook handler that triages every failure by code, applies different retry strategies, sends different email sequences, and tracks outcomes. Here's what the complete version looks like:
// Soft declines that are worth retrying
const SOFT_DECLINES = new Set([
'insufficient_funds', 'processing_error',
'try_again_later', 'reenter_transaction',
'approve_with_id',
]);
// Hard declines where retry is pointless
const HARD_DECLINES = new Set([
'expired_card', 'stolen_card', 'lost_card',
'card_not_supported', 'invalid_account',
'pickup_card', 'incorrect_number',
]);
// Never contact about these
const FRAUD_DECLINES = new Set([
'stolen_card', 'pickup_card',
]);
app.post('/webhooks/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
if (event.type === 'invoice.payment_failed') {
const invoice = event.data.object;
const code = invoice.last_payment_error?.decline_code;
if (FRAUD_DECLINES.has(code)) {
// Don't retry, don't email about this card
await flagForReview(invoice);
} else if (SOFT_DECLINES.has(code)) {
// Stripe Smart Retries will handle the retry timing
// Send a gentle dunning email with card update option
await sendDunningEmail(invoice, { tone: 'informational' });
} else if (HARD_DECLINES.has(code)) {
// Don't retry. go straight to card update request
await sendCardUpdateEmail(invoice, { urgency: 'high' });
} else {
// generic_decline, do_not_honor, unknown
// Treat as soft for retry, but also send update email
await sendDunningEmail(invoice, { tone: 'standard' });
}
}
res.json({ received: true });
});
// SaveMRR does all of the above automatically .
// decline-aware triage, smart email sequences,
// and one-click card update links.SaveMRR automates this entire triage for $19/mo. When a payment fails, SaveMRR reads the decline code and automatically selects the right recovery path: soft declines get a gentle dunning reminder sequence while Stripe retries. Hard declines get an immediate card update email with a one-click link. Fraud declines are flagged and skipped. No webhook code needed. paste your Stripe key and SaveMRR handles the rest.
SaveMRR also prevents the most recoverable decline (expired_card, 15-20% of failures) with pre-dunning alerts. By listening for customer.source.expiring and emailing customers 30 days before their card expires, you prevent the failure from ever happening. Combined with decline-aware dunning, SaveMRR recovers 40-55% of all failed payments vs. 10-15% with Stripe's default emails.
Tip: Run a free Revenue Scan to see your decline code breakdown. Most founders have no idea what's causing their failures. The Scan shows you exactly how many are soft declines (recoverable by retry), hard declines (need card updates), and expired cards (preventable with pre-dunning).
Related Stripe Billing Issues
Frequently Asked Questions
What does generic_decline mean in Stripe?
generic_decline is a catch-all code from the issuing bank. They declined the charge but didn't provide a specific reason. It accounts for 15-20% of SaaS payment failures. Treat it as a potentially soft decline: retry once after 24 hours. If it fails again, send a card update email. About 30-40% of generic_decline charges succeed on a second attempt.
Should I retry an expired_card decline?
No. Retrying an expired card will always fail; the card's expiration date is a hard constraint enforced by the issuing bank. The only fix is for the customer to enter a new card. Send them a card update email immediately with a one-click link to your billing portal. Better yet, prevent this entirely with pre-dunning card expiry alerts 30 days before expiry.
What's the difference between card_declined and a specific decline_code?
card_declined is the error code (the type of error). The decline_code is the specific reason (e.g., insufficient_funds, expired_card). A payment can have code: 'card_declined' with decline_code: 'insufficient_funds'. If the decline_code is missing, the bank declined without specifying why. Stripe categorizes these as generic_decline.
How often do SaaS subscription payments fail?
5-10% of all recurring SaaS charges fail, depending on customer demographics, card types, and regions. The failure rate is higher for international customers (more cross-border declines), prepaid/debit cards (more insufficient_funds), and month-to-month plans (cards expire more often relative to billing frequency). Annual plans have lower failure rates because there are fewer charge attempts per year.
Can I see the raw bank decline code from the card network?
Not directly through the standard Stripe API. Stripe translates the raw network decline code (ISO 8583 response codes like 05, 51, 54) into their standardized decline_code values. If you need the raw network code for debugging, you can find it in the Stripe Dashboard by clicking on a failed payment and looking at the "Decline details" section, which sometimes includes the network code.
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