If your WooCommerce store has been live for more than a few months, you’ve probably seen it: fake orders with garbage addresses, hundreds of failed payment attempts in one night, or a wave of spam customer registrations. Bots love checkout pages — they use them to test stolen credit card numbers (card testing or “carding”), create junk accounts, and probe for weaknesses. The standard fix is to add reCAPTCHA to your WooCommerce checkout, and this guide covers every way to do it: with code, with a plugin, on the classic checkout, and on the newer block-based checkout — where most tutorials on the internet quietly stop working.
Before you start: which checkout is your store running?
This is the step almost every guide skips, and it determines everything that follows. WooCommerce currently ships two different checkouts:
Classic (shortcode) checkout — the checkout page contains the woocommerce_checkout shortcode. It renders on the server as a normal HTML form, submits as a regular POST, and PHP hooks like woocommerce_checkout_process fire exactly as they have for a decade. All the classic tutorials assume this.
Block checkout — the default for new stores since WooCommerce 8.3. The checkout page contains the Checkout block, which is a React application. It talks to your server through the Store API (/wc/store/v1/checkout) with JSON requests — there is no traditional form submit, and the classic PHP validation hooks never fire.
To check yours: edit the Checkout page in wp-admin. If you see the woocommerce_checkout shortcode, you’re on classic. If you see checkout blocks, you’re on the block checkout. Code written for one does nothing on the other — if you’ve ever pasted a reCAPTCHA snippet and wondered why no badge appeared, this is almost always why.
Choosing a reCAPTCHA type
reCAPTCHA v2 checkbox — the familiar “I’m not a robot” box. Visible friction, but customers understand it, and it’s the easiest to validate: the widget produces a response token, your server verifies it, done.
reCAPTCHA v2 invisible — no checkbox; the challenge only appears for suspicious traffic. Less friction, slightly fiddlier integration because the challenge must be triggered programmatically on submit.
reCAPTCHA v3 — fully invisible, returns a score from 0.0 (bot) to 1.0 (human) instead of a pass/fail. Zero customer friction, but you must choose a score threshold, and — the part that catches everyone — v3 tokens expire after two minutes. Checkout is exactly the kind of page where a customer types card details for five minutes before submitting, so a naive v3 integration generates a token on page load and then rejects a completely legitimate customer at submit time. A correct v3 implementation regenerates the token immediately before the checkout request is sent. Keep this in mind whether you code it yourself or evaluate a plugin.
Whichever you choose, get your keys at Google’s reCAPTCHA admin console (google.com/recaptcha/admin): register your domain, pick the type, and copy the site key (public, goes in the page) and secret key (server-side only, never in HTML).
Method 1: Add reCAPTCHA to the classic checkout with code
If you’re comfortable with a child theme’s functions.php or a snippets plugin, here is a minimal, working v2-checkbox integration for the classic checkout:
// 1. Load the reCAPTCHA API on the checkout page only
add_action( 'wp_enqueue_scripts', function () {
if ( is_checkout() && ! is_wc_endpoint_url() ) {
wp_enqueue_script(
'google-recaptcha',
'https://www.google.com/recaptcha/api.js',
array(), null, true
);
}
} );
// 2. Render the widget above the Place Order button
add_action( 'woocommerce_review_order_before_submit', function () {
echo '<div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>';
} );
// 3. Verify the token server-side during checkout validation
add_action( 'woocommerce_checkout_process', function () {
$token = isset( $_POST['g-recaptcha-response'] )
? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) )
: '';
if ( '' === $token ) {
wc_add_notice( __( 'Please confirm you are not a robot.', 'your-textdomain' ), 'error' );
return;
}
$response = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', array(
'timeout' => 10,
'body' => array(
'secret' => 'YOUR_SECRET_KEY',
'response' => $token,
'remoteip' => isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '',
),
) );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $body['success'] ) ) {
wc_add_notice( __( 'reCAPTCHA verification failed. Please try again.', 'your-textdomain' ), 'error' );
}
} );
Three details worth understanding rather than copy-pasting past:
The verification must be server-side. Hiding the Place Order button behind a JavaScript check stops no one — bots don’t run your JavaScript; they POST directly to your server. The siteverify call from PHP is the actual protection.
One important gap: because the classic checkout refreshes its order-review fragment over AJAX, aggressive optimization plugins that defer or delay scripts can prevent the widget from rendering. If the checkbox doesn’t appear, exclude recaptcha/api.js from your optimizer’s defer/delay rules first.
This covers checkout only. Bots also hit your login, registration, lost-password, and pay-for-order pages — each needs its own field and validation hook if you go the manual route. That’s five more integrations of the same pattern.
Why this code does nothing on the block checkout
Paste the snippet above into a store running the block checkout and… nothing happens. No widget, no validation. The reasons are structural:
The block checkout is a React app, so woocommerce_review_order_before_submit never runs — that hook belongs to the server-rendered template. Injecting a widget means registering a component through WooCommerce’s checkout block extensibility APIs, in JavaScript, built with the WooCommerce blocks toolchain.
Submission goes through the Store API as JSON, so woocommerce_checkout_process never fires either. Your token has to travel inside the Store API request (via extensionData) and be validated in a woocommerce_store_api_checkout_update_order_from_request callback or an ExtendSchema endpoint on the server.
And if you use reCAPTCHA v3, the two-minute token expiry becomes acute: the correct pattern is to intercept the checkout request as it’s about to be sent and fetch a fresh token at that moment — not on page load. Getting this wrong produces the classic symptom: the first checkout attempt fails with a “verification expired” error and the second one succeeds, because the retry happens quickly enough.
All of this is buildable — we’ve built it — but it’s a JavaScript project with a webpack build step, not a functions.php snippet. Which brings us to the practical answer for most store owners.
Method 2: Use a plugin that supports both checkouts
This is our own plugin, so judge the claim against the technical detail above: reCAPTCHA for WooCommerce handles both the classic and the block checkout, with v2 checkbox, v2 invisible, and v3 support — including the fresh-token-on-submit handling that v3 needs on the block checkout. Beyond checkout it covers the other bot entry points in one switch each: login, registration, lost password, pay-for-order, add-payment-method, and WooCommerce’s AJAX endpoints, plus Cloudflare Turnstile as an alternative if you’d rather avoid Google entirely.
Whether you use ours or another plugin, the evaluation checklist is the same: does it explicitly support the block checkout (not just classic)? For v3, does it regenerate tokens at submit time? Does it validate server-side? Does it cover login and registration, not only checkout?
Common problems and fixes
“reCAPTCHA verification expired” on the first checkout attempt. The v3 token-staleness problem described above — the token was generated at page load and died before the customer finished typing. Fix: an integration that fetches the token at submit time.
Widget doesn’t appear at all. On classic checkout: a script optimizer deferring the API, or a caching plugin serving a stale fragment. On block checkout: your integration only supports classic. Also check the browser console for a “Invalid site key” error — keys are per-domain, so staging and live need the domain added in the reCAPTCHA console.
Legitimate customers blocked by v3. Your score threshold is too aggressive. 0.5 is the standard starting point; if real customers report failures, drop toward 0.3 before abandoning v3. VPN users and privacy-focused browsers score lower through no fault of their own.
Checkout page is cached. Never page-cache checkout. Most caching plugins exclude it by default, but verify — a cached checkout can serve one customer’s token (or nonce) to another, breaking far more than reCAPTCHA.
Orders still coming from bots at the payment gateway. Card-testing bots sometimes bypass the checkout page and hit payment endpoints directly. Protecting pay-for-order and the WooCommerce AJAX endpoints closes this; so does enabling your gateway’s own fraud tooling alongside reCAPTCHA. They’re complementary layers, not alternatives.
Frequently asked questions
Does reCAPTCHA slow down my checkout page?
It adds one script from Google’s CDN, typically a few tens of kilobytes, loaded asynchronously. On a normal store the impact is negligible compared to payment gateway scripts. Load it only on the pages that need it (as the code above does) rather than site-wide.
Which is better for WooCommerce checkout: reCAPTCHA v2 or v3?
v2 checkbox is simpler and gives a clear pass/fail; v3 is invisible but needs a score threshold and correct token-refresh handling because tokens expire after two minutes. For most stores, v2 checkbox or v2 invisible on checkout is the pragmatic choice; v3 shines on login and registration where friction matters more.
Why doesn’t my reCAPTCHA code work on the new WooCommerce checkout?
The block checkout is a React application that submits through the Store API, so classic PHP hooks like woocommerce_checkout_process never fire and template hooks never render. Block checkout support requires a JavaScript integration built on WooCommerce’s checkout block APIs, plus Store API server-side validation.
Can I use Cloudflare Turnstile instead of Google reCAPTCHA?
Yes. Turnstile is a privacy-friendly alternative with a similar token model and no Google dependency. The same rules apply: verify tokens server-side, and make sure your integration supports whichever checkout your store runs.
Will reCAPTCHA stop card-testing attacks completely?
It stops the bulk of automated attempts at the checkout form, but determined attackers may target payment endpoints directly. Combine reCAPTCHA on all entry points with your payment gateway’s fraud protection and rate limiting for full coverage.

