You open WooCommerce and the Orders screen is full of orders you do not recognize. Dozens or hundreds of them, marked Failed or Pending payment, often for the same cheap product, with random names, odd email addresses and billing addresses that do not match the country they came from. Your payment gateway emails you about declined payments, or Stripe or PayPal warns you about unusual activity. Now and then one order in the batch actually went through.
This is almost always a card testing attack (also called carding). Someone holding a list of stolen card numbers is using your checkout to find out which cards still work. They do not want your product. They want a yes or a no from the card issuer, and your store gives them that answer for free. This guide is for store owners and for the designers and developers who look after their stores. It covers how to confirm the attack, how to slow it down today, how to clean up, and how to make your store a poor target next time.
Card testing is not the same as being hacked. Nobody needs your WordPress password to do it: the attacker uses your public checkout like a shopper, just thousands of times. If instead real customers report fraud on their cards after buying from you, that is a different and more serious problem. Go straight to the checkout skimmer guide.
Why it matters even when the payments fail
- Fees. Depending on your gateway and your contract, you may pay for attempts that fail, not only for successful charges. Check your gateway’s pricing page or merchant agreement for how failed authorizations and disputes are billed.
- Your gateway account. Processors watch decline rates closely. A sudden burst of declines can bring warnings, extra checks, held payouts or restrictions until the attack stops.
- Chargebacks. Every test that succeeds is a charge on a stolen card. The real cardholder will dispute it, and disputes carry fees and count against your account. Refunding those charges yourself, quickly, is usually better than waiting for the dispute.
- Load on the store. Hundreds of checkout requests a minute slow the site for real customers and fill your database with junk orders.
- Emails to strangers. When a test succeeds, WooCommerce sends an order confirmation to whatever email address the attacker typed, which may belong to an innocent person.
What is happening
Card testing tools are scripts. They load your site, add a product to the cart, and send the checkout form straight to the endpoint WooCommerce uses to place orders, over and over, with a different card each time. On the classic checkout (the [woocommerce_checkout] shortcode) that endpoint is /?wc-ajax=checkout. On the block checkout it is /wp-json/wc/store/v1/checkout, part of the WooCommerce Store API. Some attackers skip checkout and use My account > Payment methods > Add payment method, which asks the gateway to verify a card without charging it. In Stripe that shows up as a stream of failed SetupIntents rather than failed payments.
Every attempt creates an order (or a hidden checkout draft on the block checkout) and an API call to your gateway. The issuer answers, the gateway passes the answer back, and WooCommerce marks the order Failed or leaves it Pending payment. The attacker logs which cards passed and sells or uses them elsewhere.
Cheap products are popular targets because a small charge is less likely to be noticed by the real cardholder. Donation forms and “name your price” products are favorites because the attacker can choose a tiny amount.
Step 1: Confirm it is card testing
Look for several of these together. One odd order is just an odd order.
| Sign |
Where to look |
| Many orders in a short window, often overnight in your time zone |
WooCommerce > Orders, sorted by date |
| The same low-cost product, donation or name your price item every time |
The order list totals and items |
| Status Failed or Pending payment, with order notes showing declines such as “Your card was declined”, “Your card’s security code is incorrect” or “Your card has insufficient funds” |
The order notes panel on each order |
| Guest orders with random names, mismatched billing details and throwaway email addresses |
The billing column and customer details |
| Origin shown as Unknown on many orders |
The Origin column (order attribution) in recent WooCommerce versions. This is a hint, not proof: bots that post straight to the checkout usually carry no attribution data, but some real orders show Unknown too, for example when consent tools block tracking. |
A spike in failed payments, decline codes such as card_declined, incorrect_cvc or insufficient_funds, or many failed SetupIntents |
Your gateway dashboard (Stripe Payments list, WooPayments Transactions, PayPal activity) |
| Lots of POST requests to the checkout endpoint from a few IP addresses, or from many addresses with the same user agent |
Your server access log (below) |
Count the failed orders with WP-CLI
If you have SSH access, these commands give you numbers fast. Connect over SSH and cd into the site folder, the one that contains wp-config.php. WP-CLI is wp on most hosts and wp-cli on some container platforms. The WP-CLI cheat sheet covers the basics.
wp user list --role=administrator --fields=ID,user_login
wp wc shop_order list --user=1 --status=failed --after=2026-11-06T00:00:00 --format=count
What it does: the first line lists administrator accounts so you can pick an ID. The WooCommerce commands run through the WooCommerce REST API and need to act as a user with permission to read orders, which is what --user=1 does; replace 1 with an administrator ID from the first command. --status=failed limits the list to failed orders, --after takes a date and time in ISO 8601 format (replace it with the start of the attack), and --format=count prints a single number. Good output: a small number in line with a normal day. Bad output: a number in the dozens or hundreds. Run it again with --status=pending as well.
wp wc shop_order list --user=1 --status=failed --per_page=50 --fields=id,date_created,total,payment_method --format=table
What it does: lists the 50 most recent failed orders with their ID, time, total and gateway. --per_page sets how many rows come back and --fields picks the columns. Identical totals arriving seconds apart are the classic pattern.
Find the source in the access log
grep -E "wc-ajax=checkout|wc/store/v1/checkout" /PATH/TO/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 20
Where: server SSH. Placeholder: /PATH/TO/access.log is your site’s access log; your host’s control panel or support team can tell you where it lives. What each part does: grep -E keeps only lines for either checkout endpoint, awk '{print $1}' prints the first field (normally the visitor’s IP address), sort | uniq -c counts each address, sort -rn puts the busiest first and head -n 20 shows the top 20. Good output: a long tail of addresses with one or two requests each. Bad output: a handful of addresses with hundreds of requests. If your site sits behind Cloudflare or another proxy, the first field may be the proxy’s address rather than the visitor’s; Cloudflare’s own Security Events or Analytics then show the real sources. The Linux and SSH cheat sheet explains these tools in more depth.
Write down the time window, the order IDs, the product targeted and the top IP addresses. You will need them for the gateway and for your records.
Step 2: Slow it down in the first hour
No single setting stops every attack. Stripe’s own guidance is that simple filters based on one signal, such as IP address, are usually not enough on their own, and that layered defenses work best. Work down this list in order; the first few take minutes.
Tell your gateway and turn on its fraud tools
- Stripe. Contact Stripe support and say you are under a card testing attack. Stripe runs automatic card testing protections and Radar, its fraud screening, on its side. In the Dashboard, Radar lets you review blocked payments and, depending on your plan, add custom rules and block lists (for example blocking an IP address or email address you have seen in the attack). Check in the Dashboard which Radar features your Stripe plan includes. If you use 3D Secure rules, requesting 3D Secure more often adds a step that card testing scripts usually cannot complete.
- WooPayments. Go to Payments > Settings and find the Fraud protection section. WooPayments is built on Stripe and uses Radar underneath. The Basic level blocks payments when the issuing bank cannot verify the security code; the Advanced level lets you add your own rules. WooPayments documents an Enable public key encryption option under its advanced settings that can help during a card testing attack, and says it is not meant to stay on all the time. Turn it on during the attack and review it afterward.
- PayPal. If you take cards through PayPal’s advanced card fields, check the 3D Secure option in the plugin’s card settings and set it to trigger 3D Secure for every transaction while the attack lasts (the exact label varies between plugin versions). Contact PayPal merchant support with your evidence.
Put a challenge on the checkout
A CAPTCHA, or Cloudflare Turnstile, on the checkout makes each attempt cost the attacker something. Two cautions:
- It must protect the checkout you actually run. When WooCommerce audited anti-spam and CAPTCHA plugins in late 2024, it found some did not protect the Checkout block or the Store API at all, so the bots simply went round them. Pick a plugin that explicitly supports the Checkout block if you use it, then confirm by watching your failed order count fall.
- Test a real order straight away. A challenge that fails for real shoppers costs more than the attack. Place an order in a private window on a phone and on a computer.
Avoid a CAPTCHA that appears on every page; it only needs to guard the checkout and the add payment method form.
Turn on WooCommerce’s rate limiting (block checkout)
If your checkout page uses the Checkout block, go to WooCommerce > Settings > Advanced > Features and check Rate limiting Checkout block and Store API. WooCommerce’s developer documentation says this switch applies rate limiting only to the Place order flow of the Checkout block (POST requests to the Store API checkout endpoint). It does not protect the classic shortcode checkout, which does not use the Store API. Not sure which one you run? The checkout troubleshooting guide shows how to tell.
Developers can tune the Store API limiter with the woocommerce_store_api_rate_limit_options filter. According to the WooCommerce documentation it is off by default, and its defaults are 25 requests per 10 seconds per user or IP address, with proxy support off. A stricter setting looks like this, placed in a small site-specific plugin or a trusted code snippets plugin:
add_filter( 'woocommerce_store_api_rate_limit_options', function () {
return [
'enabled' => true,
'proxy_support' => true,
'limit' => 5,
'seconds' => 60,
];
} );
What each line does: enabled switches limiting on, limit and seconds allow five requests per visitor per minute (an example to adjust, not a magic number), and proxy_support tells WooCommerce to read the visitor’s address from proxy headers. Only set proxy_support to true if the site really sits behind Cloudflare, a load balancer or a similar proxy. Without a proxy, an attacker can fake those headers; with a proxy but without this setting, every shopper can look like the same address and real customers get blocked together. Good result: attackers start receiving HTTP 429 (Too Many Requests) responses, visible in your access log.
Rate limit at the edge
For the classic checkout, and as a second layer for the block checkout, add a rate limiting rule at your CDN or web application firewall. In Cloudflare this is a rate limiting rule in the WAF section, matching POST requests whose path is /wp-json/wc/store/v1/checkout, or whose query string contains wc-ajax=checkout. Available periods and actions depend on your plan, so check what yours includes. If you do not use a CDN, ask your host whether their firewall can limit POSTs to those two addresses. The Cloudflare settings cheat sheet lists where these rules live, and the Cloudflare block guide helps if your own rule starts catching real customers.
Take away the target for now
- Hide the product being tested. WooCommerce’s own card testing advice suggests temporarily making low-cost, donation or name your price products private. Set the product’s visibility to Private, or mark it out of stock, until the attack stops.
- Turn off guest checkout temporarily. In WooCommerce > Settings > Accounts & Privacy, uncheck the option that lets customers place orders without an account. Labels have changed between versions, so look for the guest checkout setting. This costs some real sales, so treat it as a short-term measure.
- Stop new account sign-ups from My account on the same screen if attackers are registering accounts to reach Add payment method.
- Block persistent sources. If one IP address or a small range is doing most of the work, block it in your firewall. Expect the attacker to switch addresses; this buys time, it does not fix the problem.
Step 3: Clean up the orders
- Export before you delete. In WooCommerce > Orders, filter by status Failed and the attack dates, and export or copy the order IDs, times, totals and IP addresses (each order shows the customer IP in its details). Your gateway may ask for this, and you want a record.
- Refund anything that succeeded. Look for attack orders marked Processing or Completed. Do not ship them. Open each one and use the refund button that refunds through the gateway (it reads like Refund … via Stripe), so the money actually goes back to the card. A refund recorded only in WooCommerce does not move money.
- Trash the failed orders. Select them in the Orders list and use Bulk actions > Move to Trash. Empty the trash once your export is safe.
- Check stock. Unpaid orders can hold stock for the number of minutes set in Hold stock (minutes) under WooCommerce > Settings > Products > Inventory, after which WooCommerce cancels them. If canceled orders did not put stock back, correct the product’s stock count by hand.
- Remove fake accounts. If the attacker registered accounts, list the newest ones and delete those that belong to the attack:
wp user list --role=customer --orderby=registered --order=DESC --number=50 --fields=ID,user_login,user_email,user_registered
What it does: shows the 50 most recently registered customers. --orderby=registered --order=DESC sorts newest first and --number=50 limits the list. Delete fakes in the admin Users screen, checking each one first, because a real customer may have registered during the same window.
Step 4: Talk to your payment gateway
Send a short, factual message to your gateway’s support team, even if they contacted you first:
- The time window of the attack in UTC, and whether it is still running.
- The number of failed attempts and the order IDs, plus any that succeeded and have been refunded.
- The top IP addresses and the product or form targeted.
- What you have changed: CAPTCHA, rate limits, guest checkout, fraud rules.
- Questions: is the account restricted or are payouts on hold, what else do they recommend, and how are fees for these attempts handled.
Keep their reply with your notes. A printable version of this whole response lives in the store fraud and card testing checklist.
Step 5: Make your store a poor target
Attackers move on when a store becomes slow and expensive to test against. Keep these in place after the attack ends:
- Keep the checkout challenge on, and retest a real order after every WooCommerce, theme or gateway update.
- Keep the rate limits, both WooCommerce’s and the edge rule.
- Use your gateway’s fraud tools properly. Review Radar or WooPayments Fraud protection rules every month or two. Do not loosen them just because things are quiet.
- Consider an anti-fraud extension. WooCommerce’s Anti-Fraud extension documents velocity controls such as limits on order attempts, failed payments and added payment methods, plus a checkout CAPTCHA, for both the classic and block checkout. Its documentation also advises against simply lowering thresholds because a store is under attack, so read its guidance before tightening everything.
- Watch for spikes. A daily look at failed orders, or an alert from your gateway on declines, catches the next wave in minutes rather than days.
- Keep the gateway plugin current. Gateway plugins add protections over time, and old versions miss them. The safe update routine shows how to do this without breaking checkout.
- Think about very cheap products. A product under a few currency units with guest checkout is the easiest test target there is. Bundle it, require an account, or make sure the challenge covers it.
Bots that hammer the whole site, not just checkout, are covered in the high CPU bots guide and the traffic flood guide.
If none of that worked
If attempts keep arriving after a CAPTCHA and rate limits, check that the protection actually runs on the endpoint the attacker uses. Search the access log for the attack requests and look at the path: if they hit /wp-json/wc/store/v1/checkout while your CAPTCHA plugin only guards the classic form, or they hit /?wc-ajax=checkout while you only switched on the Store API limiter, the defense and the attack are in different places. Also check the My account add payment method page and any other forms that call the gateway, such as donation or booking plugins.
Then open a ticket with your host and include:
- The endpoint being attacked and sample log lines with timestamps.
- The busiest source addresses and user agents.
- What you have already enabled, and a request for rate limiting or blocking at their firewall.
If sales are stopped right now, the store checkout emergency page lists the first things to check. The headers tool confirms whether a CDN is in front of your site.
See also