Checkout Bouncer

Notes

Where the classic WooCommerce checkout actually submits

The form is on the checkout page. The request that places the order is not, and anything wired to the page template never sees it.

4 min read

The order form sits on your checkout page, so most people assume the order goes from there. It does not. Since WooCommerce 2.4 the classic checkout posts to the wc-ajax checkout endpoint, a separate URL that runs the whole payment sequence and answers with JSON. Because the browser never leaves the page, nothing on screen tells you the request went anywhere else.

That gap matters as soon as you bolt anything onto your checkout to keep bots out. A guard attached to the page template sees a page load, not the request that places the order.

What the wc-ajax checkout endpoint is

WooCommerce added its own AJAX handler in version 2.4. Before then, front-end AJAX went through admin-ajax.php, and the WooCommerce developer blog is blunt about why that had to change: "It loads WordPress admin, and in certain cases this can be a huge performance issue because of that extra overhead." So the team built a route that skips the admin bootstrap entirely. In their own testing, "adding items to the cart was around 40% faster using the new endpoints".

The route is a query argument on the site root. WC_AJAX::get_endpoint() builds it by adding wc-ajax to home_url(), which is why the URL in your network tab reads /?wc-ajax=checkout. WordPress then fires the matching action, and the handler behind it is four lines long.

public static function checkout() {
	wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
	WC()->checkout()->process_checkout();
	wp_die( 0 );
}

So that is the entire order. Validation, payment, order creation and the gateway call all happen inside process_checkout().

Three URLs, one handler

Now for the part that catches people out. The checkout action registers three times, not once. WooCommerce keeps a list of AJAX events that logged-out visitors may call, and checkout sits on that list.

For every name on it, core adds three hooks: one for logged-in users, one for everybody else, and one for the fast front-end route. So /?wc-ajax=checkout and /wp-admin/admin-ajax.php?action=woocommerce_checkout arrive at the same method. Two doors, one room.

If your protection knows about one of them, the other still stands open.

Why a page-bound guard misses it

Plenty of anti-bot add-ons attach themselves to the checkout page. First they enqueue a script when the checkout template renders, then they look for a token when that template renders again. And on an ordinary browser visit, that works fine.

But a wc-ajax checkout request never renders the template. It defines a constant, calls process_checkout(), prints a JSON body and stops. There is no page, no header, no footer and no template hierarchy. So anything hanging off a render-time check simply never runs.

WooCommerce treats the route as machinery rather than content, and says so in the headers. Every wc-ajax checkout response carries X-Robots-Tag: noindex and no-cache headers before a single byte of the body.

How to see it on your own store

You do not need a plugin to check this. Open your checkout in a browser, open the network tab, and place a test order.

  1. Watch for a POST while the spinner turns. Its URL carries ?wc-ajax=checkout.
  2. Read the response. It is JSON with a result field, not HTML.
  3. Then open your access log for that same second. The line that matters is the POST to /, not the GET for the checkout page.

If your store runs the Checkout block instead, you will see a request to the Store API rather than this one. Both routes ship in a default install, and the Store API route behaves the same way: a request that creates an order without ever drawing a checkout page.

What this does not mean

None of this is a vulnerability. The wc-ajax checkout endpoint is deliberate, documented behaviour, and every classic WooCommerce store on the internet uses it. It also runs exactly the validation the checkout has always run, because it calls the same method.

So the problem is narrower than it looks. It is an assumption problem. Protection written against the page rather than the request will report itself as active while the requests that matter walk past it.

Nor does one endpoint tell the whole story. A store can also take money through the pay for order page, which runs your gateway against an order that already exists. Counting the doors is the job, rather than hardening whichever one you happened to look at first.

Where Checkout Bouncer fits

Checkout Bouncer scores the checkout with Google reCAPTCHA v3 and reads that score on the request that places the order, rather than on the page that draws the form. The plugin is free, GPL and listed on WordPress.org.

Its limits are worth naming out loud. The plugin guards the WooCommerce checkout and nothing else, so login, registration, comments and contact forms stay somebody else's problem. Nor is it a firewall or a malware scanner. And it uses reCAPTCHA v3 only.

If you would rather just see the list, the free scanner reads a store and reports which checkout routes answer. Two minutes, one use.

See which doors into your checkout are standing open