Card testing is the step in stolen-card fraud where someone checks which cards still work. A stolen card number is worth little until a bank has confirmed it is open, unexpired, and not yet flagged, and the cheapest way to get that confirmation is to push the number through a real merchant's payment form. If you run a checkout, your payment form is the free oracle. If you are a cardholder staring at a $0 or $1 line you do not recognize, you were the sample.
Stripe's definition lists the aliases you will see elsewhere: card testing is also called carding, account testing, enumeration, and card checking. "Carding" often means the whole business of using stolen cards; "enumeration" and "BIN attack" refer to one specific probe shape covered below. From the merchant's side they all look the same: a burst of small authorizations you did not ask for.
Where card testing sits in the supply chain
Stolen card data arrives in bulk and unverified. Phishing kits, e-commerce skimmers, and breached databases produce dumps of tens of thousands of numbers, and a dump is a mixed bag: some cards already cancelled, some expired, some live. Nobody paying for cards wants to pay for the dead ones.
So the market built a verification layer. Rapid7's write-up of carding shops describes records priced at $4 to $25 on one shop and $10 to $30 on another, with a "check time" window after purchase in which the buyer runs the card through a checker service and gets a refund if it declines. The checker is the thing that hits you. It is a script with a list of cards and a list of merchant endpoints, and it works through both until it has a decline or an approval for every number.
Two things follow from that structure. The attack is not aimed at you: your store is a validation endpoint, chosen because it responds, not because of what you sell. And the attacker is not trying to keep the goods. A successful $1 charge is a side effect. The product is a list of confirmed-live cards, which goes back to the shop at a higher price or straight to someone who will cash it out on a merchant with more to lose.
Stripe reported that at the peak of a 2022 wave its systems were blocking more than 20 million card testing attempts per day, up more than 100x since 2019. That is one processor's view. The volume is large because checking is cheap and each confirmed card pays for thousands of failed probes.
If you are a cardholder who saw a strange $0 or $1 charge
Two cases.
A $0 or $0.01 line that shows as "pending" and then disappears is usually an authorization check, not a charge. Merchants run these legitimately when you save a card, and card testers run them for the reason above. It costs you nothing either way.
A settled $1, $2, or odd-amount charge from a merchant you have never heard of is different. Your card number is in someone's list and it has just been marked live, which means it is about to be used for something bigger. Call the number on the back of the card, report the charge, and ask for a replacement. Disputing the $1 is optional; replacing the card is the point.
If you are a merchant, the rest of this is for you.
The three probe shapes
Card testers need one bit of information per card: does the issuer accept it. There are three ways to ask.
$0 authorizations through card-on-file
Any flow that saves a card for later runs a validation with the issuer without moving money. On Stripe that is the SetupIntent, which the docs describe as "similar to a payment, but no charge is created". Stripe's card-testing guide says this is the method fraudsters prefer, because validations during card setup "don't typically show up on cardholder statements", so nobody reports them.
It is also the shape most merchants leave unguarded. Radar, Stripe's fraud screening, does not scan SetupIntents by default. You have to turn it on, and you pay per confirmation attempt when you do. Any "add a payment method" page, free-trial card capture, or "save card for next time" checkbox that is reachable without a login is a $0 oracle.
Micro-charges
The older shape. The script creates a real payment for $0.50, $1.00, or a random amount under $5, and reads the response. Small amounts are chosen so the cardholder does not notice and so the merchant's own risk rules, tuned to catch large orders, stay quiet.
This is the shape that leaves settled charges on your account and therefore disputes later. It is also the shape that had one Hacker News poster, rameerez, counting "400+ cards and 40+ names under one single IP" in their Stripe dashboard.
BIN enumeration
A card number is a bank prefix, an account number, and a check digit. If an attacker knows one live card from a bank, they know the BIN, and they can generate every Luhn-valid number in that range and submit them with guessed expiry dates and CVVs. Stripe's Radar team described enumeration as "a guessing game" with low authorization rates, and noted the 2022 wave was a shift away from it toward verification attacks using higher-quality stolen dumps.
Enumeration is loud. Thousands of incorrect_number and incorrect_cvc declines in a row, from cards sharing the same first six or eight digits, from few devices. It is the easiest of the three to spot and the one Visa now measures directly, as covered below.
Why the decline response is the leak
The issuer's answer comes back through your processor as a decline code, and decline codes are specific. Stripe's list includes incorrect_cvc, incorrect_number, expired_card, insufficient_funds, stolen_card, and do_not_honor. Each one tells the attacker something different. incorrect_cvc means the number is live and only the CVV is wrong, so try the other 999. insufficient_funds means live, real, and worth holding for payday. stolen_card means burn it.
Stripe's own guidance for stolen_card, lost_card, and fraudulent is to present them to the customer as generic_decline. That is the right advice, and it is the first thing to check in your integration: if your checkout surfaces the processor's decline code to the browser, you are running a card-quality API for the people testing against you. Return the same generic message for every failure and let the real reason go to your logs.
The fix is only partial. An approval is still an approval, and a $0 validation still either succeeds or fails. Generic responses take away the detail, not the bit.
Why small merchants are the target
Large merchants have fraud teams, tuned rules, and processors who will pick up the phone. Small merchants have a publishable key on a public page and a checkout that accepts guests. Stripe's card-testing guide spells out that attackers "can use your publishable key and use it to retry a large number of payments on your website".
Finding these endpoints is not hard. The publishable key sits in page source. Payment plugins for WordPress, WooCommerce, and PrestaShop share the same form structure across thousands of sites, so a script written once works everywhere. In one campaign running from August 2024, skimmers injected into an estimated 49 merchants' sites validated cards against Stripe's deprecated /v1/sources endpoint before exfiltrating them, so that only live cards were sent home.
Key rotation, the common first instinct, buys time and not much else. In one Hacker News thread, a_simm reported that "cycling (all) the keys stopped it immediately", and chrisdkemper replied that they had cycled keys multiple times and the attacks resumed anywhere from "6 weeks to less than a week" later. The publishable key is public by design. Rotating it is like changing the sign on a door that is still unlocked.
What it costs the merchant
The attacker spends nothing. You pay at four points.
Authorization fees. Every probe that reaches the network is an authorization request, approved or declined. On Stripe's standard pricing there is no per-attempt charge, but the card-testing guide lists "authorization fees for custom pricing plans" as a consequence, and Adyen passes through scheme fees on refused transactions and refused retries. Visa has also introduced integrity fees on excessive retries. Adyen's mapping of the categories says that for declines caused by bad card details, you may retry up to 15 times in a 30-day period before both an excessive-retry fee and a data-quality fee apply. A BIN enumeration run blows through that in the first minute.
Dispute fees. Micro-charges that succeed get noticed eventually, and each dispute costs $15 on Stripe whether you win or lose. Refunding proactively avoids the dispute but not the processing cost of the original charge.
Monitoring programs. This is the slow, expensive one. Visa's Acquirer Monitoring Program (VAMP) counts every fraud report and every dispute against your captured payments, and Stripe's summary of the program puts the non-compliant threshold at a 0.5% ratio and the excessive threshold at 1.5% outside CEMEA. VAMP also has a separate enumeration track: an account with 300,000 enumerated transactions and an enumeration ratio over 20% is placed in excessive monitoring. Mastercard's Excessive Chargeback Merchant program starts fining in month two, and by months 7 to 11 the fine is $25,000 per month plus $5 per chargeback over 300. A small SaaS with 200 real payments a month and 40 successful micro-charges from one attack is well over every ratio.
Termination. Stripe's monitoring-programs page says plainly that failing to remediate "can result in the network refusing to process further payments to you". AdBlock's CEO told Stripe they "were on the cusp of losing the ability to process payments" during the 2022 wave. Processor termination is the cost that does not scale with attack size. It is binary.
There is a fifth cost that never shows on an invoice. Issuers learn your merchant descriptor, and a high decline ratio makes them more likely to decline your legitimate customers too, which Stripe's guide notes can persist "even after card testing ceases".
How card testing is detected
The standard advice is a CAPTCHA and an IP rate limit. Both help against the laziest scripts and neither is the answer.
CAPTCHA is solved by services for fractions of a cent per solve, and an attacker who has already bought a dump of cards is not price-sensitive about the solve. It also does nothing for the endpoint the attacker is hitting directly with your publishable key, which never renders your page.
Per-IP rate limits fail in the other direction. Card testers rotate through residential proxies, so each IP sends a handful of probes and stays under any threshold. Meanwhile your real customers behind one corporate NAT or one mobile carrier egress share an address and trip the limit. IP is a soft signal. Count on it and you block the wrong people while the attack walks through.
The signals that separate a checker from a customer are about cards and devices, not addresses.
Distinct cards per device. A customer uses one card, occasionally two. A checker uses hundreds from a single browser session, and even with IP rotation the browser fingerprint stays constant because rotating it breaks the script. The "400+ cards under one IP" count above is the crude version of this; the same count keyed by device fingerprint holds up when the IPs start rotating.
Amount ladders. Real checkouts cluster at your price points. A sequence of $0.00, $0.50, $1.00, $1.03 from one device is a probe ladder: the script is finding the lowest amount your integration will authorize. $0 authorizations at volume from a device with no purchase history is the SetupIntent shape.
Card fingerprint spread. The processor gives you a card fingerprint that is stable across attempts without exposing the number. A card fingerprint seen across many accounts or many devices in a short window is being tested or was just confirmed.
Cluster velocity. Card testers create accounts to reach card-on-file flows. Those accounts share a device, or a disposable email pattern, or a phone. Linking them and counting across the link catches the second and third waves that per-account counters miss.
This is what I built Portreeve to do at the checkout step. One call to checkout_attempt with the card fingerprint, funding type, amount, and a device token from the browser snippet returns allow, review, or block plus reason codes in under 100 ms. The block-strength card rules count distinct cards per device and small-amount and $0 probe ladders, so the fingerprint snippet is required for those shapes to be blocked rather than routed to review. The reason codes tell you which shape fired.
Whichever tool you use, wire the response the same way. block returns the same generic decline the customer would see for any other failure. review lets the payment proceed and lands it in a queue you look at daily, because the false-positive cost of a hard block on a real customer is higher than one $1 fraudulent charge you refund. Stripe's guide recommends refunding suspicious low-amount successes for exactly this reason, and the handling-verdicts reference covers the generic-decline pattern and the review-then-revoke flow in code.
Two integration notes. Put the check in front of the SetupIntent confirmation, not only the payment, or the $0 shape goes straight past it. And do not surface anything from your fraud check to the client beyond the generic failure, for the same reason you already hide decline codes.
Where to go next
If you are on Stripe, the processor-specific playbook is in card testing on Stripe: which Radar rules to turn on, why SetupIntents need the _transactions_ counters, and the order to work through the night the attack is live. The device-fingerprint side of the detection above, including the browser snippet and the device token, is documented in device fingerprinting.
If you are being hit this week, the order of operations is: stop surfacing decline reasons, put a check in front of every endpoint that can authorize a card including card-on-file, refund the successful micro-charges before they become disputes, and then fix the counters so the next wave gets blocked on the second card instead of the four hundredth.
You can start on the free tier with 1,000 screened events a month and no card required.