A declined card is a sale you can still save, if the buyer knows what to do next. Most checkouts show "Your card was declined" for every case, so a typo in the CVC looks the same as a stolen card.

Stripe tells you the reason in decline_code. You get it on the card error (err.decline_code when err.type === 'card_error') and on a PaymentIntent as last_payment_error.decline_code. Stripe lists 50 codes. They fall into five buckets, and each bucket needs a different message.

Codes checked against Stripe's decline codes docs on 18 October 2026.

The five buckets

Bucket Codes What to do
Buyer can fix it 16 Ask them to correct the number, CVC, date, address or PIN, or finish 3D Secure.
Retry 5 A temporary error at the bank or network. Retry once.
Another card 6 No funds, a limit, or the card can't pay this. Ask for another card.
Bank must say why 19 The issuer gave no reason. Only the buyer's bank can explain.
Show as generic 4 Fraud, lost, stolen or block list. Never tell the buyer the real reason.

The last row matters most. Stripe's guidance for fraudulent, lost_card, stolen_card and merchant_blacklist is to show a generic decline, so you never tell a fraudster which check caught them.

Drop-in mapping

const BUCKET = { fix: ['authentication_required', 'authentication_not_handled', 'duplicate_transaction', 'incorrect_address', 'incorrect_cvc', 'incorrect_number', 'incorrect_pin', 'incorrect_zip', 'invalid_cvc', 'invalid_expiry_month', 'invalid_expiry_year', 'invalid_number', 'invalid_pin', 'offline_pin_required', 'online_or_offline_pin_required', 'mobile_device_authentication_required'], retry: ['approve_with_id', 'issuer_not_available', 'processing_error', 'reenter_transaction', 'try_again_later'], otherCard: ['card_not_supported', 'expired_card', 'insufficient_funds', 'pin_try_exceeded', 'testmode_decline', 'withdrawal_count_limit_exceeded'], generic: ['fraudulent', 'lost_card', 'merchant_blacklist', 'stolen_card'], // everything else: the bank must say why }; const MESSAGE = { fix: 'Some card details don\'t match. Please check them and try again.', retry: 'We could not approve this payment. Please try again in a moment.', otherCard: 'This card can\'t be used for this payment. Please use another card.', bank: 'Your bank declined this payment. Please contact your bank or use another card.', }; export function declineMessage(code) { if (BUCKET.generic.includes(code)) return MESSAGE.bank; // never reveal fraud checks for (const k of ['fix', 'retry', 'otherCard']) if (BUCKET[k].includes(code)) return MESSAGE[k]; return MESSAGE.bank; } 

Two refinements worth adding:

  • For duplicate_transaction, tell the buyer to check their email first. The earlier payment may already have gone through.
  • For authentication_required on an off-session renewal, you can't show 3D Secure in the background. Email the customer a link to come back and confirm.

Test them before buyers do

Many codes have a Stripe test card. A few:

  • insufficient_funds: 4000 0000 0000 9995
  • expired_card: 4000 0000 0000 0069
  • incorrect_cvc: 4000 0000 0000 0127
  • generic_decline: 4000 0000 0000 0002

The full list

All 50 codes, each with its meaning, what to do, a buyer message you can copy and its test card where one exists, are on one searchable page: Stripe decline codes 2026. The test cards for every scenario are at Stripe test cards.

Disclosure: I build AgentaOS, a merchant of record. The pages above are free and need no account.