WooCommerce Detection From The Outside: One Signal Is Never Enough.

One of our own sites returns 200 on the Store API with ten products and 404 on cart and checkout. Single-signal detection gets it wrong.

Aditya Sharma·10 min read

Short answer: reliable woocommerce detection needs at least two signals. The Store API proves the plugin is active. The cart and checkout pages prove somebody can actually buy something. Those two disagree more often than you would expect, including on one of our own sites.

Four sites, four different answers

Table of detection signals across four sites. woocommerce.com returns 200 on the Store API, cart and checkout. nexterwp.com returns 200 on the Store API with ten products and 200 on shop, but 404 on cart and checkout. theplusaddons.com and wordpress.org return 404 throughout
The second row is ours and it is the interesting one. WooCommerce is active, serving a

catalogue of ten products, with a working shop page and no cart, checkout or account pages at all.*

SiteStore API/shop//cart//checkout/Verdict
woocommerce.com200n/a200200live store
nexterwp.com200, 10 products200404404Woo active, catalogue only
theplusaddons.com404n/a404n/anot a store, mentions “woocommerce” 11 times
wordpress.org404n/a404n/anot a store

A detector keying on the Store API alone would report nexterwp.com as a live store. One keying on /cart/ would report no store. Both would be wrong, and the real answer is more specific than either: WooCommerce is installed and being used as a product catalogue, with the actual selling happening somewhere else.

That is a completely legitimate configuration, and it is exactly the case that breaks simple detection.

I went one layer deeper on nexterwp.com while re-checking this table, because the Store API response carries more than a product count. It also states, per product, whether the item is purchasable:

curl -s "https://nexterwp.com/wp-json/wc/store/v1/products?per_page=3" | python3 -c "
import json,sys
for p in json.load(sys.stdin):
    print(p['name'], p['prices']['price'], p['is_purchasable'])
"
Performance Leggings 3999 True
Casual Striped Tee 4499 True
Denim Dream Jacket 1999 True

Every sampled product reports is_purchasable: True, with a real price attached, on a site whose /cart/ and /checkout/ both return 404. WooCommerce’s own API considers these items buyable in principle, and no path on the site currently lets a visitor act on that. That is not a contradiction inside WooCommerce, is_purchasable describes the product’s own configuration, in stock, priced, not restricted, independent of whether a checkout flow is wired up to let anyone act on it. It is the clearest evidence yet for why this post’s opening claim holds: the plugin’s own data model can describe a fully sellable catalogue while the site itself provides no way to buy anything from it, and only a check that spans both, the API’s opinion of the product and the actual page that would complete a sale, catches the gap between them.

Which WooCommerce detection signals to trust

Table ranking detection signals. The Store API endpoint and cart and checkout page status are strong. WooCommerce plugin asset paths are medium. The word woocommerce appearing in HTML and the generator meta tag are weak
The bottom two are the signals most detection scripts use, because they are the easiest

to grep for. They are also the two that produce false positives on any site that writes about ecommerce.*

SignalStrengthFailure mode
/wp-json/wc/store/v1/productsstrongproves Woo is active, not that anything can be bought
cart and checkout return 200strongcan 302 on an empty cart, which reads as healthy
wp-content/plugins/woocommerce/ assetsmediumabsent when bundled, minified, or CDN-served
the word “woocommerce” in the HTMLweakeleven mentions here, sells nothing through Woo
weakfrequently removed, never names WooCommerce anyway

theplusaddons.com mentions “woocommerce” eleven times in its homepage HTML and sells nothing through it. A word-match detector calls that a store.

The checkout signal has its own trap, which I found while testing checkout monitoring: a /checkout/ URL requested with an empty cart returns a 302 rather than a 200, because WooCommerce redirects away. A detector following redirects records a success and learns nothing.

The row in my own table that turned out to be wrong

I re-ran the exact loop from this post while expanding it, rather than assume the original table still held, and one result had changed enough to be worth investigating rather than just updating a number.

=== theplusaddons.com ===
store api: 404
/shop/         404
/cart/         404
/checkout/     200
/my-account/   404

/checkout/ now returns 200, not 404. That contradicts this post’s own earlier verdict of “not a store.” I checked what was actually there instead of trusting the status code alone:

curl -s https://theplusaddons.com/checkout/ | grep -oE "<title>[^<]*</title>"
<title>Checkout | The Plus Addons for Elementor</title>

A real checkout page, live, serving a title that says exactly what it is. And searching that same page for platform markers found WooCommerce and woocommerce in the HTML, not a generic word mention this time but class names and script paths, the kind that indicate the plugin is actually rendering the page rather than the word simply appearing in prose somewhere on the site.

So theplusaddons.com is a WooCommerce store, selling the plugin itself, and this post’s own detection table called it “not a store” because the one signal it trusted most, the Store API, returned 404. I checked why:

curl -s https://theplusaddons.com/wp-json/ | python3 -c "
import json,sys
d=json.load(sys.stdin)
print([n for n in d['namespaces'] if 'wc' in n.lower()])
"
[]

No wc/ namespace is registered in the REST API discovery document at all. Not blocked, not firewalled, simply never activated, most likely because the site runs WooCommerce’s older shortcode-based checkout rather than the block-based checkout the Store API exists to serve, so nothing on the site has ever called it and WordPress never registered the routes.

That is the actual failure mode this post has been arguing against from the first paragraph, found in its own reference table rather than a hypothetical: the single strongest signal in the ranked list, the Store API, produces a false negative on a real, live, selling WooCommerce installation, because the signal’s presence depends on which checkout architecture the store happens to use, not on whether WooCommerce is active and taking orders. A detector trusting the Store API alone, exactly the “strong” signal this post recommended leaning on hardest, would have filed a real customer-facing store as a brochure site.

The corrected verdict, and the reason a fourth check earns its place: a direct request to the page path itself, checked for a store-specific page title rather than only a status code, catches what an API-only check misses. /checkout/ returning 200 with a title naming itself “Checkout” is a signal in its own right, worth treating as equally strong to the Store API rather than subordinate to it, precisely because the two can disagree, as they do here.

Following the redirect, not just reading its status code

The same re-run turned up something worth adding to how the loop is read, on a site already confirmed as a real, functioning store. woocommerce.com itself now redirects three of the four paths this post checks:

Path requestedStatusRedirects to
/shop/302/products/
/checkout/302/cart/
/my-account/302a WordPress.com OAuth login screen

None of those are failures. /shop/ moved to a renamed URL, /checkout/ redirects to the cart when nothing is in it, which this post already names as expected, and /my-account/ bounces to a centralised login system woocommerce.com uses instead of WordPress’s own. A loop that stops at the status code and treats every non-200 as “not present” would read all three as absence, on a site that is unambiguously a live, selling store. Add -L to follow redirects, or check the location header explicitly with curl -sI, and read where a redirect lands before deciding what it means, the same discipline the checkout monitoring post already applies to the empty-cart case specifically.

Running the same loop against a site that definitely is not a store

For a clean control, I ran the loop against protuno.com itself, which is not WooCommerce, not even WordPress on the visitor-facing side, since the frontend is a separate Next.js application entirely.

=== protuno.com ===
store api: 404
/shop/         308
/cart/         308
/checkout/     308
/my-account/   308

Every path redirects, 308, rather than returning a clean 404 outright, because the frontend strips trailing slashes and issues a permanent redirect to the slash-free version of whatever URL was requested, a routing detail with nothing to do with commerce. Following one of those redirects settles the question properly:

curl -s -L https://protuno.com/cart | grep -oE "<title>[^<]*</title>"
<title>Page Not Found · Protuno</title>

A 404 page, titled as one, reached after the redirect. That is the correct verdict for a site that sells nothing, and it took following the redirect chain to confirm it rather than stopping at the first 308 and guessing what it meant. The general rule this and the theplusaddons.com correction above both point at: a raw status code from any single request in this loop is a clue, not a verdict, and the verdict only becomes trustworthy once you have either read the page title behind a 200, or followed a redirect to see what it actually resolves to.

The loop

D=example.com
printf 'store api: ' ; curl -s -o /dev/null -w '%{http_code}\n' "https://$D/wp-json/wc/store/v1/products"
for p in /shop/ /cart/ /checkout/ /my-account/; do
  printf '%-14s ' "$p"
  curl -s -o /dev/null -w '%{http_code}\n' "https://$D$p"
done

Read the combination rather than any single line. Store API 200 with cart and checkout 404 means a catalogue. All four 200 means a live selling path. Store API 404 with the word appearing in the HTML means somebody writes about ecommerce.

Why bother

Table of four reasons an agency detects this: knowing which client sites take money, applying the right checklist, auditing an inherited portfolio, and scoping a quote correctly
The third row is the honest reason. Detection is not clever. It is something nobody has

written down for the whole portfolio, and the answer changes when a client adds a shop without telling anybody.*

The practical value is triage. A store outage costs trading hours; a brochure outage costs goodwill. Knowing which is which decides who gets phoned at the weekend, and getting that classification wrong in either direction, as this post’s own first table did, means either under-reacting to a real outage or paging someone for a site that was never taking orders.

It also matters for the mail side. A store sends transactional email that a brochure site does not, so its SPF and DMARC posture carries a different weight and a deliverability problem there costs orders rather than enquiries.

It also decides which checks apply. Stores need cart and checkout excluded from caching, gateway monitoring and order-rate baselines. Brochure sites need none of that, and running store checks against them generates noise that trains people to ignore the reports.

Protuno’s free audit detects a storefront from the domain alone and reports whether the cart and checkout addresses answer, which is how the table above was built and, just as importantly, how it kept getting re-checked and corrected while this post itself was being expanded. Straight with you as on every post here: Till, the eCommerce agent, is built and named but not live yet. Until it ships, the loop and the follow-up questions above are the whole method, and neither takes long to run.

Run the loop across your portfolio once. On ours, nexterwp.com came back exactly that way: a catalogue nobody had flagged as anything other than a marketing site, and theplusaddons.com came back the other way, a real store this post’s own first pass had wrongly written off.

Both mistakes cost something different. Missing a catalogue-only site means treating it like a brochure when it is one migration away from taking orders. Missing a live store, the actual error this post’s original table made, means a site is generating revenue with nobody monitoring the checkout for the outages the gateway health post describes. Neither failure announces itself. Both are visible in under a minute to whoever runs the four-line loop and reads what each result actually says, rather than trusting the first signal that answers.

Comments