Portreeve
reference · 7 Sept 202613 min read

Stripe decline codes, and which ones mean card testing

A lookup table of Stripe decline codes, what each means, what to show the customer, and the code mix that signals card testing rather than plain bad luck.

Stripe decline codes are the values Stripe puts in outcome.reason on a Charge and in decline_code on a card error. There are fifty of them for cards, and most of your volume lands on the ones that say nothing: Stripe's own documentation notes that issuers categorize most declines as generic.

This page is a lookup. What each code means, what to put in front of the customer, whether a retry can work, and which combinations of codes indicate that someone is running stolen cards through your checkout rather than that your customers are having a bad day.

Every meaning below is Stripe's own, from the decline codes reference.

Three ways a Stripe payment fails

Before you read the code, read outcome.type. Stripe lists three reasons a payment fails: an issuer decline, a blocked payment, and an invalid API call.

type: "issuer_declined" means the request reached the card network and the issuing bank said no. network_status is declined_by_network, and network_decline_code holds the raw 2-4 digit code the bank returned.

type: "blocked" means Stripe stopped it. Radar or Adaptive Acceptance rejected the payment and, in Stripe's words, "it doesn't obtain authorization from the card issuer." network_status is not_sent_to_network and both network code fields are null.

type: "invalid" means your API call was malformed. These usually do not produce a payment in the Dashboard at all.

The distinction matters because generic_decline appears under both of the first two. Same code, two completely different causes.

In the API, read last_payment_error.decline_code on the PaymentIntent, or outcome.reason on the Charge. A declined card raises a card_error; some codes surface as the error code itself, as in Stripe's own example returning "code": "incorrect_cvc" with "param": "cvc".

The card decline codes

Meanings and next steps below are Stripe's. The retry column is the practical read of them.

CodeWhat it meansShow the customerRetry
generic_declineUnknown issuer reason, or Radar / Adaptive Acceptance blocked the paymentGeneric decline messageNo
do_not_honorUnknown issuer reasonGeneric decline messageNo
call_issuer, no_action_taken, revocation_of_authorization, revocation_of_all_authorizations, security_violation, service_not_allowed, stop_payment_order, transaction_not_allowedAll documented as "declined for an unknown reason"Generic decline message, suggest contacting the bankNo
not_permittedThe payment isn't permittedGeneric decline message, suggest contacting the bankNo
insufficient_fundsNot enough funds or credit"Not enough funds. Try another card."No
card_velocity_exceededBalance, credit limit, or transaction amount limit exceeded"Try another card."No
withdrawal_count_limit_exceededBalance or credit limit exceeded"Try another card."No
incorrect_cvc / invalid_cvcCVC wrong"Check the security code."Yes, after correction
incorrect_number / invalid_numberCard number wrong"Check the card number."Yes, after correction
invalid_expiry_month / invalid_expiry_yearExpiry wrong"Check the expiry date."Yes, after correction
expired_cardCard has expired"That card has expired."No
incorrect_zipPostal code wrong"Check the billing postal code."Yes, after correction
incorrect_addressAddress wrong"Check the billing address."Yes, after correction
fraudulentStripe suspects the payment is fraudulentGeneric decline message onlyNo
lost_cardReported lostGeneric decline message onlyNo
stolen_cardReported stolenGeneric decline message onlyNo
pickup_cardCustomer can't use this card, possibly reported lost or stolenGeneric decline messageNo
merchant_blacklistMatched a value on your own Stripe block listGeneric decline message onlyNo
restricted_cardCustomer can't use this card, possibly reported lost or stolenGeneric decline messageNo
card_not_supportedCard doesn't support this type of purchase"This card can't be used for this purchase."No
currency_not_supportedCard doesn't support the specified currency"This card can't be charged in ."No
invalid_account, new_account_information_availableCard, or the account behind it, is invalid"Contact your bank."No
invalid_amountAmount invalid or above what's allowedCheck your amount firstOnly if your amount was wrong
duplicate_transactionIdentical amount and card submitted very recently"You may already have been charged."No, check for an existing payment
authentication_requiredNeeds 3D SecureRun the authentication flowYes, with 3DS
authentication_not_handledYou proceeded without the required authenticationRun the 3DS/SCA flowYes, with 3DS
mobile_device_authentication_requiredWallet tap needs re-authentication"Tap your device again."Yes
issuer_not_availableStripe couldn't reach the issuer"Something went wrong, try again."Yes, once
processing_errorError while processing the card"Something went wrong, try again."Yes, then later
reenter_transactionIssuer couldn't process it, reason unknown"Something went wrong, try again."Yes, once
approve_with_idPayment can't be authorized"Something went wrong, try again."Yes, once
testmode_declineA Stripe test card number was used"Use a real card."No

Card-reader codes (incorrect_pin, invalid_pin, offline_pin_required, online_or_offline_pin_required, pin_try_exceeded) apply to Terminal, not to web checkout. do_not_try_again and try_again_later are listed as deprecated decline codes; both names live on as advice codes, which is a separate field.

Local payment methods have their own set in the same reference, and the two lists share names. partner_generic_decline, payment_limit_exceeded, and invalid_billing_agreement are LPM-only, but insufficient_funds, expired_card, processing_error, invalid_amount, currency_not_supported, and duplicate_transaction appear in both. The LPM versions map to a different charge outcome reason: an LPM insufficient_funds produces partner_insufficient_funds on the outcome, not the card meaning. Branch on the payment method type before you branch on the code.

Reading Stripe's generic_decline and do_not_honor

These two are most of your decline volume and neither tells you anything. Stripe is direct about it: issuers categorize most declines as generic, and for privacy and security reasons they discuss the specifics of a decline only with their cardholders.

So look at what came with it. Stripe's own suggestion for a generic or do_not_honor decline is to examine the associated data: if the CVC or AVS check failed, that is your actual signal, and asking the customer to verify both before you charge again beats asking them to call their bank.

The other thing to check is whether the issuer saw the payment at all. A generic_decline with network_status: "not_sent_to_network" is Stripe blocking, not a bank declining, and nothing your customer says to their issuer will change it. That case is covered in more detail in the Stripe Radar walkthrough.

Advice codes and how many times you may retry

Alongside the decline code, outcome.advice_code tells you what to do next. There are three values.

Advice codeMeaning
do_not_try_againDon't use this card again for the same transaction
try_again_laterThe issuer declined, but you can retry
confirm_card_dataSome of the information provided is incorrect; have the customer validate their card

There is also network_advice_code, the raw 2-4 digit code from the issuer. Mastercard calls these Merchant Advice Codes. The meaning differs by network, so read it together with the card brand.

Retries are metered, and the two ceilings you care about are not the same number. Stripe recommends a maximum of eight retries for charges that permit them, and warns that additional retries can look like fraud to issuers, which raises declines on your legitimate charges. Visa's rules are looser but sharper-edged: its April 2021 response code rules let a merchant reattempt a Category 2 decline ("issuer cannot approve at this time") up to 15 times in 30 days, while a Category 1 decline ("issuer will never approve") permits no reattempt at all. Stripe's eight is the safer number to build against.

What to show the customer

Four buckets cover every code.

Correctable data (incorrect_cvc, incorrect_number, invalid_expiry_*, incorrect_zip, incorrect_address). Name the field. This is the only class where you should send the customer back into the form.

Funds and limits (insufficient_funds, card_velocity_exceeded, withdrawal_count_limit_exceeded). Say the card was declined and offer another payment method. Do not retry on a timer.

Authentication (authentication_required, authentication_not_handled). Don't write copy for these. Run the 3DS flow.

Everything else. One generic string: "Your card was declined. Contact your bank or try another card." Stripe explicitly instructs you to present fraudulent, lost_card, stolen_card, and merchant_blacklist this way. Telling a card tester that the card is reported stolen tells them exactly what they wanted to know, for free.

The code mix that means card testing

A legitimate decline stream is spread across many cards, many customers, and codes weighted toward insufficient_funds and generic_decline, at your normal amounts, at your normal traffic shape.

Card testing looks different in four ways at once, and no single one of them is conclusive.

Distinct cards per entity. One browser, one IP, or one new account attempting five, twenty, or two hundred different card numbers. This is the strongest single indicator, because no real customer has forty cards.

Small or zero amounts. Stripe notes that testers prefer card setup because "card validation and authorizations during card setup don't typically show up on cardholder statements", and that when they use payments they "create small amount payments, which cardholders are less likely to notice and report as fraudulent."

A burst. Minutes, not hours. Stripe's own identification guide points at a spike in failed or blocked payments and a spike in 402 errors on the Developers page.

A code mix without a fix. generic_decline and do_not_honor dominate, mixed with incorrect_cvc (the tester has the number but is guessing the CVC) and insufficient_funds (the card is live, which is exactly what they were checking for). Notably absent: incorrect_number corrected on the second attempt, which is what a real fat-fingered customer produces.

If you have Radar with rules, the counters to build on are card_count_for_ip_address_hourly, card_count_for_email_daily, and declined_charges_per_ip_address_hourly, all in the supported attributes list. Read the rules reference for the semantics before you tune a threshold: counts exclude the payment currently being processed, so the first attempt in a window reads 0, and an hourly window is "up to 3900 seconds (5 minute buckets)" rather than a clean hour.

The per-IP versions are the weak ones. Testers rotate residential proxies, and shared corporate or mobile egress puts unrelated customers behind one address. Counting per device survives the rotation; velocity checks covers how the windows behave in practice.

This is the thing I built Portreeve for: a checkout_attempt verdict that reads velocity per card, per device, and per identity cluster before the charge is created, and returns allow, review, or block with reason codes in under 100 ms. The card probe rules are device-keyed, so the browser fingerprint snippet is what makes those shapes blockable at ordinary amounts; without a device token they top out at review. It is in open beta.

On the practitioner side, the recurring report is that you end up counting this yourself. In an Ask HN thread on card testing attacks, tinyprojects described rolling "a combination of IP-based bans on creating checkout links + notifications if a purchase has many failed attempts using diff cards," and a_simm reported that "cycling (all) the keys stopped it immediately," which points at the entry path Stripe itself names: your publishable key, used to retry payments without touching your site. The mechanics of that path are in card testing on Stripe.

One number worth knowing while you decide how aggressive to be: in the US, Stripe charges $15.00 for each dispute you receive, and another $15.00 if you respond to it manually, refunded only if you win. That is more than most of these test charges are worth.

Where Radar's blocks appear

A Radar block does not produce an issuer decline code. It produces an outcome with type: "blocked" and a reason describing the block:

"outcome": {
  "network_decline_code": null,
  "network_advice_code": null,
  "network_status": "not_sent_to_network",
  "reason": "highest_risk_level",
  "advice_code": "do_not_try_again",
  "risk_level": "highest",
  "seller_message": "Stripe blocked this charge as too risky.",
  "type": "blocked"
}

reason here carries the block reason instead of a network code. highest_risk_level is the Radar model. Accounts on IC+ pricing can also see low_probability_of_authorization with seller_message: "Stripe blocked this payment as it is unlikely to be authorized.", which is Adaptive Acceptance avoiding network costs rather than fraud prevention.

Blocked payments still show up as failures in your Dashboard, marked Blocked by Stripe, and seller_message is the field to log. Two blocks that both surface as generic_decline to the customer can have entirely different reason values internally.

One quirk: if a Radar rule uses a post-authorization attribute such as :cvc_check: or :address_zip_check:, Stripe has to exchange data with the issuer to evaluate it, so the customer can see a temporary authorization on their statement even though the charge was blocked. Stripe says it generally disappears after a few days.

Reproducing a decline in test mode

Each shape has a test card. Useful when you are writing the handler and want to see your own copy render.

Card numberResult
4000000000000002card_declined with decline_code: generic_decline
4000000000009995card_declined with decline_code: insufficient_funds
4000000000009987card_declined with decline_code: lost_card
4000000000009979card_declined with decline_code: stolen_card
4000000000000069error code: expired_card, no decline code
4000000000000127error code: incorrect_cvc, no decline code
4000000000000119error code: processing_error, no decline code
4100000000000019always blocked by Radar as highest risk

Note the split in that table: the first four come back as card_declined with a decline_code, while the next three surface the reason as the error code and leave decline_code unset. If your handler only reads decline_code, three of these fall through to your generic branch. Include a CVC when you test 4000000000000127, because Stripe skips the CVC check if you omit it.

The corresponding PaymentMethod tokens (pm_card_visa_chargeDeclinedInsufficientFunds, pm_card_chargeDeclinedIncorrectCvc, pm_card_radarBlock, and so on) work in server-side test calls without a card number. Full list in Stripe's testing reference.

Related references

If you want the decline stream counted for you before the charge is created, the free tier screens 1,000 events a month with no card: sign up.

← Back to all posts