How to Detect WooCommerce on Any Website (API Guide)

July 26, 2026 · 11 min read

WooCommerce is not a platform you can spot from the outside the way you can spot a hosted storefront. It is a plugin bolted onto a WordPress install that runs on whatever hosting the merchant already had, at whatever domain they already owned. There is no dedicated CDN hostname, no vendor DNS record, no checkout domain that gives it away. From the URL alone, a WooCommerce store and a static brochure site look identical.

What WooCommerce does leave behind is a set of specific artifacts in the HTML it renders. This guide covers what those are, why one carries a usable version number, how to query them through the DetectZeStack API, and the one failure mode that will quietly corrupt your data if you do not guard against it. Every API response below comes from a live scan run while writing this article.

What Makes WooCommerce Detectable

The wp-content/plugins/woocommerce Asset Path

WooCommerce enqueues its frontend JavaScript through the standard WordPress asset pipeline, which means the script is served from the plugin's own directory. Every store running it loads at least one file from a path shaped like this:

<script src="https://example.com/wp-content/plugins/woocommerce/assets/js/frontend/woocommerce.min.js?ver=9.3.6"></script>

Two things are happening in that one line. The path segment /wp-content/plugins/woocommerce/ is the detection: nothing else puts a directory by that name in an asset URL. And the ?ver= query string is WordPress's cache-busting parameter, which it populates with the version the plugin declares. That is where the version number in the API response comes from.

This is a real fetch of yithemes.com, grepped for WooCommerce asset references:

curl -sL -A "Mozilla/5.0" https://yithemes.com/ \
  | grep -oE 'woocommerce/assets/js/[^"]+'
woocommerce/assets/js/frontend/woocommerce.min.js?ver=9.3.6
woocommerce/assets/js/js-cookie/js.cookie.min.js?ver=2.1.4
woocommerce/assets/js/select2/select2.full.min.js?ver=4.0.3

The first line is the one that matters. The bundled js-cookie and select2 versions are those libraries' own, not WooCommerce's — worth knowing if you ever parse these paths by hand and wonder why you got three different numbers.

woocommerce_* Cookies and body Classes

Two more signals show up on a store page, and they are useful for manual verification even though they are not what the API keys off.

The first is the woocommerce-no-js class, which WooCommerce adds to the body element and then strips with JavaScript on load. It sits alongside whatever other classes the theme emits. From the same live fetch of yithemes.com:

<body class="home page-template-default page page-id-22179 wp-embed-responsive
  theme-yithemes woocommerce-no-js ywcas-yithemes-theme yith-wcstripe
  yith-wcan-pro eur site-lang-en-US yith-not-stripe-customer safari">

The second is cookies. Once a visitor interacts with a cart, WooCommerce sets woocommerce_cart_hash, woocommerce_items_in_cart, and a wp_woocommerce_session_ cookie keyed by a hash. You can see these in Chrome DevTools under Application > Cookies. They are strong confirmation when present, but they are behavioural — a first, uninteracted page load usually will not set them, so their absence proves nothing.

The WordPress Dependency Signal

WooCommerce cannot exist without WordPress, which makes the pairing itself a check on your data. Every genuine WooCommerce detection should come back alongside a WordPress detection. In the API response, WooCommerce is filed under two categories at once — Ecommerce and WordPress plugins — which reflects exactly this dual nature.

The inverse is not true and is the more useful fact for prospecting: most WordPress sites are not stores. Live scans of wpbeginner.com and kinsta.com both return WordPress and an empty Ecommerce category. "Runs WordPress" is a population of hundreds of millions; "runs WooCommerce" is the commercial slice of it.

Manual Ways to Detect WooCommerce (and Where They Break)

View Source and DevTools

For a single domain, the manual check takes about fifteen seconds. Open the site, press Ctrl+U (or Cmd+Option+U on macOS), and search the source for plugins/woocommerce. If you get a hit, the site runs WooCommerce and the ?ver= on that line is your version number. Failing that, search the body tag for woocommerce-no-js, or open DevTools and look for the cart cookies after adding something to a cart.

From a terminal it is a one-liner:

curl -sL -A "Mozilla/5.0" https://example.com/ | grep -c "plugins/woocommerce"

Why Manual Checks Do Not Scale

That workflow is fine once and useless at a hundred domains, for reasons that have nothing to do with typing speed:

Detect WooCommerce With the DetectZeStack API

Try It Free With GET /demo

The /demo endpoint runs a full scan with no API key and no signup, so start there. Here is a real scan of woocommerce.com itself, filtered to the one detection:

curl -s "https://detectzestack.com/demo?url=woocommerce.com" \
  | jq '.technologies[] | select(.name == "WooCommerce")'
{
  "name": "WooCommerce",
  "version": "11.1.0",
  "categories": [
    "Ecommerce",
    "WordPress plugins"
  ],
  "confidence": 100,
  "description": "WooCommerce is an open-source ecommerce plugin for WordPress.",
  "website": "https://woocommerce.com",
  "icon": "WooCommerce.svg",
  "source": "http"
}

Read the fields. confidence is 100 because the asset path matched directly rather than being inferred. source is http, meaning the evidence came from the HTML response — and for WooCommerce it will always be http, which turns out to matter a great deal later in this article. version is populated straight from the script's ?ver= string.

Full Stack Scan With GET /analyze

The boolean is rarely the interesting part. The full /analyze response is what turns a domain into a qualified record, because it returns everything else on the page. Here is the same woocommerce.com scan through the authenticated endpoint, trimmed to the fields that matter:

curl -s "https://detectzestack.p.rapidapi.com/analyze?url=woocommerce.com" \
  -H "X-RapidAPI-Key: YOUR_KEY" \
  -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" | jq '.'
{
  "url": "https://woocommerce.com",
  "domain": "woocommerce.com",
  "technologies": [
    { "name": "WooCommerce",     "categories": ["Ecommerce", "WordPress plugins"], "confidence": 100, "version": "11.1.0", "source": "http" },
    { "name": "WordPress",       "categories": ["CMS", "Blogs"],                   "confidence": 100, "version": "7.0.2",  "source": "http" },
    { "name": "WordPress VIP",   "categories": ["PaaS"],                           "confidence": 100, "version": "",       "source": "http" },
    { "name": "Yoast SEO",       "categories": ["SEO", "WordPress plugins"],       "confidence": 100, "version": "27.3",   "source": "http" },
    { "name": "Nginx",           "categories": ["Web servers", "Reverse proxies"], "confidence": 100, "version": "",       "source": "http" },
    { "name": "PHP",             "categories": ["Programming languages"],          "confidence": 100, "version": "",       "source": "http" },
    { "name": "MySQL",           "categories": ["Databases"],                      "confidence": 100, "version": "",       "source": "http" },
    { "name": "Google Tag Manager", "categories": ["Tag managers"],                "confidence": 100, "version": "",       "source": "http" },
    { "name": "Let's Encrypt",   "categories": ["SSL/TLS certificate authority"],  "confidence": 70,  "version": "",       "source": "tls" }
  ],
  "categories": {
    "Ecommerce": ["WooCommerce"],
    "WordPress plugins": ["WooCommerce", "Yoast SEO"],
    "CMS": ["WordPress"],
    "PaaS": ["WordPress VIP"],
    "SEO": ["Yoast SEO"]
  },
  "meta": { "status_code": 200, "tech_count": 14, "scan_depth": "full" },
  "cached": false,
  "response_ms": 1646
}

That single call is a brief: WooCommerce 11.1.0 on WordPress 7.0.2, running on WordPress VIP managed hosting behind Nginx, with Yoast SEO and Google Tag Manager. The categories map makes the store check a one-line lookup in code — test whether categories["Ecommerce"] contains WooCommerce — and the hosting entry tells you the account size bracket before you have spoken to anyone.

Boolean Yes/No With GET /check?tech=WooCommerce

When all you want is a flag, /check answers directly without making you filter an array:

curl -s "https://detectzestack.p.rapidapi.com/check?url=yithemes.com&tech=WooCommerce" \
  -H "X-RapidAPI-Key: YOUR_KEY" \
  -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" | jq '.'
{
  "domain": "yithemes.com",
  "technology": "WooCommerce",
  "detected": true,
  "confidence": 100,
  "version": "9.3.6",
  "categories": ["Ecommerce", "WordPress plugins"],
  "response_ms": 388,
  "cached": false
}

The tech parameter is case insensitive and the response echoes back the canonical name. Branch on detected.

Reading the Response: Confidence, Version, and Categories

Three fields carry most of the meaning, and each has a failure mode worth naming:

The source field takes values like http, dns, and tls depending on which layer produced the evidence. WooCommerce is only ever http: no DNS record or certificate property reveals a WordPress plugin, so a successful HTML fetch is a hard prerequisite for detecting it at all.

Scanning a List of Domains With POST /analyze/batch

Batch is how you work through a list. It accepts up to 10 URLs per request, scans them concurrently, and gives each item its own error field so one bad domain does not fail the whole batch:

curl -s -X POST "https://detectzestack.p.rapidapi.com/analyze/batch" \
  -H "X-RapidAPI-Key: YOUR_KEY" \
  -H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["woocommerce.com", "yithemes.com", "kinsta.com", "wpbeginner.com"]}' \
  | jq -r '.results[]
      | select(.result.technologies[]?.name == "WooCommerce")
      | .result.domain'

Run against those four real domains, that filter returns woocommerce.com and yithemes.com. Both kinsta.com and wpbeginner.com are WordPress sites and neither is a store — a clean demonstration that the WordPress filter and the WooCommerce filter select genuinely different populations.

Each URL in a batch counts as one request against your monthly quota, so a 10-URL batch costs 10 requests. Batching saves round trips and wall-clock time, not quota. For lists in the hundreds or thousands, the batch scanning guide covers pacing, retries, and cost.

Here is the same idea as a Python pipeline that keeps failed fetches out of your negatives:

import csv
import requests

API = "https://detectzestack.p.rapidapi.com/analyze/batch"
HEADERS = {
    "X-RapidAPI-Key": "YOUR_KEY",
    "X-RapidAPI-Host": "detectzestack.p.rapidapi.com",
    "Content-Type": "application/json",
}
BATCH_SIZE = 10  # API maximum per request


def scan(domains):
    """Scan up to 10 domains, yielding (domain, technologies, status_code)."""
    resp = requests.post(API, headers=HEADERS, json={"urls": domains}, timeout=60)
    resp.raise_for_status()
    for item in resp.json()["results"]:
        result = item.get("result")
        if not result:
            print(f"  skipped {item['url']}: {item.get('error', 'no result')}")
            continue
        status = result.get("meta", {}).get("status_code")
        yield result["domain"], result.get("technologies", []), status


def main():
    with open("domains.txt") as f:
        domains = [line.strip() for line in f if line.strip()]

    stores, inconclusive = [], []
    for i in range(0, len(domains), BATCH_SIZE):
        chunk = domains[i:i + BATCH_SIZE]
        print(f"Scanning {i + 1}-{i + len(chunk)} of {len(domains)}...")
        for domain, techs, status in scan(chunk):
            if status != 200:
                # No page HTML means no WooCommerce assets to match on.
                inconclusive.append(domain)
                print(f"  {domain}: status {status}, inconclusive")
                continue

            by_name = {t["name"]: t for t in techs}
            woo = by_name.get("WooCommerce")
            if not woo:
                continue

            wp = by_name.get("WordPress")
            stores.append({
                "domain": domain,
                "woo_version": woo.get("version", ""),
                "wp_version": wp.get("version", "") if wp else "",
                "stack": ", ".join(sorted(by_name)),
            })
            print(f"  {domain}: WooCommerce {woo.get('version') or 'version unknown'}")

    with open("woocommerce_stores.csv", "w", newline="") as f:
        writer = csv.DictWriter(
            f, fieldnames=["domain", "woo_version", "wp_version", "stack"]
        )
        writer.writeheader()
        writer.writerows(stores)

    print(f"\n{len(stores)} WooCommerce stores out of {len(domains)} domains.")
    print(f"{len(inconclusive)} inconclusive, retry later: {inconclusive}")


if __name__ == "__main__":
    main()

The guard that matters most: check meta.status_code. WooCommerce is detected only from rendered HTML. If a site answers your scan with a 403 or 429 instead of its page, there is no asset path to match and the detection will be missing on a store that is entirely WooCommerce-built. A live scan of barefootbuttons.com while writing this returned "status_code": 403 and zero technologies — which says nothing at all about what that site runs. Treat any non-200 as inconclusive, not negative.

WooCommerce vs Shopify: Telling Ecommerce Platforms Apart

Both are ecommerce platforms and both land in the Ecommerce category, but they are detectable in structurally different ways, and the difference decides how much you can trust a negative result.

  WooCommerce Shopify
Detection layer http only http and dns
Primary signal /wp-content/plugins/woocommerce/ asset path CNAME to Shopify infrastructure; cdn.shopify.com assets
Version available Yes, from the script ?ver= string No plugin version to report
Always paired with WordPress, PHP, MySQL Nothing — the platform is the whole stack
Survives a blocked fetch No Yes, via DNS

That last row is not theoretical. A live scan of deathwishcoffee.com came back with "status_code": 429 — the site rate-limited the request and returned no page — and yet the response still contained:

{
  "name": "Shopify",
  "categories": ["Ecommerce"],
  "confidence": 80,
  "source": "dns"
}

The DNS record identified the platform even though the HTML never arrived. Confidence is 80 rather than 100 precisely because the corroborating HTTP evidence was missing. A WooCommerce store behind the same rate limiter would have returned nothing.

The practical takeaway for anyone building an ecommerce prospect list: a missing Shopify detection on a 200-status scan is fairly strong evidence of absence, while a missing WooCommerce detection on any non-200 scan is no evidence at all. For a fuller side-by-side, see Shopify vs WooCommerce detection and how to detect Shopify.

Practical Use Cases for WooCommerce Detection

Prospecting WordPress Ecommerce Agencies and Plugin Vendors

If you sell WooCommerce extensions, payment integrations, hosting, or store maintenance, the detection is your addressable market filter — but the version field is what makes it a priority queue. A store several minor versions behind current is a care-plan lead; one on the latest release is a growth lead. Layer the surrounding signals from the same /analyze response:

Competitive and Market Share Research

Because the version number is exposed, WooCommerce is one of the few technologies where you can measure adoption of a specific release across a sample rather than just presence. Scan a fixed list on a schedule, record the version each time, and you have a version-distribution curve and an upgrade-lag figure for your segment. The same repeated scans catch platform migrations, the highest-intent moment in the ecommerce sales cycle — tracking technology changes over time covers the mechanics, and ecommerce tech stack analysis covers what else to record alongside the platform.

Limitations and False Negatives to Expect

An absent WooCommerce detection is an unknown, not a no. Four causes, in rough order of how often they bite:

  1. The scan never saw the page. Bot protection, rate limiting, and geo-blocking all produce a non-200 response and therefore zero HTML detections. This is the dominant cause on ecommerce domains specifically, because stores run aggressive bot defences. Check meta.status_code every time.
  2. The scanned page does not load store assets. Scans hit the homepage by default. A business whose homepage is a marketing page with the shop on /shop or a subdomain may not enqueue WooCommerce assets on the URL you scanned. If a domain matters, scan the shop path directly.
  3. Asset rewriting. Optimization plugins and CDN pipelines that concatenate, rename, or proxy scripts can destroy the recognisable path. This usually costs you the version first and the detection second.
  4. Headless WooCommerce. A store using WooCommerce purely as a backend behind a custom frontend serves no WordPress assets on the public site at all. Rare, but growing, and undetectable from the frontend by design.

The discipline that follows is a three-state model in your pipeline — detected, not detected, inconclusive — rather than a boolean column. The Python example above keeps the inconclusive rows in their own list for exactly this reason.

Get Your API Key and Start Detecting

Validate the whole approach before paying for anything. The keyless /demo endpoint runs a full scan on any domain:

curl -s "https://detectzestack.com/demo?url=yithemes.com" \
  | jq '{domain, ecommerce: (.categories["Ecommerce"] // []), status: .meta.status_code}'
{
  "domain": "yithemes.com",
  "ecommerce": [
    "WooCommerce"
  ],
  "status": 200
}

When you are ready for the authenticated endpoints, plans are the standard DetectZeStack tiers, and every URL scanned counts as one request:

Plan Price Requests / month
BasicFree100
Pro$91,000
Ultra$2910,000
Mega$7950,000

Results are cached for 24 hours by default, and a cache hit returns "cached": true with a near-zero response_ms. The free plan's 100 monthly requests are enough to run a real slice of your target list end to end before spending anything.

Conclusion

WooCommerce is invisible at the DNS layer and obvious in the HTML. The whole detection rests on one artifact — the /wp-content/plugins/woocommerce/ asset path, with the version riding along in the ?ver= query string — and once you know that, both the capability and the limit follow directly. You get a plugin version on almost every hit, which is unusual and genuinely useful for segmentation. You get nothing at all when the fetch fails, which is common on ecommerce domains and will silently poison a list that treats absence as a negative.

Start with /demo on a store you already know, use /check when you want a boolean, /analyze when you want the surrounding stack, and /analyze/batch when you have a list. Keep the status_code guard in every pipeline, and what you build will be a list you can actually act on.

Related Reading

Start Detecting WooCommerce Stores Today

100 free API requests/month. No credit card required. Detect WooCommerce and thousands of other technologies.

Get Your Free API Key

Get API updates and tech detection tips

Join the mailing list. No spam, unsubscribe anytime.