Notes
How to tell if your store is actually being card tested
Bursts of small failed authorisations, repeated addresses and throwaway emails look nothing like ordinary declines. Here is how to tell them apart with evidence you already have.
7 min read
Card testing detection starts with evidence you already have, because a failed order proves nothing on its own. Cards expire, balances run out, people mistype a CVC and give up. The useful question is whether your failures form a pattern. Three things you already have will answer it: the WooCommerce orders list, your gateway's decline records, and your server's access log.
Card testing detection: the shape to look for
Card testing is someone checking which stolen card numbers still work. Stripe's guidance on card testing describes it as trying "to determine whether stolen card information is valid so that they can use it to make purchases". Attackers run it at scale with scripts that "test a large amount of card information at once".
Two details matter for card testing detection. Attackers prefer amounts that stay quiet: Stripe notes testers "create small amount payments, which cardholders are less likely to notice and report as fraudulent". And attempts arrive fast. WooCommerce says attackers "attack a site with hundreds (or even thousands!) of stolen card numbers in a short period of time".
So the signature card testing detection looks for is not one odd order. Stripe puts it plainly: "You can identify most card testing activity by a significant increase in failed authorization and payments." The symptoms it lists are a spike in failed or blocked payments and a spike in 402 errors. It adds "a spike in suspicious payments with low transaction amounts, often with nonsensical customer names and emails". Ordinary declines are scattered; card testing is a burst.
Card testing detection in your orders list
Start with the Failed status, which WooCommerce defines as "the customer's payment failed or was declined". Its guidance says the giveaway is "a large increase in the number of orders being assigned the Failed status". Those orders "contain multiple notes about cards being declined".
Sort failed orders by date and look at the gaps. Human failures cluster loosely across a day. Scripted ones land seconds apart, in a run of dozens or hundreds, then stop.
Then look at what was in the basket. Attacks converge on whatever is cheapest, because the amount only has to be plausible enough to authorise. WooCommerce's payments documentation describes an attack "where the orders all contain an inexpensive item", and says you "should consider adjusting your fraud protection rules to block those". It does not name a figure, and neither should you: the right floor is whatever sits below your own cheapest genuine order. If one cheap product, a donation or a gift card accounts for most of your failures, that is a strong signal.
Finally, read the billing details across the whole run rather than one order at a time. The repetition gives it away: one billing address behind many card numbers, many emails tied to one address, or a run of throwaway addresses. Stripe treats each as a countable signal, with attributes such as card_count_for_billing_address_hourly and a boolean is_disposable_email, which flags an address that "uses a known throwaway email address provider".
What the gateway's decline records add
Card testing detection sharpens once the gateway's own records are in front of you. Your orders list tells you an attempt failed. Your gateway tells you why, and that is the part you cannot guess.
Decline reasons are not equally informative. Stripe documents do_not_honor and call_issuer as "the card was declined for an unknown reason". Others are specific. incorrect_cvc, invalid_expiry_month and incorrect_number point at someone guessing fields they do not hold. That is what a tester does with a bare card number. A cluster of those, across many cards, in a short window, is not a run of clumsy customers. Some reasons stay hidden from the shopper: Stripe tells merchants not to report lost_card, stolen_card or fraudulent, so they surface only in your records.
Use the gateway to count what the orders list cannot. Stripe's rule attributes name the ratios worth measuring: declined_charges_per_ip_address_hourly, card_count_for_ip_address_hourly, and, in the other direction, total_charges_per_card_number_hourly. That last one matters. Many cards from one address is the obvious case. One card retried from many addresses is the distributed version, and it hides from anything counting per IP. Stripe warns that "simple firewall rules or filters based on a single heuristic such as IP addresses are usually not sufficient to prevent card testing on their own".
What server logs add
Card testing detection has a third source, and it is the one people forget. Logs tell you where the traffic landed, which decides what a fix has to cover. A store has more than one route to a payment. Block checkout posts to the Store API, documented as POST /wc/store/v1/checkout; classic checkout uses its own AJAX endpoint; pay-for-order links are a third path. Search your access log for POSTs to each and compare volumes with a normal day.
Three things there beat the raw count. The user agent, because one repeated agent across hundreds of requests is a script. The timing, because near-identical intervals are machine-generated. The response codes, because successful POSTs that produced failed orders confirm the requests reached checkout.
One caveat before concluding "it is all one IP". If your store sits behind a CDN, load balancer or reverse proxy, the address in your log is the proxy's own. It is the shopper's only when the real client IP arrives in a header such as X-Forwarded-For and your server reads it. Get that wrong and a distributed attack looks like one busy customer.
Ordinary declines, and other things that look the same
Card testing detection is also about ruling things out, because plenty of decline volume is normal. Insufficient funds, expired cards and issuer timeouts are the background noise of taking payments, and more traffic means more of them. A failed order is also not an abandoned one. WooCommerce separates Failed from Pending payment, where "the order has been received, but no payment has been made", and from Cancelled.
Retries imitate the pattern convincingly. Stripe is explicit that "excessive retries (dunning) of payments can look like card testing if they come in extreme spikes with low success rate". If you run subscriptions and a billing batch fires, expect a spike that is not an attack. Gateway trouble looks similar too, appearing as processing_error or issuer_not_available rather than as guessed card fields. Variety is what separates them: retries hit a small set of cards belonging to existing customers, while testing hits many cards you have never seen.
What to check first
- Filter orders to Failed, sort by date, and look at the interval between attempts. Seconds apart in a long run is the clearest tell.
- Check what was being bought. One cheap item dominating the failures matches the documented pattern.
- Open the order notes on a handful and read the decline reasons your gateway wrote there.
- In the gateway, count distinct cards per IP and per billing address, then count IPs per card.
- Look for successful orders inside the same window. This is the urgent part: WooCommerce's guidance is that "refunding any successful orders that you suspect are unauthorized is of the utmost importance".
- Search the access log for POSTs to every checkout route, not only the one you use.
Act quickly rather than diagnose perfectly; the damage compounds. Stripe notes that a high decline rate "might damage the reputation of your business with card issuers". That damage "can result in an increased decline rate for legitimate payments, even after card testing ceases".
One note on the plugin behind this site
Checkout Bouncer is a free, GPL plugin on WordPress.org that does one narrow part of this job. It scores checkout submissions with Google reCAPTCHA v3. That returns a value between 0.0 and 1.0 where, in Google's words, "1.0 is very likely a good interaction, 0.0 is very likely a bot". It maps the routes an order can take into your store, scores the ones that can carry a token, and closes the ones that cannot.
That second half exists because Stripe's advice on CAPTCHAs that fail to stop attacks begins with making validation apply to every request that can create a payment. It covers the checkout only, not login, registration or comments. It is not a firewall or a malware scanner, and it cannot tell you whether last week was an attack. That investigation is still yours, and it works whether or not you install anything. If the pattern you find is a second checkout nobody remembers, duplicate and rogue checkout pages covers that case, and the Checkout Scanner looks for them automatically.