Notes
WooCommerce captcha not stopping failed orders? Check the Store API path
A captcha on the checkout page only covers the request it is wired to. The block checkout places orders through the Store API, where classic checkout hooks never fire.
8 min read
You installed a captcha on your WooCommerce checkout. The failed orders kept arriving, or stopped for a while and came back. Before you swap it for a different captcha, rule this out. A WooCommerce captcha not stopping failed orders can be working exactly as built and still never run. WooCommerce has two ways a customer can place an order from the storefront. A check wired to one of them is never called on the other.
Two ways an order arrives
The classic checkout is a real HTML form. WooCommerce's template renders <form name="checkout" method="post" class="checkout woocommerce-checkout" ...>, and the front-end script serialises it and posts urlencoded data to /?wc-ajax=checkout. That endpoint calls WC()->checkout()->process_checkout(), as does a non-AJAX fallback on wp_loaded. There, WooCommerce verifies a nonce named woocommerce-process-checkout-nonce, then fires woocommerce_checkout_process and later woocommerce_after_checkout_validation. Those are the two hooks where a classic-checkout captcha does its work.
The block checkout does none of that. It sends JSON to POST /wc/store/v1/checkout, which is the Store API. That is a separate REST namespace at /wp-json/wc/store/v1/ that WooCommerce describes as unauthenticated: "It does not require API keys or authentication tokens for access." The block's documented order flow confirms where the order is placed: "The /checkout StoreApi endpoint is called if there are no payment errors."
What gates that request is a nonce sent in a header named Nonce, created with wp_create_nonce( 'wc_store_api' ). A Cart-Token header works instead, since "When using a Cart-Token a Nonce Token is not required." The route registers GET, POST and PUT with 'permission_callback' => '__return_true', so no capability check runs on the route itself.
The critical part is what that route does not do. Its source contains no reference to WC_Checkout, process_checkout, woocommerce_checkout_process or woocommerce_after_checkout_validation; it builds and processes the order itself. WooCommerce documents the consequence: "Any other WC hooks that fire in the Shortcode process (e.g. woocommerce_checkout_order_processed will not fire on Store API requests from the blocks)".
Why a WooCommerce captcha not stopping failed orders is usually a coverage gap
The mechanics of a WooCommerce captcha not stopping failed orders are mundane. A captcha injects a field into the classic form and validates it on woocommerce_checkout_process. On the block path there is no form to inject into and no hook to validate on. Nothing errors, the settings screen still says enabled, and the order goes through. WooCommerce reported this in December 2024, after auditing captcha plugins. Its post names the ones it audited and the versions that fixed them: "we discovered in some cases this didn't completely stop the attacks either. When our team audited those plugins, we found that they were not protecting the Checkout Block/Store API."
This is bigger than block stores. WooCommerce core registers Store API routes on rest_api_init, not conditionally on what your checkout page contains. Running the classic shortcode checkout does not mean the route is absent from your site. WooCommerce again: "recently we have seen card attacks specifically targeting the Store API alone, even without the use of the Checkout block." The block is not the route's only consumer either; WooPayments and express payment buttons use it too.
And the checkout page is not where an order comes into being. WooCommerce's own tutorial places one with plain HTTP calls and no checkout page loaded. A GET to /wc/store/v1/cart for a nonce, a POST to /wc/store/v1/cart/add-item, then a POST to /wc/store/v1/checkout with a billing address and payment method in the JSON body. Whatever your checkout page renders plays no part in that sequence.
What "server-side" means for reCAPTCHA v3
reCAPTCHA v3 returns a score from 0.0 to 1.0 where, in Google's words, "1.0 is very likely a good interaction, 0.0 is very likely a bot". Note the hedging: it is a risk signal, not a verdict. Google offers 0.5 as a starting default and recommends observing your own traffic before enforcing.
The score only exists on your server. Verification is a server-to-server POST to https://www.google.com/recaptcha/api/siteverify carrying your secret key and the visitor's token, so the browser never sees a score. The token must travel with the request that places the order, and something must check it there. The action name needs checking too: "you should verify that the action name is the name you expect". Otherwise a token minted anywhere else on the site passes at checkout. Tokens expire after two minutes and can be verified only once. A slow checkout hits that limit whenever the token dates from page load rather than from submission.
Why failed orders still cost you
Visa defines enumeration as a fraudster "systematically" testing or guessing payment credentials "to identify which payment credentials are valid". It also names the target: "The most common method is for fraudsters to target legitimate eCommerce merchants that have weak fraud controls in place." Attack transactions are "often less than $10 USD."
A decline is not always free. Whether a declined authorisation costs you anything depends on your pricing plan and your acquirer. Stripe lists authorisation fees among the costs of card testing for merchants on custom pricing plans. On pass-through pricing, a card-not-present authorisation attempt can carry a per-attempt network fee that applies to declines as well as approvals. The card network sets that rate and your acquirer passes it through. It differs between domestic and cross-border authorisations, so the only reliable figure is the one on your own schedule. Stripe adds the longer tail. A high decline rate "might damage the reputation of your business with card issuers and card networks". It "can result in an increased decline rate for legitimate payments, even after card testing ceases."
What to check on your own store
- Confirm which page WooCommerce treats as the checkout. It is set at WooCommerce > Settings > Advanced. Inspecting the wrong page is the easiest way to a confident wrong answer.
- Work out whether it is block or classic. Edit the page, open the three-dot menu, switch to the Code editor. If the content begins
<!-- wp:woocommerce/checkout -->, it is the block checkout. On the live page, view source and search:wc-block-checkoutindicates the block;name="checkout"withclass="checkout woocommerce-checkout"indicates the classic form. - Read the status warnings. WooCommerce > Status flags a checkout page containing neither the shortcode nor the block, or both. "Contains both" is worth acting on: two checkouts on one page.
- Confirm the Store API route exists on your site. Open
https://yoursite.com/wp-json/wc/store/v1in a browser. It returns a read-only index of routes with no side effects; search it forcheckout. Do not test the checkout route itself. A POST attempts a real payment, and a GET can write to an order already held in your session. - Check whether rate limiting is switched on. WooCommerce ships Store API rate limiting, but it is "optional and disabled by default". The toggle is at WooCommerce > Settings > Advanced > Features, labelled "Rate limiting Checkout block and Store API". The documented default is 25 requests per 10 seconds, POST only.
- Read the orders list properly. WooCommerce names one common sign: "a large increase in the number of orders being assigned the Failed status", with order notes recording declined cards. Open them and read the notes; the gateway's own reason text is there. Check the Draft sub-tab too. Draft orders are a Store API artefact, and a scheduled clean-up deletes them every 24 hours, so the evidence may already be gone.
- Cross-check the gateway dashboard. Stripe's signals: a spike in failed or blocked payments, 402 errors with an outcome of
generic_decline, and low-value payments with nonsensical names and email addresses. Refund any you suspect are unauthorised; WooPayments' guidance calls that "of the utmost importance". - Ask the right question about your captcha. Not "is it active". Seeing
recaptcha/api.jsin the page source only proves the browser loads a script on that page. The question is whether the token is verified on your server as part of the request that places the order, on both submission paths.
Where a block-checkout check can run
For a developer: on the block path the hook that fires before the order is finalised is woocommerce_store_api_checkout_update_order_from_request, called with the order and the request. The route catches RouteException and turns it into an API error, which is how a check inside the route stops an order. That behaviour is observed in WooCommerce source rather than published as a supported contract, so test it on staging and re-test after WooCommerce updates. Get the condition wrong on a live store and you block every order on the block path.
Where Checkout Bouncer fits
Checkout Bouncer does this one job. A Google reCAPTCHA v3 check on the WooCommerce checkout, verified on your server, on the classic form submission and on the Store API checkout request. It is a free, GPL plugin on WordPress.org, and the features page lists the checkout surfaces it covers. It uses reCAPTCHA v3 only, it protects the checkout and nothing else, and it is neither a firewall nor a malware scanner. If you would rather fix what you have than install another plugin, run the eight checks above first. The Checkout Scanner page walks the same route-by-route audit. They cost nothing and they tell you whether you have a problem at all.